feat: 实现命令面板、颜色取色、JSON格式化和系统信息功能
- 重构项目架构,采用四层架构模式 (Command → Service → Platform → Utils) - 实现命令面板功能,支持快捷搜索和特征分类 - 添加颜色取色功能,支持屏幕像素颜色获取 - 添加JSON格式化功能,支持JSON格式化和压缩 - 添加系统信息功能,显示操作系统和硬件信息 - 移除旧的状态文档和无用配置文件
This commit is contained in:
2064
src-tauri/Cargo.lock
generated
2064
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -15,12 +15,26 @@ name = "tauri_app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.4.0", features = [] }
|
||||
tauri-build = { version = "2.4", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2.4.0", features = [] }
|
||||
tauri-plugin-opener = "2.5.0"
|
||||
tauri = { version = "2.4", features = [] }
|
||||
tauri-plugin-opener = "2.5"
|
||||
tauri-plugin-global-shortcut = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
serde_json = { version = "1", features = ["preserve_order"] }
|
||||
sysinfo = "0.30"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.58", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_UI_HiDpi",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
"Win32_System_StationsAndDesktops",
|
||||
"Win32_System_SystemInformation",
|
||||
"Win32_Storage_FileSystem",
|
||||
] }
|
||||
wmi = "0.14"
|
||||
serde_derive = "1.0"
|
||||
|
||||
@@ -2,9 +2,14 @@
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"windows": ["main", "picker_overlay"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-hide",
|
||||
"core:window:allow-show",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-set-focus",
|
||||
"core:window:allow-is-visible",
|
||||
"opener:default"
|
||||
]
|
||||
}
|
||||
|
||||
110
src-tauri/src/commands/json_format_commands.rs
Normal file
110
src-tauri/src/commands/json_format_commands.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
//! JSON 格式化命令
|
||||
//!
|
||||
//! 定义 JSON 格式化相关的 Tauri 命令
|
||||
|
||||
use crate::models::json_format::{JsonFormatConfig, JsonFormatResult, JsonValidateResult};
|
||||
use crate::services::json_format_service::JsonFormatService;
|
||||
|
||||
/// 格式化 JSON 命令
|
||||
///
|
||||
/// Tauri 命令,用于从前端调用 JSON 格式化功能
|
||||
///
|
||||
/// # 参数
|
||||
///
|
||||
/// * `input` - 输入的 JSON 字符串
|
||||
/// * `config` - 格式化配置
|
||||
///
|
||||
/// # 返回
|
||||
///
|
||||
/// 返回格式化结果,包含成功标志、结果字符串和错误信息
|
||||
///
|
||||
/// # 前端调用示例
|
||||
///
|
||||
/// ```typescript
|
||||
/// import { invoke } from '@tauri-apps/api/tauri';
|
||||
///
|
||||
/// const result = await invoke('format_json', {
|
||||
/// input: '{"name":"test","value":123}',
|
||||
/// config: {
|
||||
/// indent: 2,
|
||||
/// sort_keys: false,
|
||||
/// mode: 'pretty'
|
||||
/// }
|
||||
/// });
|
||||
/// console.log(result.success); // true
|
||||
/// console.log(result.result); // 格式化后的 JSON
|
||||
/// ```
|
||||
#[tauri::command]
|
||||
pub fn format_json(input: String, config: JsonFormatConfig) -> JsonFormatResult {
|
||||
JsonFormatService::format(&input, &config)
|
||||
.unwrap_or_else(|e| JsonFormatResult {
|
||||
success: false,
|
||||
result: String::new(),
|
||||
error: Some(e.to_string()),
|
||||
is_valid: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// 验证 JSON 命令
|
||||
///
|
||||
/// 验证输入的字符串是否为有效的 JSON
|
||||
///
|
||||
/// # 参数
|
||||
///
|
||||
/// * `input` - 输入的 JSON 字符串
|
||||
///
|
||||
/// # 返回
|
||||
///
|
||||
/// 返回验证结果
|
||||
///
|
||||
/// # 前端调用示例
|
||||
///
|
||||
/// ```typescript
|
||||
/// import { invoke } from '@tauri-apps/api/tauri';
|
||||
///
|
||||
/// const result = await invoke('validate_json', {
|
||||
/// input: '{"valid": true}'
|
||||
/// });
|
||||
/// console.log(result.is_valid); // true
|
||||
/// ```
|
||||
#[tauri::command]
|
||||
pub fn validate_json(input: String) -> JsonValidateResult {
|
||||
JsonFormatService::validate(&input).unwrap_or_else(|e| JsonValidateResult {
|
||||
is_valid: false,
|
||||
error_message: Some(e.to_string()),
|
||||
error_line: None,
|
||||
error_column: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// 压缩 JSON 命令
|
||||
///
|
||||
/// 去除 JSON 中的所有空格和换行
|
||||
///
|
||||
/// # 参数
|
||||
///
|
||||
/// * `input` - 输入的 JSON 字符串
|
||||
///
|
||||
/// # 返回
|
||||
///
|
||||
/// 返回压缩后的 JSON
|
||||
///
|
||||
/// # 前端调用示例
|
||||
///
|
||||
/// ```typescript
|
||||
/// import { invoke } from '@tauri-apps/api/tauri';
|
||||
///
|
||||
/// const result = await invoke('compact_json', {
|
||||
/// input: '{ "name" : "test" }'
|
||||
/// });
|
||||
/// console.log(result.result); // '{"name":"test"}'
|
||||
/// ```
|
||||
#[tauri::command]
|
||||
pub fn compact_json(input: String) -> JsonFormatResult {
|
||||
JsonFormatService::compact(&input).unwrap_or_else(|e| JsonFormatResult {
|
||||
success: false,
|
||||
result: String::new(),
|
||||
error: Some(e.to_string()),
|
||||
is_valid: false,
|
||||
})
|
||||
}
|
||||
8
src-tauri/src/commands/mod.rs
Normal file
8
src-tauri/src/commands/mod.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
//! Tauri 命令处理层
|
||||
//!
|
||||
//! 定义与前端交互的 Tauri 命令,作为前端和业务逻辑之间的适配器
|
||||
|
||||
pub mod json_format_commands;
|
||||
pub mod picker_color_commands;
|
||||
pub mod system_info_commands;
|
||||
pub mod window_commands;
|
||||
311
src-tauri/src/commands/picker_color_commands.rs
Normal file
311
src-tauri/src/commands/picker_color_commands.rs
Normal file
@@ -0,0 +1,311 @@
|
||||
//! 取色器命令
|
||||
//!
|
||||
//! 提供完整的屏幕取色功能(使用透明遮罩窗口方案)
|
||||
//!
|
||||
//! # 架构设计
|
||||
//!
|
||||
//! - **后端(Rust)**:负责窗口创建/销毁和屏幕取色
|
||||
//! - **前端(HTML/CSS/JS)**:负责光标样式和用户交互
|
||||
//!
|
||||
//! # 优势
|
||||
//!
|
||||
//! 使用透明全屏遮罩 + CSS 光标,完美解决 Windows 系统光标竞争问题,
|
||||
//! 避免了传统的 SetCursor API 与系统的 race condition。
|
||||
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use serde::Serialize;
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ScreenRegionRgba {
|
||||
pub width: i32,
|
||||
pub height: i32,
|
||||
/// RGBA 字节数组(长度 = width * height * 4)
|
||||
pub data: Vec<u8>,
|
||||
/// 中心点颜色(从 data 直接计算),便于前端展示
|
||||
pub center: crate::models::color::RgbInfo,
|
||||
pub center_hex: String,
|
||||
}
|
||||
|
||||
/// 预热取色器窗口(隐藏创建)
|
||||
///
|
||||
/// 目的:避免第一次显示 WebView 时的“白屏闪一下”(WebView 首帧默认白底/初始化抖动)。
|
||||
pub(crate) fn prewarm_picker_window(app: &AppHandle) -> Result<(), String> {
|
||||
use tauri::WebviewWindowBuilder;
|
||||
|
||||
if app.get_webview_window("picker_overlay").is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
WebviewWindowBuilder::new(
|
||||
app,
|
||||
"picker_overlay",
|
||||
tauri::WebviewUrl::App("picker.html".into()),
|
||||
)
|
||||
.title("取色器")
|
||||
.fullscreen(true)
|
||||
.transparent(true)
|
||||
.always_on_top(true)
|
||||
.decorations(false)
|
||||
.skip_taskbar(true)
|
||||
.resizable(false)
|
||||
// 关键:先不可见创建,等真正开始取色时再 show
|
||||
.visible(false)
|
||||
// 尽可能早地把背景设为透明,降低首帧白底概率
|
||||
.initialization_script(
|
||||
r#"
|
||||
try {
|
||||
document.documentElement.style.background = 'transparent';
|
||||
document.body && (document.body.style.background = 'transparent');
|
||||
} catch (_) {}
|
||||
"#,
|
||||
)
|
||||
.build()
|
||||
.map_err(|e| format!("预热取色器窗口失败: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 启动取色器(推荐使用 ⭐)
|
||||
///
|
||||
/// 打开透明全屏遮罩窗口,光标由前端 CSS 控制。
|
||||
///
|
||||
/// # 工作流程
|
||||
///
|
||||
/// 1. 后端隐藏主窗口
|
||||
/// 2. 后端创建全屏透明遮罩窗口
|
||||
/// 3. **前端通过 CSS 设置 `cursor: crosshair` 控制光标**
|
||||
/// 4. 用户点击任意位置,前端调用 `pick_color_at_point` 取色
|
||||
/// 5. 取色完成后前端调用 `close_picker_window` 关闭遮罩窗口
|
||||
///
|
||||
/// # 参数
|
||||
///
|
||||
/// * `app` - Tauri 应用句柄
|
||||
///
|
||||
/// # 前端实现示例
|
||||
///
|
||||
/// picker.html:
|
||||
/// ```html
|
||||
/// <style>
|
||||
/// body {
|
||||
/// width: 100vw;
|
||||
/// height: 100vh;
|
||||
/// background-color: transparent;
|
||||
/// cursor: crosshair; /* 关键!前端控制光标 */
|
||||
/// }
|
||||
/// </style>
|
||||
/// <script>
|
||||
/// async function handleClick(e) {
|
||||
/// const color = await invoke('pick_color_at_point', {
|
||||
/// x: e.clientX,
|
||||
/// y: e.clientY
|
||||
/// });
|
||||
/// console.log('HEX:', color.hex);
|
||||
/// await invoke('close_picker_window');
|
||||
/// }
|
||||
/// document.addEventListener('click', handleClick);
|
||||
/// </script>
|
||||
/// ```
|
||||
#[tauri::command]
|
||||
pub async fn start_color_picker(app: AppHandle) -> Result<(), String> {
|
||||
// 先隐藏主窗口
|
||||
if let Some(main_window) = app.get_webview_window("main") {
|
||||
main_window.hide().map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
// 等待窗口完全隐藏
|
||||
thread::sleep(Duration::from_millis(150));
|
||||
|
||||
// 打开透明遮罩窗口
|
||||
open_picker_window(app).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 打开取色器遮罩窗口(内部辅助函数)
|
||||
///
|
||||
/// # 参数
|
||||
///
|
||||
/// * `app` - Tauri 应用句柄
|
||||
///
|
||||
/// # 返回
|
||||
///
|
||||
/// 返回成功或错误信息
|
||||
pub(crate) async fn open_picker_window(app: AppHandle) -> Result<(), String> {
|
||||
use tauri::WebviewWindowBuilder;
|
||||
|
||||
// 检查窗口是否已存在
|
||||
if let Some(existing) = app.get_webview_window("picker_overlay") {
|
||||
// 复用已存在窗口:避免频繁 close/build 引起的白屏闪烁
|
||||
existing
|
||||
.show()
|
||||
.and_then(|_| existing.set_focus())
|
||||
.map_err(|e| format!("显示取色器窗口失败: {}", e))?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 创建全屏透明遮罩窗口
|
||||
let picker_window = WebviewWindowBuilder::new(
|
||||
&app,
|
||||
"picker_overlay",
|
||||
tauri::WebviewUrl::App("picker.html".into()),
|
||||
)
|
||||
.title("取色器")
|
||||
.fullscreen(true)
|
||||
.transparent(true)
|
||||
.always_on_top(true)
|
||||
.decorations(false)
|
||||
.skip_taskbar(true)
|
||||
.resizable(false)
|
||||
// 先不可见创建,再显式 show(降低首帧白底闪烁)
|
||||
.visible(false)
|
||||
.initialization_script(
|
||||
r#"
|
||||
try {
|
||||
document.documentElement.style.background = 'transparent';
|
||||
document.body && (document.body.style.background = 'transparent');
|
||||
} catch (_) {}
|
||||
"#,
|
||||
)
|
||||
.build()
|
||||
.map_err(|e| format!("创建取色器窗口失败: {}", e))?;
|
||||
|
||||
// 显式 show + focus,确保在某些系统上立即可见
|
||||
picker_window
|
||||
.show()
|
||||
.and_then(|_| picker_window.set_focus())
|
||||
.map_err(|e| format!("显示取色器窗口失败: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 关闭取色器遮罩窗口
|
||||
///
|
||||
/// # 前端调用示例
|
||||
///
|
||||
/// ```typescript
|
||||
/// import { invoke } from '@tauri-apps/api/tauri';
|
||||
///
|
||||
/// await invoke('close_picker_window');
|
||||
/// ```
|
||||
#[tauri::command]
|
||||
pub async fn close_picker_window(app: AppHandle) -> Result<(), String> {
|
||||
if let Some(window) = app.get_webview_window("picker_overlay") {
|
||||
// 不 close:只 hide,避免窗口销毁/重建导致的白屏闪烁
|
||||
// 先隐藏遮罩窗口,再恢复主窗口,过渡更自然
|
||||
window
|
||||
.hide()
|
||||
.map_err(|e| format!("隐藏取色器窗口失败: {}", e))?;
|
||||
|
||||
// 恢复主窗口
|
||||
if let Some(main_window) = app.get_webview_window("main") {
|
||||
main_window
|
||||
.show()
|
||||
.and_then(|_| main_window.set_focus())
|
||||
.map_err(|e| format!("显示主窗口失败: {}", e))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 在指定坐标取色
|
||||
///
|
||||
/// # 参数
|
||||
///
|
||||
/// * `x` - 屏幕 X 坐标
|
||||
/// * `y` - 屏幕 Y 坐标
|
||||
///
|
||||
/// # 返回
|
||||
///
|
||||
/// 返回颜色信息
|
||||
#[tauri::command]
|
||||
pub async fn pick_color_at_point(
|
||||
x: i32,
|
||||
y: i32,
|
||||
) -> Result<crate::models::color::ColorInfo, String> {
|
||||
let (r, g, b) = crate::utils::screen::WindowsScreen::get_pixel_color(x, y)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(crate::models::color::ColorInfo::new(r, g, b, x, y))
|
||||
}
|
||||
|
||||
/// 获取“最上层应用”的颜色(排除取色遮罩自身的影响)
|
||||
///
|
||||
/// 在 Windows 上,如果遮罩窗口位于最顶层,`GetPixel` 读到的是**合成后的颜色**,
|
||||
/// 即可能包含遮罩的透明叠加,从而导致取色偏暗/偏差。
|
||||
///
|
||||
/// 这里的策略是:先隐藏遮罩窗口,等待一帧左右让桌面合成刷新,再读取屏幕像素。
|
||||
#[tauri::command]
|
||||
pub async fn pick_color_at_point_topmost(
|
||||
app: AppHandle,
|
||||
x: i32,
|
||||
y: i32,
|
||||
) -> Result<crate::models::color::ColorInfo, String> {
|
||||
// 先隐藏遮罩窗口(不 close,避免白屏闪烁)
|
||||
if let Some(overlay) = app.get_webview_window("picker_overlay") {
|
||||
let _ = overlay.hide();
|
||||
}
|
||||
|
||||
// 给桌面合成一点时间刷新(过短可能还会读到遮罩叠加结果)
|
||||
thread::sleep(Duration::from_millis(35));
|
||||
|
||||
let (r, g, b) = crate::utils::screen::WindowsScreen::get_pixel_color(x, y)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(crate::models::color::ColorInfo::new(r, g, b, x, y))
|
||||
}
|
||||
|
||||
/// 捕获屏幕区域像素(用于前端放大镜)
|
||||
#[tauri::command]
|
||||
pub async fn capture_screen_region_rgba(
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
) -> Result<ScreenRegionRgba, String> {
|
||||
let data = crate::utils::screen::WindowsScreen::capture_region_rgba(x, y, width, height)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let cx = width / 2;
|
||||
let cy = height / 2;
|
||||
let idx = ((cy as usize) * (width as usize) + (cx as usize)) * 4;
|
||||
let r = data[idx];
|
||||
let g = data[idx + 1];
|
||||
let b = data[idx + 2];
|
||||
|
||||
Ok(ScreenRegionRgba {
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
center: crate::models::color::RgbInfo { r, g, b },
|
||||
center_hex: format!("#{:02X}{:02X}{:02X}", r, g, b),
|
||||
})
|
||||
}
|
||||
|
||||
/// RGB 转 HSL 命令
|
||||
///
|
||||
/// 将 RGB 颜色值转换为 HSL 颜色值
|
||||
///
|
||||
/// # 参数
|
||||
///
|
||||
/// * `r` - 红色分量 (0-255)
|
||||
/// * `g` - 绿色分量 (0-255)
|
||||
/// * `b` - 蓝色分量 (0-255)
|
||||
///
|
||||
/// # 返回
|
||||
///
|
||||
/// 返回 HSL 颜色值
|
||||
///
|
||||
/// # 前端调用示例
|
||||
///
|
||||
/// ```typescript
|
||||
/// import { invoke } from '@tauri-apps/api/tauri';
|
||||
///
|
||||
/// const hsl = await invoke('rgb_to_hsl', { r: 255, g: 0, b: 0 });
|
||||
/// console.log(hsl); // { h: 0, s: 100, l: 50 }
|
||||
/// ```
|
||||
#[tauri::command]
|
||||
pub fn rgb_to_hsl(r: u8, g: u8, b: u8) -> crate::models::color::HslInfo {
|
||||
crate::utils::color_conversion::rgb_to_hsl(r, g, b)
|
||||
}
|
||||
29
src-tauri/src/commands/system_info_commands.rs
Normal file
29
src-tauri/src/commands/system_info_commands.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
//! 系统信息命令
|
||||
//!
|
||||
//! 定义系统信息相关的 Tauri 命令
|
||||
|
||||
use crate::models::system_info::SystemInfo;
|
||||
use crate::services::system_info_service::SystemInfoService;
|
||||
|
||||
/// 获取系统信息命令
|
||||
///
|
||||
/// Tauri 命令,用于从前端调用系统信息查询功能
|
||||
///
|
||||
/// # 返回
|
||||
///
|
||||
/// 返回包含所有系统信息的结构体
|
||||
///
|
||||
/// # 前端调用示例
|
||||
///
|
||||
/// ```typescript
|
||||
/// import { invoke } from '@tauri-apps/api/tauri';
|
||||
///
|
||||
/// const info = await invoke('get_system_info');
|
||||
/// console.log(info.os.name); // "Windows"
|
||||
/// console.log(info.cpu.model); // "Intel Core i7..."
|
||||
/// console.log(info.memory.total_gb); // 16.0
|
||||
/// ```
|
||||
#[tauri::command]
|
||||
pub fn get_system_info() -> Result<SystemInfo, String> {
|
||||
SystemInfoService::get_system_info().map_err(|e| e.to_string())
|
||||
}
|
||||
70
src-tauri/src/commands/window_commands.rs
Normal file
70
src-tauri/src/commands/window_commands.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
//! 窗口命令
|
||||
//!
|
||||
//! 定义窗口管理相关的 Tauri 命令
|
||||
|
||||
use tauri::Window;
|
||||
use crate::services::window_service::WindowService;
|
||||
|
||||
/// 切换窗口显示/隐藏命令
|
||||
///
|
||||
/// 根据窗口当前状态切换显示或隐藏
|
||||
///
|
||||
/// # 参数
|
||||
///
|
||||
/// * `window` - Tauri 窗口对象,自动由框架注入
|
||||
///
|
||||
/// # 前端调用示例
|
||||
///
|
||||
/// ```typescript
|
||||
/// import { invoke } from '@tauri-apps/api/tauri';
|
||||
///
|
||||
/// await invoke('toggle_window');
|
||||
/// ```
|
||||
#[tauri::command]
|
||||
pub fn toggle_window(window: Window) -> Result<(), String> {
|
||||
WindowService::toggle_window(&window)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 隐藏窗口命令
|
||||
///
|
||||
/// 将窗口隐藏,使其不再可见
|
||||
///
|
||||
/// # 参数
|
||||
///
|
||||
/// * `window` - Tauri 窗口对象,自动由框架注入
|
||||
///
|
||||
/// # 前端调用示例
|
||||
///
|
||||
/// ```typescript
|
||||
/// import { invoke } from '@tauri-apps/api/tauri';
|
||||
///
|
||||
/// await invoke('hide_window');
|
||||
/// ```
|
||||
#[tauri::command]
|
||||
pub fn hide_window(window: Window) -> Result<(), String> {
|
||||
WindowService::hide_window(&window)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 显示窗口命令
|
||||
///
|
||||
/// 显示窗口并将其设置为焦点窗口
|
||||
///
|
||||
/// # 参数
|
||||
///
|
||||
/// * `window` - Tauri 窗口对象,自动由框架注入
|
||||
///
|
||||
/// # 前端调用示例
|
||||
///
|
||||
/// ```typescript
|
||||
/// import { invoke } from '@tauri-apps/api/tauri';
|
||||
///
|
||||
/// await invoke('show_window');
|
||||
/// ```
|
||||
#[tauri::command]
|
||||
pub fn show_window(window: Window) -> Result<(), String> {
|
||||
WindowService::show_window(&window)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
76
src-tauri/src/error.rs
Normal file
76
src-tauri/src/error.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
//! 错误处理模块
|
||||
//!
|
||||
//! 提供统一的错误类型定义和错误处理机制
|
||||
|
||||
use std::fmt;
|
||||
|
||||
/// 应用统一错误类型
|
||||
///
|
||||
/// 定义了应用中可能出现的所有错误类型,每个错误都携带详细的错误信息
|
||||
#[derive(Debug)]
|
||||
pub enum AppError {
|
||||
/// 平台不支持
|
||||
///
|
||||
/// 表示当前平台不支持某项功能
|
||||
PlatformNotSupported(String),
|
||||
|
||||
/// 屏幕访问失败
|
||||
///
|
||||
/// 表示无法获取或访问屏幕设备
|
||||
ScreenAccessFailed(String),
|
||||
|
||||
/// 窗口操作失败
|
||||
///
|
||||
/// 表示窗口显示、隐藏或聚焦等操作失败
|
||||
WindowOperationFailed(String),
|
||||
|
||||
/// 光标操作失败
|
||||
///
|
||||
/// 表示光标设置或恢复操作失败
|
||||
CursorOperationFailed(String),
|
||||
|
||||
/// 无效的颜色数据
|
||||
///
|
||||
/// 表示提供的颜色数据格式不正确或超出范围
|
||||
InvalidColorData(String),
|
||||
|
||||
/// 颜色转换失败
|
||||
///
|
||||
/// 表示颜色空间转换(如 RGB 到 HSL)失败
|
||||
ColorConversionFailed(String),
|
||||
|
||||
/// 系统信息获取失败
|
||||
///
|
||||
/// 表示获取系统信息时失败
|
||||
SystemInfoFailed(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for AppError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
AppError::PlatformNotSupported(msg) => write!(f, "平台不支持: {}", msg),
|
||||
AppError::ScreenAccessFailed(msg) => write!(f, "屏幕访问失败: {}", msg),
|
||||
AppError::WindowOperationFailed(msg) => write!(f, "窗口操作失败: {}", msg),
|
||||
AppError::CursorOperationFailed(msg) => write!(f, "光标操作失败: {}", msg),
|
||||
AppError::InvalidColorData(msg) => write!(f, "颜色数据无效: {}", msg),
|
||||
AppError::ColorConversionFailed(msg) => write!(f, "颜色转换失败: {}", msg),
|
||||
AppError::SystemInfoFailed(msg) => write!(f, "系统信息获取失败: {}", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for AppError {}
|
||||
|
||||
/// 应用统一返回类型
|
||||
///
|
||||
/// 用于所有可能返回错误的函数,简化错误处理代码
|
||||
pub type AppResult<T> = Result<T, AppError>;
|
||||
|
||||
/// 为 Tauri 实现自动转换
|
||||
///
|
||||
/// 允许 `AppError` 自动转换为 `String`,以满足 Tauri 命令的要求
|
||||
impl From<AppError> for String {
|
||||
fn from(error: AppError) -> String {
|
||||
error.to_string()
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,54 @@
|
||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||
#[tauri::command]
|
||||
fn greet(name: &str) -> String {
|
||||
format!("Hello, {}! You've been greeted from Rust!", name)
|
||||
}
|
||||
//! Tauri 应用入口
|
||||
//!
|
||||
//! 提供应用初始化和模块组装功能
|
||||
|
||||
// 模块声明
|
||||
mod commands;
|
||||
mod error;
|
||||
mod models;
|
||||
mod platforms;
|
||||
mod services;
|
||||
mod utils;
|
||||
|
||||
// 重新导出常用类型
|
||||
pub use error::{AppError, AppResult};
|
||||
|
||||
/// 运行 Tauri 应用
|
||||
///
|
||||
/// 初始化应用、注册插件、设置全局快捷键并启动应用
|
||||
///
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.invoke_handler(tauri::generate_handler![greet])
|
||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||
.setup(|app| {
|
||||
// 预热取色器窗口:避免第一次取色出现“白屏闪一下”
|
||||
// 窗口会以 hidden 状态创建,不会影响用户体验
|
||||
let _ = commands::picker_color_commands::prewarm_picker_window(app.handle());
|
||||
|
||||
utils::shortcut::register_global_shortcuts(app)?;
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
// window 窗口操作
|
||||
commands::window_commands::toggle_window,
|
||||
commands::window_commands::hide_window,
|
||||
commands::window_commands::show_window,
|
||||
// 取色器命令
|
||||
commands::picker_color_commands::rgb_to_hsl,
|
||||
commands::picker_color_commands::start_color_picker,
|
||||
commands::picker_color_commands::close_picker_window,
|
||||
commands::picker_color_commands::pick_color_at_point,
|
||||
commands::picker_color_commands::pick_color_at_point_topmost,
|
||||
commands::picker_color_commands::capture_screen_region_rgba,
|
||||
// Json格式化命令
|
||||
commands::json_format_commands::format_json,
|
||||
commands::json_format_commands::validate_json,
|
||||
commands::json_format_commands::compact_json,
|
||||
// 操作系统信息命令
|
||||
commands::system_info_commands::get_system_info,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
.expect("运行 Tauri 应用时出错");
|
||||
}
|
||||
|
||||
91
src-tauri/src/models/color.rs
Normal file
91
src-tauri/src/models/color.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
//! 颜色数据模型
|
||||
//!
|
||||
//! 定义颜色相关的数据结构,包括 RGB、HSL 和完整的颜色信息
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::utils::color_conversion;
|
||||
|
||||
/// 颜色信息
|
||||
///
|
||||
/// 包含颜色的完整信息,支持多种颜色格式和屏幕坐标
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ColorInfo {
|
||||
/// 十六进制颜色值(格式:#RRGGBB)
|
||||
pub hex: String,
|
||||
/// RGB 颜色值
|
||||
pub rgb: RgbInfo,
|
||||
/// HSL 颜色值
|
||||
pub hsl: HslInfo,
|
||||
/// 屏幕坐标 X(像素)
|
||||
pub x: i32,
|
||||
/// 屏幕坐标 Y(像素)
|
||||
pub y: i32,
|
||||
}
|
||||
|
||||
impl ColorInfo {
|
||||
/// 从 RGB 值创建颜色信息
|
||||
///
|
||||
/// # 参数
|
||||
///
|
||||
/// * `r` - 红色分量 (0-255)
|
||||
/// * `g` - 绿色分量 (0-255)
|
||||
/// * `b` - 蓝色分量 (0-255)
|
||||
/// * `x` - 屏幕坐标 X(像素)
|
||||
/// * `y` - 屏幕坐标 Y(像素)
|
||||
///
|
||||
/// # 返回
|
||||
///
|
||||
/// 返回包含完整颜色信息的 `ColorInfo` 实例
|
||||
///
|
||||
/// # 示例
|
||||
///
|
||||
/// ```no_run
|
||||
/// use crate::models::color::ColorInfo;
|
||||
///
|
||||
/// let color = ColorInfo::new(255, 0, 0, 100, 200);
|
||||
/// assert_eq!(color.hex, "#FF0000");
|
||||
/// ```
|
||||
pub fn new(r: u8, g: u8, b: u8, x: i32, y: i32) -> Self {
|
||||
let hex = format!("#{:02X}{:02X}{:02X}", r, g, b);
|
||||
|
||||
Self {
|
||||
hex,
|
||||
rgb: RgbInfo { r, g, b },
|
||||
hsl: color_conversion::rgb_to_hsl(r, g, b),
|
||||
x,
|
||||
y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// RGB 颜色
|
||||
///
|
||||
/// 表示 RGB 颜色模式的颜色值
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RgbInfo {
|
||||
/// 红色分量 (0-255)
|
||||
pub r: u8,
|
||||
/// 绿色分量 (0-255)
|
||||
pub g: u8,
|
||||
/// 蓝色分量 (0-255)
|
||||
pub b: u8,
|
||||
}
|
||||
|
||||
/// HSL 颜色
|
||||
///
|
||||
/// 表示 HSL 颜色模式的颜色值
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HslInfo {
|
||||
/// 色相 (0-360)
|
||||
///
|
||||
/// 表示颜色在色轮上的角度,0° 为红色,120° 为绿色,240° 为蓝色
|
||||
pub h: u16,
|
||||
/// 饱和度 (0-100)
|
||||
///
|
||||
/// 表示颜色的鲜艳程度,0% 为灰色,100% 为完全饱和
|
||||
pub s: u8,
|
||||
/// 亮度 (0-100)
|
||||
///
|
||||
/// 表示颜色的明暗程度,0% 为黑色,100% 为白色
|
||||
pub l: u8,
|
||||
}
|
||||
85
src-tauri/src/models/json_format.rs
Normal file
85
src-tauri/src/models/json_format.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
//! JSON 格式化相关数据模型
|
||||
//!
|
||||
//! 定义 JSON 格式化工具使用的数据结构
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// JSON 格式化配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JsonFormatConfig {
|
||||
/// 缩进空格数(默认 2)
|
||||
#[serde(default = "default_indent")]
|
||||
pub indent: u32,
|
||||
|
||||
/// 是否对 key 进行排序
|
||||
#[serde(default)]
|
||||
pub sort_keys: bool,
|
||||
|
||||
/// 格式化模式
|
||||
#[serde(default)]
|
||||
pub mode: FormatMode,
|
||||
}
|
||||
|
||||
/// 默认缩进空格数
|
||||
fn default_indent() -> u32 {
|
||||
2
|
||||
}
|
||||
|
||||
/// JSON 格式化模式
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub enum FormatMode {
|
||||
/// 标准格式化(美化)
|
||||
#[serde(rename = "pretty")]
|
||||
Pretty,
|
||||
/// 压缩格式(去除空格和换行)
|
||||
#[serde(rename = "compact")]
|
||||
Compact,
|
||||
}
|
||||
|
||||
impl Default for FormatMode {
|
||||
fn default() -> Self {
|
||||
Self::Pretty
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for JsonFormatConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
indent: default_indent(),
|
||||
sort_keys: false,
|
||||
mode: FormatMode::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// JSON 格式化结果
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JsonFormatResult {
|
||||
/// 是否成功
|
||||
pub success: bool,
|
||||
|
||||
/// 格式化后的 JSON 字符串
|
||||
pub result: String,
|
||||
|
||||
/// 错误信息(如果失败)
|
||||
pub error: Option<String>,
|
||||
|
||||
/// 原始 JSON 是否有效
|
||||
pub is_valid: bool,
|
||||
}
|
||||
|
||||
/// JSON 验证结果
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JsonValidateResult {
|
||||
/// 是否有效的 JSON
|
||||
pub is_valid: bool,
|
||||
|
||||
/// 错误信息(如果无效)
|
||||
pub error_message: Option<String>,
|
||||
|
||||
/// 错误位置(行号,从 1 开始)
|
||||
pub error_line: Option<usize>,
|
||||
|
||||
/// 错误位置(列号,从 1 开始)
|
||||
pub error_column: Option<usize>,
|
||||
}
|
||||
7
src-tauri/src/models/mod.rs
Normal file
7
src-tauri/src/models/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
//! 数据模型模块
|
||||
//!
|
||||
//! 定义应用中使用的数据结构
|
||||
|
||||
pub mod color;
|
||||
pub mod json_format;
|
||||
pub mod system_info;
|
||||
170
src-tauri/src/models/system_info.rs
Normal file
170
src-tauri/src/models/system_info.rs
Normal file
@@ -0,0 +1,170 @@
|
||||
//! 系统信息相关数据模型
|
||||
//!
|
||||
//! 定义系统信息工具使用的数据结构
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 系统信息(完整版)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SystemInfo {
|
||||
/// 操作系统信息
|
||||
pub os: OsInfo,
|
||||
/// 硬件信息(主板、BIOS)
|
||||
pub hardware: HardwareInfo,
|
||||
/// CPU 信息
|
||||
pub cpu: CpuInfo,
|
||||
/// 内存信息
|
||||
pub memory: MemoryInfo,
|
||||
/// GPU 信息列表
|
||||
pub gpu: Vec<GpuInfo>,
|
||||
/// 磁盘信息列表
|
||||
pub disks: Vec<DiskInfo>,
|
||||
/// 计算机信息
|
||||
pub computer: ComputerInfo,
|
||||
/// 显示器信息
|
||||
pub display: DisplayInfo,
|
||||
/// 网络信息
|
||||
pub network: NetworkInfo,
|
||||
}
|
||||
|
||||
/// 操作系统信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OsInfo {
|
||||
/// 操作系统名称
|
||||
pub name: String,
|
||||
/// 操作系统版本
|
||||
pub version: String,
|
||||
/// 系统架构
|
||||
pub arch: String,
|
||||
/// 内核版本
|
||||
pub kernel_version: String,
|
||||
/// 主机名
|
||||
pub host_name: String,
|
||||
/// 运行时间(可读格式)
|
||||
pub uptime_readable: String,
|
||||
}
|
||||
|
||||
/// 硬件信息(主板、BIOS)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HardwareInfo {
|
||||
/// 制造商
|
||||
pub manufacturer: String,
|
||||
/// 型号
|
||||
pub model: String,
|
||||
/// BIOS 版本
|
||||
pub bios_version: String,
|
||||
/// BIOS 序列号
|
||||
pub bios_serial: String,
|
||||
}
|
||||
|
||||
/// CPU 信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CpuInfo {
|
||||
/// CPU 型号
|
||||
pub model: String,
|
||||
/// 物理核心数
|
||||
pub cores: usize,
|
||||
/// 逻辑处理器数
|
||||
pub processors: usize,
|
||||
/// 最大频率 (MHz)
|
||||
pub max_frequency: u32,
|
||||
/// 当前使用率 (0-100)
|
||||
pub usage_percent: f32,
|
||||
}
|
||||
|
||||
/// 内存信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MemoryInfo {
|
||||
/// 总内存 (GB)
|
||||
pub total_gb: f64,
|
||||
/// 可用内存 (GB)
|
||||
pub available_gb: f64,
|
||||
/// 已用内存 (GB)
|
||||
pub used_gb: f64,
|
||||
/// 使用率 (0-100)
|
||||
pub usage_percent: f64,
|
||||
}
|
||||
|
||||
/// GPU 信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GpuInfo {
|
||||
/// GPU 名称
|
||||
pub name: String,
|
||||
/// 显存 (GB)
|
||||
pub vram_gb: f64,
|
||||
/// 驱动版本
|
||||
pub driver_version: String,
|
||||
}
|
||||
|
||||
/// 磁盘信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DiskInfo {
|
||||
/// 盘符 (如 "C:")
|
||||
pub drive_letter: String,
|
||||
/// 卷标
|
||||
pub volume_label: String,
|
||||
/// 文件系统类型
|
||||
pub file_system: String,
|
||||
/// 总容量 (GB)
|
||||
pub total_gb: f64,
|
||||
/// 可用空间 (GB)
|
||||
pub available_gb: f64,
|
||||
/// 已用空间 (GB)
|
||||
pub used_gb: f64,
|
||||
/// 使用率 (0-100)
|
||||
pub usage_percent: f64,
|
||||
}
|
||||
|
||||
/// 计算机信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ComputerInfo {
|
||||
/// 计算机名称
|
||||
pub name: String,
|
||||
/// 用户名
|
||||
pub username: String,
|
||||
/// 域名/工作组
|
||||
pub domain: String,
|
||||
/// 制造商
|
||||
pub manufacturer: String,
|
||||
/// 型号
|
||||
pub model: String,
|
||||
/// 序列号
|
||||
pub serial_number: String,
|
||||
}
|
||||
|
||||
/// 显示器信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DisplayInfo {
|
||||
/// 屏幕数量
|
||||
pub monitor_count: u32,
|
||||
/// 主显示器分辨率
|
||||
pub primary_resolution: String,
|
||||
/// 所有显示器分辨率列表
|
||||
pub all_resolutions: Vec<String>,
|
||||
}
|
||||
|
||||
/// 网络信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NetworkInfo {
|
||||
/// 网络接口列表
|
||||
pub interfaces: Vec<InterfaceInfo>,
|
||||
/// 总下载 (MB)
|
||||
pub total_downloaded_mb: f64,
|
||||
/// 总上传 (MB)
|
||||
pub total_uploaded_mb: f64,
|
||||
}
|
||||
|
||||
/// 网络接口信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InterfaceInfo {
|
||||
/// 接口名称
|
||||
pub name: String,
|
||||
/// MAC 地址
|
||||
pub mac_address: String,
|
||||
/// IP 地址列表
|
||||
pub ip_networks: Vec<String>,
|
||||
/// 上传速度 (KB/s)
|
||||
pub upload_speed_kb: f64,
|
||||
/// 下载速度 (KB/s)
|
||||
pub download_speed_kb: f64,
|
||||
}
|
||||
9
src-tauri/src/platforms/mod.rs
Normal file
9
src-tauri/src/platforms/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
//! 平台相关模块
|
||||
//!
|
||||
//! 定义不同平台的特定实现
|
||||
|
||||
pub mod system_info;
|
||||
|
||||
// Windows 平台实现
|
||||
#[cfg(windows)]
|
||||
pub mod windows;
|
||||
22
src-tauri/src/platforms/system_info.rs
Normal file
22
src-tauri/src/platforms/system_info.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
//! 系统信息平台抽象
|
||||
//!
|
||||
//! 定义获取系统信息的平台相关接口
|
||||
|
||||
use crate::error::AppResult;
|
||||
use crate::models::system_info::SystemInfo;
|
||||
|
||||
/// 系统信息获取 trait
|
||||
///
|
||||
/// 定义获取系统信息的接口,不同平台需要实现此 trait
|
||||
pub trait SystemInfoAccessor {
|
||||
/// 获取完整的系统信息
|
||||
///
|
||||
/// # 返回
|
||||
///
|
||||
/// 返回包含所有系统信息的结构体
|
||||
///
|
||||
/// # 错误
|
||||
///
|
||||
/// 平台不支持或获取信息失败时返回错误
|
||||
fn get_system_info(&self) -> AppResult<SystemInfo>;
|
||||
}
|
||||
3
src-tauri/src/platforms/windows/mod.rs
Normal file
3
src-tauri/src/platforms/windows/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
//! Windows 平台特定实现
|
||||
|
||||
pub mod system_info_impl;
|
||||
509
src-tauri/src/platforms/windows/system_info_impl.rs
Normal file
509
src-tauri/src/platforms/windows/system_info_impl.rs
Normal file
@@ -0,0 +1,509 @@
|
||||
//! Windows 平台系统信息实现
|
||||
//!
|
||||
//! 使用 WMI 和 sysinfo 获取系统信息
|
||||
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::models::system_info::{
|
||||
ComputerInfo, CpuInfo, DisplayInfo, DiskInfo, GpuInfo, HardwareInfo, InterfaceInfo,
|
||||
MemoryInfo, NetworkInfo, OsInfo, SystemInfo,
|
||||
};
|
||||
use crate::platforms::system_info::SystemInfoAccessor;
|
||||
use serde::Deserialize;
|
||||
use std::time::Duration;
|
||||
use sysinfo::System;
|
||||
|
||||
/// Windows 平台系统信息实现
|
||||
#[cfg(windows)]
|
||||
pub struct WindowsSystemInfo;
|
||||
|
||||
#[cfg(windows)]
|
||||
impl SystemInfoAccessor for WindowsSystemInfo {
|
||||
fn get_system_info(&self) -> AppResult<SystemInfo> {
|
||||
// 使用 sysinfo 获取基础信息
|
||||
let mut sys = System::new_all();
|
||||
|
||||
// 等待一小会儿以收集CPU使用率和网络速率
|
||||
std::thread::sleep(Duration::from_millis(200));
|
||||
sys.refresh_all();
|
||||
sys.refresh_cpu();
|
||||
|
||||
// 使用 WMI 获取详细硬件信息
|
||||
let wmi_result = Self::get_wmi_info();
|
||||
|
||||
// 解构 WMI 结果,提供默认值
|
||||
let (hw_info, gpu_infos, disk_labels, net_ips) = match wmi_result {
|
||||
Ok((hw, gpus, labels, ips)) => (hw, gpus, labels, ips),
|
||||
Err(_) => (
|
||||
HardwareInfo {
|
||||
manufacturer: "Unknown".to_string(),
|
||||
model: "Unknown".to_string(),
|
||||
bios_version: "Unknown".to_string(),
|
||||
bios_serial: "Unknown".to_string(),
|
||||
},
|
||||
vec![],
|
||||
std::collections::HashMap::new(),
|
||||
std::collections::HashMap::new(),
|
||||
)
|
||||
};
|
||||
|
||||
Ok(SystemInfo {
|
||||
os: Self::get_os_info(&sys)?,
|
||||
hardware: hw_info,
|
||||
cpu: Self::get_cpu_info(&sys)?,
|
||||
memory: Self::get_memory_info(&sys)?,
|
||||
gpu: gpu_infos,
|
||||
disks: Self::get_disk_info(&sys, &disk_labels)?,
|
||||
computer: Self::get_computer_info()?,
|
||||
display: Self::get_display_info()?,
|
||||
network: Self::get_network_info(&sys, &net_ips)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// WMI 结构体映射
|
||||
/// 这些结构体名称必须匹配 Windows WMI 类名(包含下划线)
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
#[allow(non_camel_case_types)]
|
||||
struct Win32_VideoController {
|
||||
name: String,
|
||||
driver_version: Option<String>,
|
||||
adapter_ram: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
#[allow(non_camel_case_types)]
|
||||
struct Win32_ComputerSystem {
|
||||
manufacturer: Option<String>,
|
||||
model: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
#[allow(non_camel_case_types)]
|
||||
struct Win32_BaseBoard {
|
||||
manufacturer: Option<String>,
|
||||
product: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
#[allow(non_camel_case_types)]
|
||||
struct Win32_Bios {
|
||||
#[allow(dead_code)]
|
||||
manufacturer: Option<String>,
|
||||
version: Option<String>,
|
||||
serial_number: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
#[allow(non_camel_case_types)]
|
||||
struct Win32_LogicalDisk {
|
||||
device_id: String,
|
||||
volume_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
#[allow(non_camel_case_types)]
|
||||
struct Win32_NetworkAdapterConfiguration {
|
||||
mac_address: Option<String>,
|
||||
ip_address: Option<Vec<String>>,
|
||||
ip_enabled: Option<bool>,
|
||||
}
|
||||
|
||||
/// WMI 信息返回类型
|
||||
type WmiInfoResult = Result<(
|
||||
HardwareInfo,
|
||||
Vec<GpuInfo>,
|
||||
std::collections::HashMap<String, String>,
|
||||
std::collections::HashMap<String, Vec<String>>,
|
||||
), String>;
|
||||
|
||||
#[cfg(windows)]
|
||||
impl WindowsSystemInfo {
|
||||
/// 获取 WMI 硬件信息(容错版本,某个查询失败不影响其他查询)
|
||||
fn get_wmi_info() -> WmiInfoResult {
|
||||
use wmi::{COMLibrary, WMIConnection};
|
||||
use std::collections::HashMap;
|
||||
|
||||
let com = COMLibrary::new()
|
||||
.map_err(|e| format!("初始化 COM 失败: {:?}", e))?;
|
||||
|
||||
let wmi_con = WMIConnection::new(com)
|
||||
.map_err(|e| format!("连接 WMI 失败: {:?}", e))?;
|
||||
|
||||
// 1. 获取硬件信息(结合 ComputerSystem 和 BaseBoard)
|
||||
let hw_info = {
|
||||
let sys_query: Result<Vec<Win32_ComputerSystem>, _> = wmi_con.query();
|
||||
let board_query: Result<Vec<Win32_BaseBoard>, _> = wmi_con.query();
|
||||
let bios_query: Result<Vec<Win32_Bios>, _> = wmi_con.query();
|
||||
|
||||
let sys_result = sys_query.ok();
|
||||
let board_result = board_query.ok();
|
||||
let bios_result = bios_query.ok();
|
||||
|
||||
let sys = sys_result.as_ref().and_then(|v| v.first());
|
||||
let board = board_result.as_ref().and_then(|v| v.first());
|
||||
let bios = bios_result.as_ref().and_then(|v| v.first());
|
||||
|
||||
// 优先使用主板信息,回退到系统信息
|
||||
let manufacturer = board
|
||||
.and_then(|b| b.manufacturer.clone())
|
||||
.or_else(|| sys.and_then(|s| s.manufacturer.clone()))
|
||||
.unwrap_or_else(|| "Unknown".to_string());
|
||||
|
||||
let model = board
|
||||
.and_then(|b| b.product.clone())
|
||||
.or_else(|| sys.and_then(|s| s.model.clone()))
|
||||
.unwrap_or_else(|| "Unknown".to_string());
|
||||
|
||||
HardwareInfo {
|
||||
manufacturer,
|
||||
model,
|
||||
bios_version: bios
|
||||
.and_then(|b| b.version.clone())
|
||||
.unwrap_or_else(|| "Unknown".to_string()),
|
||||
bios_serial: bios
|
||||
.and_then(|b| b.serial_number.clone())
|
||||
.unwrap_or_else(|| "Unknown".to_string()),
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 获取显卡信息(容错)
|
||||
let gpu_infos = {
|
||||
let gpu_query: Result<Vec<Win32_VideoController>, _> = wmi_con.query();
|
||||
gpu_query
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|g| GpuInfo {
|
||||
name: g.name,
|
||||
vram_gb: g.adapter_ram.unwrap_or(0) as f64 / (1024.0 * 1024.0 * 1024.0),
|
||||
driver_version: g.driver_version.unwrap_or_else(|| "Unknown".to_string()),
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
// 3. 获取磁盘卷标
|
||||
let mut disk_labels = HashMap::new();
|
||||
if let Ok(disk_query_result) = wmi_con.query::<Win32_LogicalDisk>() {
|
||||
for disk in disk_query_result {
|
||||
if let Some(vol) = disk.volume_name {
|
||||
if !vol.is_empty() {
|
||||
disk_labels.insert(disk.device_id, vol);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 获取网络 IP(修复 MAC 大小写匹配)
|
||||
let mut net_ips = HashMap::new();
|
||||
if let Ok(net_query_result) = wmi_con.query::<Win32_NetworkAdapterConfiguration>() {
|
||||
for net in net_query_result {
|
||||
if let (Some(true), Some(mac), Some(ips)) = (net.ip_enabled, net.mac_address, net.ip_address) {
|
||||
// WMI 返回大写 MAC,sysinfo 返回小写,统一转为小写
|
||||
net_ips.insert(mac.to_lowercase(), ips);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((hw_info, gpu_infos, disk_labels, net_ips))
|
||||
}
|
||||
|
||||
/// 获取操作系统信息
|
||||
fn get_os_info(_sys: &System) -> AppResult<OsInfo> {
|
||||
use windows::Win32::System::SystemInformation::GetNativeSystemInfo;
|
||||
|
||||
let mut sys_info = unsafe { std::mem::zeroed() };
|
||||
unsafe {
|
||||
GetNativeSystemInfo(&mut sys_info);
|
||||
}
|
||||
|
||||
let arch = unsafe {
|
||||
match sys_info.Anonymous.Anonymous.wProcessorArchitecture.0 {
|
||||
0 => "x86 (32-bit)".to_string(),
|
||||
9 => "x64 (64-bit)".to_string(),
|
||||
5 => "ARM".to_string(),
|
||||
12 => "ARM64".to_string(),
|
||||
_ => "Unknown".to_string(),
|
||||
}
|
||||
};
|
||||
|
||||
Ok(OsInfo {
|
||||
name: "Windows".to_string(),
|
||||
version: System::os_version().unwrap_or_else(|| "Unknown".to_string()),
|
||||
arch,
|
||||
kernel_version: System::kernel_version().unwrap_or_else(|| "Unknown".to_string()),
|
||||
host_name: System::host_name().unwrap_or_else(|| "Unknown".to_string()),
|
||||
uptime_readable: Self::format_uptime(System::uptime()),
|
||||
})
|
||||
}
|
||||
|
||||
/// 获取 CPU 信息
|
||||
fn get_cpu_info(sys: &System) -> AppResult<CpuInfo> {
|
||||
let cpus = sys.cpus();
|
||||
let cpu = cpus.first().ok_or_else(|| {
|
||||
AppError::SystemInfoFailed("无法获取 CPU 信息".to_string())
|
||||
})?;
|
||||
|
||||
let physical_cores = sys.physical_core_count().unwrap_or(1);
|
||||
let usage = sys.global_cpu_info().cpu_usage();
|
||||
|
||||
Ok(CpuInfo {
|
||||
model: cpu.brand().to_string(),
|
||||
cores: physical_cores,
|
||||
processors: cpus.len(),
|
||||
max_frequency: cpu.frequency() as u32,
|
||||
usage_percent: usage,
|
||||
})
|
||||
}
|
||||
|
||||
/// 获取内存信息
|
||||
fn get_memory_info(sys: &System) -> AppResult<MemoryInfo> {
|
||||
let total = sys.total_memory() as f64;
|
||||
let available = sys.available_memory() as f64;
|
||||
let used = total - available;
|
||||
|
||||
Ok(MemoryInfo {
|
||||
total_gb: Self::bytes_to_gb(total),
|
||||
available_gb: Self::bytes_to_gb(available),
|
||||
used_gb: Self::bytes_to_gb(used),
|
||||
usage_percent: if total > 0.0 { (used / total) * 100.0 } else { 0.0 },
|
||||
})
|
||||
}
|
||||
|
||||
/// 获取磁盘信息
|
||||
fn get_disk_info(_sys: &System, disk_labels: &std::collections::HashMap<String, String>) -> AppResult<Vec<DiskInfo>> {
|
||||
let mut disk_infos = Vec::new();
|
||||
|
||||
// 在 sysinfo 0.30 中使用新的 API
|
||||
use sysinfo::Disks;
|
||||
|
||||
let disks = Disks::new_with_refreshed_list();
|
||||
|
||||
for disk in disks.list() {
|
||||
let total = disk.total_space() as f64;
|
||||
let available = disk.available_space() as f64;
|
||||
let used = total - available;
|
||||
|
||||
// 获取盘符,处理 "C:\" -> "C:" 的情况
|
||||
let name = disk.name().to_string_lossy();
|
||||
let drive_letter = name.trim_end_matches('\\').to_string();
|
||||
|
||||
// 从 WMI 查询结果中获取真实卷标
|
||||
let volume_label = disk_labels
|
||||
.get(&drive_letter)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "Local Disk".to_string());
|
||||
|
||||
disk_infos.push(DiskInfo {
|
||||
drive_letter,
|
||||
volume_label,
|
||||
file_system: disk.file_system().to_string_lossy().to_string(),
|
||||
total_gb: Self::bytes_to_gb(total),
|
||||
available_gb: Self::bytes_to_gb(available),
|
||||
used_gb: Self::bytes_to_gb(used),
|
||||
usage_percent: if total > 0.0 { (used / total) * 100.0 } else { 0.0 },
|
||||
});
|
||||
}
|
||||
|
||||
Ok(disk_infos)
|
||||
}
|
||||
|
||||
/// 获取计算机信息
|
||||
fn get_computer_info() -> AppResult<ComputerInfo> {
|
||||
use windows::Win32::System::SystemInformation::{
|
||||
ComputerNamePhysicalDnsHostname, GetComputerNameExW,
|
||||
};
|
||||
use windows::core::PWSTR;
|
||||
|
||||
let mut computer_name = [0u16; 256];
|
||||
let mut size = computer_name.len() as u32;
|
||||
|
||||
unsafe {
|
||||
let _ = GetComputerNameExW(
|
||||
ComputerNamePhysicalDnsHostname,
|
||||
PWSTR(computer_name.as_mut_ptr()),
|
||||
&mut size,
|
||||
);
|
||||
}
|
||||
|
||||
let name = String::from_utf16_lossy(&computer_name[..size as usize]);
|
||||
|
||||
Ok(ComputerInfo {
|
||||
name: name.clone(),
|
||||
username: std::env::var("USERNAME").unwrap_or_else(|_| "Unknown".to_string()),
|
||||
domain: "WORKGROUP".to_string(),
|
||||
manufacturer: name.clone(),
|
||||
model: "PC".to_string(),
|
||||
serial_number: "Unknown".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// 获取显示器信息
|
||||
fn get_display_info() -> AppResult<DisplayInfo> {
|
||||
use windows::Win32::UI::WindowsAndMessaging::{GetSystemMetrics, SM_CXSCREEN, SM_CYSCREEN};
|
||||
|
||||
let width = unsafe { GetSystemMetrics(SM_CXSCREEN) };
|
||||
let height = unsafe { GetSystemMetrics(SM_CYSCREEN) };
|
||||
let resolution = format!("{}x{}", width, height);
|
||||
|
||||
Ok(DisplayInfo {
|
||||
monitor_count: 1,
|
||||
primary_resolution: resolution.clone(),
|
||||
all_resolutions: vec![resolution],
|
||||
})
|
||||
}
|
||||
|
||||
/// 获取网络信息
|
||||
fn get_network_info(_sys: &System, net_ips: &std::collections::HashMap<String, Vec<String>>) -> AppResult<NetworkInfo> {
|
||||
use sysinfo::Networks;
|
||||
|
||||
let networks = Networks::new_with_refreshed_list();
|
||||
let mut interfaces = Vec::new();
|
||||
let mut total_down = 0.0;
|
||||
let mut total_up = 0.0;
|
||||
|
||||
for (name, data) in networks.list() {
|
||||
total_down += data.total_received() as f64;
|
||||
total_up += data.total_transmitted() as f64;
|
||||
|
||||
// 过滤掉回环接口
|
||||
if name == "LO" {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 修复 MAC 地址匹配:统一转为小写
|
||||
let mac = data.mac_address().to_string().to_lowercase();
|
||||
let ip_list = net_ips.get(&mac).cloned().unwrap_or_default();
|
||||
|
||||
// 只显示有 IP 或有流量的接口
|
||||
if !ip_list.is_empty() || data.total_received() > 0 {
|
||||
interfaces.push(InterfaceInfo {
|
||||
name: name.clone(),
|
||||
mac_address: mac,
|
||||
ip_networks: ip_list,
|
||||
upload_speed_kb: data.transmitted() as f64 / 1024.0,
|
||||
download_speed_kb: data.received() as f64 / 1024.0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(NetworkInfo {
|
||||
interfaces,
|
||||
total_downloaded_mb: total_down / 1024.0 / 1024.0,
|
||||
total_uploaded_mb: total_up / 1024.0 / 1024.0,
|
||||
})
|
||||
}
|
||||
|
||||
/// 字节转换为 GB
|
||||
fn bytes_to_gb(bytes: f64) -> f64 {
|
||||
bytes / 1024.0 / 1024.0 / 1024.0
|
||||
}
|
||||
|
||||
/// 格式化运行时间为人类可读格式
|
||||
fn format_uptime(seconds: u64) -> String {
|
||||
let days = seconds / 86400;
|
||||
let hours = (seconds % 86400) / 3600;
|
||||
let minutes = (seconds % 3600) / 60;
|
||||
|
||||
if days > 0 {
|
||||
format!("{}天 {}小时 {}分钟", days, hours, minutes)
|
||||
} else if hours > 0 {
|
||||
format!("{}小时 {}分钟", hours, minutes)
|
||||
} else {
|
||||
format!("{}分钟", minutes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 其他平台占位实现
|
||||
#[cfg(not(windows))]
|
||||
pub struct DummySystemInfo;
|
||||
|
||||
#[cfg(not(windows))]
|
||||
impl SystemInfoAccessor for DummySystemInfo {
|
||||
fn get_system_info(&self) -> AppResult<SystemInfo> {
|
||||
use sysinfo::System;
|
||||
let mut sys = System::new_all();
|
||||
sys.refresh_all();
|
||||
|
||||
Ok(SystemInfo {
|
||||
os: OsInfo {
|
||||
name: System::name().unwrap_or_else(|| "Unknown".to_string()),
|
||||
version: System::os_version().unwrap_or_default(),
|
||||
arch: std::env::consts::ARCH.to_string(),
|
||||
kernel_version: System::kernel_version().unwrap_or_default(),
|
||||
host_name: System::host_name().unwrap_or_default(),
|
||||
uptime_readable: format!("{} seconds", System::uptime()),
|
||||
},
|
||||
hardware: HardwareInfo {
|
||||
manufacturer: "Unknown".to_string(),
|
||||
model: "Unknown".to_string(),
|
||||
bios_version: "Unknown".to_string(),
|
||||
bios_serial: "Unknown".to_string(),
|
||||
},
|
||||
cpu: {
|
||||
let cpus = sys.cpus();
|
||||
let cpu = cpus.first().unwrap();
|
||||
CpuInfo {
|
||||
model: cpu.brand().to_string(),
|
||||
cores: sys.physical_core_count().unwrap_or(1),
|
||||
processors: cpus.len(),
|
||||
max_frequency: cpu.frequency(),
|
||||
usage_percent: sys.global_cpu_info().cpu_usage(),
|
||||
}
|
||||
},
|
||||
memory: {
|
||||
let total = sys.total_memory() as f64;
|
||||
let available = sys.available_memory() as f64;
|
||||
let used = total - available;
|
||||
MemoryInfo {
|
||||
total_gb: total / 1024.0 / 1024.0 / 1024.0,
|
||||
available_gb: available / 1024.0 / 1024.0 / 1024.0,
|
||||
used_gb: used / 1024.0 / 1024.0 / 1024.0,
|
||||
usage_percent: if total > 0.0 { (used / total) * 100.0 } else { 0.0 },
|
||||
}
|
||||
},
|
||||
gpu: vec![],
|
||||
disks: sys
|
||||
.disks()
|
||||
.iter()
|
||||
.map(|disk| {
|
||||
let total = disk.total_space() as f64;
|
||||
let available = disk.available_space() as f64;
|
||||
let used = total - available;
|
||||
DiskInfo {
|
||||
drive_letter: disk.name().to_string_lossy().into_owned(),
|
||||
volume_label: "Local Disk".to_string(),
|
||||
file_system: disk.file_system().to_string_lossy().into_owned(),
|
||||
total_gb: total / 1024.0 / 1024.0 / 1024.0,
|
||||
available_gb: available / 1024.0 / 1024.0 / 1024.0,
|
||||
used_gb: used / 1024.0 / 1024.0 / 1024.0,
|
||||
usage_percent: if total > 0.0 { (used / total) * 100.0 } else { 0.0 },
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
computer: ComputerInfo {
|
||||
name: System::host_name().unwrap_or_default(),
|
||||
username: std::env::var("USERNAME").unwrap_or_default(),
|
||||
domain: "WORKGROUP".to_string(),
|
||||
manufacturer: "Unknown".to_string(),
|
||||
model: "Unknown".to_string(),
|
||||
serial_number: "Unknown".to_string(),
|
||||
},
|
||||
display: DisplayInfo {
|
||||
monitor_count: 1,
|
||||
primary_resolution: "Unknown".to_string(),
|
||||
all_resolutions: vec![],
|
||||
},
|
||||
network: NetworkInfo {
|
||||
interfaces: vec![],
|
||||
total_downloaded_mb: 0.0,
|
||||
total_uploaded_mb: 0.0,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
200
src-tauri/src/services/json_format_service.rs
Normal file
200
src-tauri/src/services/json_format_service.rs
Normal file
@@ -0,0 +1,200 @@
|
||||
//! JSON 格式化服务
|
||||
//!
|
||||
//! 提供 JSON 格式化功能的核心业务逻辑
|
||||
|
||||
use crate::error::AppResult;
|
||||
use crate::models::json_format::{JsonFormatConfig, JsonFormatResult, JsonValidateResult};
|
||||
use crate::utils::json_formatter;
|
||||
|
||||
/// JSON 格式化服务
|
||||
pub struct JsonFormatService;
|
||||
|
||||
impl JsonFormatService {
|
||||
/// 格式化 JSON 字符串
|
||||
///
|
||||
/// 根据配置对输入的 JSON 字符串进行格式化
|
||||
///
|
||||
/// # 参数
|
||||
///
|
||||
/// * `input` - 输入的 JSON 字符串
|
||||
/// * `config` - 格式化配置
|
||||
///
|
||||
/// # 返回
|
||||
///
|
||||
/// 返回格式化结果
|
||||
///
|
||||
/// # 错误
|
||||
///
|
||||
/// - 输入为空时返回 `AppError::InvalidData`
|
||||
pub fn format(input: &str, config: &JsonFormatConfig) -> AppResult<JsonFormatResult> {
|
||||
// 参数验证
|
||||
if input.trim().is_empty() {
|
||||
return Ok(JsonFormatResult {
|
||||
success: false,
|
||||
result: String::new(),
|
||||
error: Some("输入内容不能为空".to_string()),
|
||||
is_valid: false,
|
||||
});
|
||||
}
|
||||
|
||||
// 调用工具函数进行格式化
|
||||
match json_formatter::format_json(input, config) {
|
||||
Ok(formatted) => Ok(JsonFormatResult {
|
||||
success: true,
|
||||
result: formatted,
|
||||
error: None,
|
||||
is_valid: true,
|
||||
}),
|
||||
Err(err) => Ok(JsonFormatResult {
|
||||
success: false,
|
||||
result: String::new(),
|
||||
error: Some(err),
|
||||
is_valid: false,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// 验证 JSON 字符串
|
||||
///
|
||||
/// 检查输入的字符串是否为有效的 JSON
|
||||
///
|
||||
/// # 参数
|
||||
///
|
||||
/// * `input` - 输入的 JSON 字符串
|
||||
///
|
||||
/// # 返回
|
||||
///
|
||||
/// 返回验证结果
|
||||
pub fn validate(input: &str) -> AppResult<JsonValidateResult> {
|
||||
// 参数验证
|
||||
if input.trim().is_empty() {
|
||||
return Ok(JsonValidateResult {
|
||||
is_valid: false,
|
||||
error_message: Some("输入内容不能为空".to_string()),
|
||||
error_line: None,
|
||||
error_column: None,
|
||||
});
|
||||
}
|
||||
|
||||
// 调用工具函数进行验证
|
||||
let result = json_formatter::validate_json(input);
|
||||
Ok(JsonValidateResult {
|
||||
is_valid: result.is_valid,
|
||||
error_message: result.error_message,
|
||||
error_line: result.error_line,
|
||||
error_column: result.error_column,
|
||||
})
|
||||
}
|
||||
|
||||
/// 压缩 JSON 字符串
|
||||
///
|
||||
/// 去除 JSON 中的所有空格和换行
|
||||
///
|
||||
/// # 参数
|
||||
///
|
||||
/// * `input` - 输入的 JSON 字符串
|
||||
///
|
||||
/// # 返回
|
||||
///
|
||||
/// 返回格式化结果
|
||||
pub fn compact(input: &str) -> AppResult<JsonFormatResult> {
|
||||
// 参数验证
|
||||
if input.trim().is_empty() {
|
||||
return Ok(JsonFormatResult {
|
||||
success: false,
|
||||
result: String::new(),
|
||||
error: Some("输入内容不能为空".to_string()),
|
||||
is_valid: false,
|
||||
});
|
||||
}
|
||||
|
||||
// 调用工具函数进行压缩
|
||||
match json_formatter::compact_json(input) {
|
||||
Ok(compacted) => Ok(JsonFormatResult {
|
||||
success: true,
|
||||
result: compacted,
|
||||
error: None,
|
||||
is_valid: true,
|
||||
}),
|
||||
Err(err) => Ok(JsonFormatResult {
|
||||
success: false,
|
||||
result: String::new(),
|
||||
error: Some(err),
|
||||
is_valid: false,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_format_valid_json() {
|
||||
let service = JsonFormatService;
|
||||
let input = r#"{"name":"test","value":123}"#;
|
||||
let config = JsonFormatConfig::default();
|
||||
|
||||
let result = service.format(input, &config).unwrap();
|
||||
assert!(result.success);
|
||||
assert!(result.is_valid);
|
||||
assert!(result.error.is_none());
|
||||
assert!(result.result.contains('\n'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_invalid_json() {
|
||||
let service = JsonFormatService;
|
||||
let input = r#"{"invalid": }"#;
|
||||
let config = JsonFormatConfig::default();
|
||||
|
||||
let result = service.format(input, &config).unwrap();
|
||||
assert!(!result.success);
|
||||
assert!(!result.is_valid);
|
||||
assert!(result.error.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_empty_input() {
|
||||
let service = JsonFormatService;
|
||||
let input = "";
|
||||
let config = JsonFormatConfig::default();
|
||||
|
||||
let result = service.format(input, &config).unwrap();
|
||||
assert!(!result.success);
|
||||
assert!(!result.is_valid);
|
||||
assert!(result.error.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_valid_json() {
|
||||
let service = JsonFormatService;
|
||||
let input = r#"{"valid": true}"#;
|
||||
|
||||
let result = service.validate(input).unwrap();
|
||||
assert!(result.is_valid);
|
||||
assert!(result.error_message.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_invalid_json() {
|
||||
let service = JsonFormatService;
|
||||
let input = r#"{"invalid": }"#;
|
||||
|
||||
let result = service.validate(input).unwrap();
|
||||
assert!(!result.is_valid);
|
||||
assert!(result.error_message.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compact_json() {
|
||||
let service = JsonFormatService;
|
||||
let input = r#"{ "name" : "test" }"#;
|
||||
|
||||
let result = service.compact(input).unwrap();
|
||||
assert!(result.success);
|
||||
assert!(result.is_valid);
|
||||
assert_eq!(result.result, r#"{"name":"test"}"#);
|
||||
}
|
||||
}
|
||||
7
src-tauri/src/services/mod.rs
Normal file
7
src-tauri/src/services/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
//! 业务逻辑层
|
||||
//!
|
||||
//! 提供应用的核心业务逻辑实现
|
||||
|
||||
pub mod json_format_service;
|
||||
pub mod system_info_service;
|
||||
pub mod window_service;
|
||||
38
src-tauri/src/services/system_info_service.rs
Normal file
38
src-tauri/src/services/system_info_service.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
//! 系统信息服务
|
||||
//!
|
||||
//! 提供系统信息查询功能的核心业务逻辑
|
||||
|
||||
use crate::error::AppResult;
|
||||
use crate::models::system_info::SystemInfo;
|
||||
use crate::platforms::system_info::SystemInfoAccessor;
|
||||
|
||||
/// 系统信息服务
|
||||
pub struct SystemInfoService;
|
||||
|
||||
impl SystemInfoService {
|
||||
/// 获取系统信息
|
||||
///
|
||||
/// 查询并返回当前系统的完整信息
|
||||
///
|
||||
/// # 返回
|
||||
///
|
||||
/// 返回包含所有系统信息的结构体
|
||||
///
|
||||
/// # 错误
|
||||
///
|
||||
/// 平台不支持或获取信息失败时返回错误
|
||||
pub fn get_system_info() -> AppResult<SystemInfo> {
|
||||
// 调用平台实现获取系统信息
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let accessor = crate::platforms::windows::system_info_impl::WindowsSystemInfo;
|
||||
accessor.get_system_info()
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let accessor = crate::platforms::windows::system_info_impl::DummySystemInfo;
|
||||
accessor.get_system_info()
|
||||
}
|
||||
}
|
||||
}
|
||||
81
src-tauri/src/services/window_service.rs
Normal file
81
src-tauri/src/services/window_service.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
//! 窗口服务
|
||||
//!
|
||||
//! 提供窗口管理相关的业务逻辑
|
||||
|
||||
use tauri::Window;
|
||||
use crate::error::{AppError, AppResult};
|
||||
|
||||
/// 窗口服务
|
||||
///
|
||||
/// 提供窗口显示、隐藏和切换等管理功能
|
||||
pub struct WindowService;
|
||||
|
||||
impl WindowService {
|
||||
/// 切换窗口显示/隐藏
|
||||
///
|
||||
/// 根据窗口当前状态切换显示或隐藏
|
||||
///
|
||||
/// # 参数
|
||||
///
|
||||
/// * `window` - Tauri 窗口引用
|
||||
///
|
||||
/// # 行为
|
||||
///
|
||||
/// - 如果窗口当前可见,则隐藏窗口
|
||||
/// - 如果窗口当前隐藏,则显示窗口并聚焦
|
||||
///
|
||||
/// # 错误
|
||||
///
|
||||
/// 窗口操作失败时返回 `AppError::WindowOperationFailed`
|
||||
pub fn toggle_window(window: &Window) -> AppResult<()> {
|
||||
let is_visible = window.is_visible()
|
||||
.map_err(|e| AppError::WindowOperationFailed(e.to_string()))?;
|
||||
|
||||
if is_visible {
|
||||
Self::hide_window(window)?;
|
||||
} else {
|
||||
Self::show_window(window)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 隐藏窗口
|
||||
///
|
||||
/// 将窗口隐藏,使其不再可见
|
||||
///
|
||||
/// # 参数
|
||||
///
|
||||
/// * `window` - Tauri 窗口引用
|
||||
///
|
||||
/// # 错误
|
||||
///
|
||||
/// 窗口操作失败时返回 `AppError::WindowOperationFailed`
|
||||
pub fn hide_window(window: &Window) -> AppResult<()> {
|
||||
window.hide()
|
||||
.map_err(|e| AppError::WindowOperationFailed(e.to_string()))
|
||||
}
|
||||
|
||||
/// 显示窗口并聚焦
|
||||
///
|
||||
/// 显示窗口并将其设置为焦点窗口
|
||||
///
|
||||
/// # 参数
|
||||
///
|
||||
/// * `window` - Tauri 窗口引用
|
||||
///
|
||||
/// # 行为
|
||||
///
|
||||
/// - 显示窗口
|
||||
/// - 将窗口设置为焦点窗口,用户可以直接与之交互
|
||||
///
|
||||
/// # 错误
|
||||
///
|
||||
/// 窗口操作失败时返回 `AppError::WindowOperationFailed`
|
||||
pub fn show_window(window: &Window) -> AppResult<()> {
|
||||
window.show()
|
||||
.and_then(|_| window.set_focus())
|
||||
.map_err(|e| AppError::WindowOperationFailed(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
93
src-tauri/src/utils/color_conversion.rs
Normal file
93
src-tauri/src/utils/color_conversion.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
//! 颜色转换工具
|
||||
//!
|
||||
//! 提供颜色空间转换算法实现
|
||||
|
||||
use crate::models::color::HslInfo;
|
||||
|
||||
/// RGB 转 HSL
|
||||
///
|
||||
/// 将 RGB 颜色值转换为 HSL 颜色值
|
||||
///
|
||||
/// # 参数
|
||||
///
|
||||
/// * `r` - 红色分量 (0-255)
|
||||
/// * `g` - 绿色分量 (0-255)
|
||||
/// * `b` - 蓝色分量 (0-255)
|
||||
///
|
||||
/// # 返回
|
||||
///
|
||||
/// 返回 HSL 颜色信息
|
||||
///
|
||||
/// # 算法说明
|
||||
///
|
||||
/// 该函数使用标准的 RGB 到 HSL 转换算法:
|
||||
/// 1. 将 RGB 值归一化到 [0, 1] 范围
|
||||
/// 2. 计算最大值和最小值
|
||||
/// 3. 根据最大值计算色相(H)
|
||||
/// 4. 根据最大值和最小值之差计算饱和度(S)
|
||||
/// 5. 亮度为最大值和最小值的平均值
|
||||
///
|
||||
/// # 示例
|
||||
///
|
||||
/// ```
|
||||
/// use crate::utils::color_conversion::rgb_to_hsl;
|
||||
///
|
||||
/// let hsl = rgb_to_hsl(255, 0, 0);
|
||||
/// assert_eq!(hsl.h, 0); // 红色
|
||||
/// assert_eq!(hsl.s, 100); // 完全饱和
|
||||
/// assert_eq!(hsl.l, 50); // 中等亮度
|
||||
/// ```
|
||||
pub fn rgb_to_hsl(r: u8, g: u8, b: u8) -> HslInfo {
|
||||
let r = r as f64 / 255.0;
|
||||
let g = g as f64 / 255.0;
|
||||
let b = b as f64 / 255.0;
|
||||
|
||||
let max = r.max(g).max(b);
|
||||
let min = r.min(g).min(b);
|
||||
let mut h = 0.0;
|
||||
let mut s = 0.0;
|
||||
let l = (max + min) / 2.0;
|
||||
|
||||
if max != min {
|
||||
let d = max - min;
|
||||
s = if l > 0.5 {
|
||||
d / (2.0 - max - min)
|
||||
} else {
|
||||
d / (max + min)
|
||||
};
|
||||
|
||||
h = match max {
|
||||
x if x == r => (g - b) / d + if g < b { 6.0 } else { 0.0 },
|
||||
x if x == g => (b - r) / d + 2.0,
|
||||
_ => (r - g) / d + 4.0,
|
||||
};
|
||||
|
||||
h /= 6.0;
|
||||
}
|
||||
|
||||
HslInfo {
|
||||
h: (h * 360.0).round() as u16,
|
||||
s: (s * 100.0).round() as u8,
|
||||
l: (l * 100.0).round() as u8,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_red_color() {
|
||||
let hsl = rgb_to_hsl(255, 0, 0);
|
||||
assert_eq!(hsl.h, 0);
|
||||
assert_eq!(hsl.s, 100);
|
||||
assert_eq!(hsl.l, 50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gray_color() {
|
||||
let hsl = rgb_to_hsl(128, 128, 128);
|
||||
assert_eq!(hsl.s, 0);
|
||||
assert_eq!(hsl.l, 50);
|
||||
}
|
||||
}
|
||||
290
src-tauri/src/utils/json_formatter.rs
Normal file
290
src-tauri/src/utils/json_formatter.rs
Normal file
@@ -0,0 +1,290 @@
|
||||
//! JSON 格式化工具函数
|
||||
//!
|
||||
//! 提供纯函数的 JSON 处理算法
|
||||
|
||||
use crate::models::json_format::{FormatMode, JsonFormatConfig};
|
||||
use serde_json::{self, Value};
|
||||
|
||||
/// 格式化 JSON 字符串
|
||||
///
|
||||
/// 对输入的 JSON 字符串进行格式化,支持美化和压缩模式
|
||||
///
|
||||
/// # 参数
|
||||
///
|
||||
/// * `input` - 输入的 JSON 字符串
|
||||
/// * `config` - 格式化配置
|
||||
///
|
||||
/// # 返回
|
||||
///
|
||||
/// 返回格式化后的 JSON 字符串
|
||||
///
|
||||
/// # 错误
|
||||
///
|
||||
/// 当输入不是有效的 JSON 时返回错误
|
||||
///
|
||||
/// # 示例
|
||||
///
|
||||
/// ```
|
||||
/// use crate::utils::json_formatter::format_json;
|
||||
/// use crate::models::json_format::{JsonFormatConfig, FormatMode};
|
||||
///
|
||||
/// let input = r#"{"name":"test","value":123}"#;
|
||||
/// let config = JsonFormatConfig::default();
|
||||
/// let result = format_json(input, &config).unwrap();
|
||||
/// assert!(result.contains('\n'));
|
||||
/// ```
|
||||
pub fn format_json(input: &str, config: &JsonFormatConfig) -> Result<String, String> {
|
||||
// 解析 JSON
|
||||
let mut value: Value = serde_json::from_str(input)
|
||||
.map_err(|e| format!("JSON 解析失败: {}", e))?;
|
||||
|
||||
// 如果需要排序 key
|
||||
if config.sort_keys {
|
||||
sort_keys(&mut value);
|
||||
}
|
||||
|
||||
// 根据模式格式化
|
||||
match config.mode {
|
||||
FormatMode::Pretty => {
|
||||
let indent_str = " ".repeat(config.indent as usize);
|
||||
serde_json::to_string_pretty(&value)
|
||||
.map_err(|e| format!("JSON 格式化失败: {}", e))
|
||||
.map(|s| replace_indent(&s, &indent_str))
|
||||
}
|
||||
FormatMode::Compact => {
|
||||
serde_json::to_string(&value)
|
||||
.map_err(|e| format!("JSON 序列化失败: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 替换缩进空格数
|
||||
///
|
||||
/// serde_json 默认使用 2 空格缩进,此函数将其替换为配置的缩进数
|
||||
fn replace_indent(json: &str, indent: &str) -> String {
|
||||
if indent == " " {
|
||||
return json.to_string();
|
||||
}
|
||||
|
||||
json.lines()
|
||||
.map(|line| {
|
||||
let trimmed = line.trim_start();
|
||||
if trimmed.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
let leading_spaces = line.len() - trimmed.len();
|
||||
if leading_spaces > 0 {
|
||||
let indent_level = leading_spaces / 2;
|
||||
format!("{}{}", indent.repeat(indent_level), trimmed)
|
||||
} else {
|
||||
line.to_string()
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
/// 对 JSON 对象的 key 进行排序
|
||||
///
|
||||
/// 递归遍历 JSON 结构,对所有对象的 key 按字母顺序排序
|
||||
fn sort_keys(value: &mut Value) {
|
||||
match value {
|
||||
Value::Object(map) => {
|
||||
// 收集所有 key-value 对
|
||||
let mut entries: Vec<(String, Value)> = map
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect();
|
||||
|
||||
// 排序 key
|
||||
entries.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
// 递归处理每个值
|
||||
for (_, v) in &mut entries {
|
||||
sort_keys(v);
|
||||
}
|
||||
|
||||
// 清空并重新插入
|
||||
map.clear();
|
||||
for (k, v) in entries {
|
||||
map.insert(k, v);
|
||||
}
|
||||
}
|
||||
Value::Array(arr) => {
|
||||
// 递归处理数组中的每个元素
|
||||
for v in arr {
|
||||
sort_keys(v);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// 验证 JSON 字符串是否有效
|
||||
///
|
||||
/// 检查输入的字符串是否为有效的 JSON
|
||||
///
|
||||
/// # 参数
|
||||
///
|
||||
/// * `input` - 输入的 JSON 字符串
|
||||
///
|
||||
/// # 返回
|
||||
///
|
||||
/// 返回验证结果,包含是否有效和错误信息
|
||||
///
|
||||
/// # 示例
|
||||
///
|
||||
/// ```
|
||||
/// use crate::utils::json_formatter::validate_json;
|
||||
///
|
||||
/// let result = validate_json(r#"{"valid": true}"#);
|
||||
/// assert!(result.is_valid);
|
||||
///
|
||||
/// let result = validate_json(r#"{"invalid": }"#);
|
||||
/// assert!(!result.is_valid);
|
||||
/// ```
|
||||
pub fn validate_json(input: &str) -> JsonValidateResult {
|
||||
match serde_json::from_str::<Value>(input) {
|
||||
Ok(_) => JsonValidateResult {
|
||||
is_valid: true,
|
||||
error_message: None,
|
||||
error_line: None,
|
||||
error_column: None,
|
||||
},
|
||||
Err(e) => {
|
||||
// 解析错误信息以获取行号和列号
|
||||
let error_msg = e.to_string();
|
||||
let (line, column) = parse_error_position(&error_msg);
|
||||
|
||||
JsonValidateResult {
|
||||
is_valid: false,
|
||||
error_message: Some(error_msg),
|
||||
error_line: line,
|
||||
error_column: column,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// JSON 验证结果结构
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct JsonValidateResult {
|
||||
pub is_valid: bool,
|
||||
pub error_message: Option<String>,
|
||||
pub error_line: Option<usize>,
|
||||
pub error_column: Option<usize>,
|
||||
}
|
||||
|
||||
/// 从错误信息中解析行号和列号
|
||||
fn parse_error_position(error_msg: &str) -> (Option<usize>, Option<usize>) {
|
||||
// serde_json 的错误格式通常是 "line X, column Y"
|
||||
if let Some(line_pos) = error_msg.find("line ") {
|
||||
let after_line = &error_msg[line_pos + 5..];
|
||||
if let Some(comma_pos) = after_line.find(',') {
|
||||
if let Ok(line) = after_line[..comma_pos].parse::<usize>() {
|
||||
if let Some(col_pos) = after_line.find("column ") {
|
||||
let after_col = &after_line[col_pos + 7..];
|
||||
if let Some(end_pos) = after_col.find(|c: char| !c.is_ascii_digit()) {
|
||||
if let Ok(col) = after_col[..end_pos].parse::<usize>() {
|
||||
return (Some(line), Some(col));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(None, None)
|
||||
}
|
||||
|
||||
/// 压缩 JSON 字符串
|
||||
///
|
||||
/// 去除所有空格和换行,生成最紧凑的 JSON 格式
|
||||
///
|
||||
/// # 参数
|
||||
///
|
||||
/// * `input` - 输入的 JSON 字符串
|
||||
///
|
||||
/// # 返回
|
||||
///
|
||||
/// 返回压缩后的 JSON 字符串
|
||||
///
|
||||
/// # 错误
|
||||
///
|
||||
/// 当输入不是有效的 JSON 时返回错误
|
||||
pub fn compact_json(input: &str) -> Result<String, String> {
|
||||
let value: Value = serde_json::from_str(input)
|
||||
.map_err(|e| format!("JSON 解析失败: {}", e))?;
|
||||
|
||||
serde_json::to_string(&value)
|
||||
.map_err(|e| format!("JSON 序列化失败: {}", e))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_format_json_pretty() {
|
||||
let input = r#"{"name":"test","value":123}"#;
|
||||
let config = JsonFormatConfig::default();
|
||||
let result = format_json(input, &config).unwrap();
|
||||
assert!(result.contains('\n'));
|
||||
assert!(result.contains(" "));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_json_compact() {
|
||||
let input = r#"{ "name" : "test" , "value" : 123 }"#;
|
||||
let config = JsonFormatConfig {
|
||||
mode: FormatMode::Compact,
|
||||
..Default::default()
|
||||
};
|
||||
let result = format_json(input, &config).unwrap();
|
||||
assert!(!result.contains('\n'));
|
||||
assert!(!result.contains(' '));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_json_invalid() {
|
||||
let input = r#"{"invalid": }"#;
|
||||
let config = JsonFormatConfig::default();
|
||||
assert!(format_json(input, &config).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_json_with_sort_keys() {
|
||||
let input = r#"{"z":1,"a":2,"m":3}"#;
|
||||
let config = JsonFormatConfig {
|
||||
sort_keys: true,
|
||||
..Default::default()
|
||||
};
|
||||
let result = format_json(input, &config).unwrap();
|
||||
// 验证 key 已排序
|
||||
let a_pos = result.find("\"a\"").unwrap();
|
||||
let m_pos = result.find("\"m\"").unwrap();
|
||||
let z_pos = result.find("\"z\"").unwrap();
|
||||
assert!(a_pos < m_pos);
|
||||
assert!(m_pos < z_pos);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_json_valid() {
|
||||
let result = validate_json(r#"{"valid": true}"#);
|
||||
assert!(result.is_valid);
|
||||
assert!(result.error_message.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_json_invalid() {
|
||||
let result = validate_json(r#"{"invalid": }"#);
|
||||
assert!(!result.is_valid);
|
||||
assert!(result.error_message.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compact_json() {
|
||||
let input = r#"{ "name" : "test" }"#;
|
||||
let result = compact_json(input).unwrap();
|
||||
assert_eq!(result, r#"{"name":"test"}"#);
|
||||
}
|
||||
}
|
||||
8
src-tauri/src/utils/mod.rs
Normal file
8
src-tauri/src/utils/mod.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
//! 工具函数模块
|
||||
//!
|
||||
//! 提供纯函数算法实现,无副作用
|
||||
|
||||
pub mod color_conversion;
|
||||
pub mod json_formatter;
|
||||
pub mod screen;
|
||||
pub mod shortcut;
|
||||
177
src-tauri/src/utils/screen.rs
Normal file
177
src-tauri/src/utils/screen.rs
Normal file
@@ -0,0 +1,177 @@
|
||||
//! Windows 屏幕访问模块
|
||||
//!
|
||||
//! 提供屏幕像素颜色获取功能
|
||||
|
||||
use crate::error::{AppError, AppResult};
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Graphics::Gdi::{
|
||||
BitBlt, CreateCompatibleBitmap, CreateCompatibleDC, DeleteDC, DeleteObject, GetDC, GetDIBits,
|
||||
GetPixel, ReleaseDC, SelectObject, BITMAPINFO, BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS,
|
||||
HBITMAP, HGDIOBJ, SRCCOPY,
|
||||
};
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
GetSystemMetrics, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN,
|
||||
};
|
||||
|
||||
/// Windows 屏幕访问器
|
||||
pub struct WindowsScreen;
|
||||
|
||||
impl WindowsScreen {
|
||||
/// 获取屏幕指定像素的 RGB 颜色
|
||||
///
|
||||
/// # 参数
|
||||
///
|
||||
/// * `x` - 屏幕横坐标(像素)
|
||||
/// * `y` - 屏幕纵坐标(像素)
|
||||
///
|
||||
/// # 返回
|
||||
///
|
||||
/// 返回 RGB 三个分量的值,每个分量范围是 0-255
|
||||
///
|
||||
/// # 错误
|
||||
///
|
||||
/// 如果无法访问屏幕或坐标无效,返回错误
|
||||
pub fn get_pixel_color(x: i32, y: i32) -> AppResult<(u8, u8, u8)> {
|
||||
unsafe {
|
||||
let screen_dc = GetDC(HWND::default());
|
||||
if screen_dc.is_invalid() {
|
||||
return Err(AppError::ScreenAccessFailed("无法获取屏幕设备上下文".to_string()));
|
||||
}
|
||||
|
||||
let color = GetPixel(screen_dc, x, y);
|
||||
ReleaseDC(HWND::default(), screen_dc);
|
||||
|
||||
// COLORREF 是一个 newtype,包含 u32 值
|
||||
// 格式: 0x00BBGGRR (蓝、绿、红)
|
||||
let color_value = color.0;
|
||||
|
||||
if color_value == 0xFFFFFFFF {
|
||||
// GetPixel 在失败时返回 CLR_INVALID (0xFFFFFFFF)
|
||||
return Err(AppError::ScreenAccessFailed("无法获取像素颜色".to_string()));
|
||||
}
|
||||
|
||||
let r = (color_value & 0xFF) as u8;
|
||||
let g = ((color_value >> 8) & 0xFF) as u8;
|
||||
let b = ((color_value >> 16) & 0xFF) as u8;
|
||||
|
||||
Ok((r, g, b))
|
||||
}
|
||||
}
|
||||
|
||||
/// 捕获屏幕指定区域像素(RGBA,行优先,左上角开始)
|
||||
///
|
||||
/// 该函数用于前端放大镜实时预览。
|
||||
pub fn capture_region_rgba(
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
) -> AppResult<Vec<u8>> {
|
||||
if width <= 0 || height <= 0 {
|
||||
return Err(AppError::ScreenAccessFailed("无效的捕获区域尺寸".to_string()));
|
||||
}
|
||||
|
||||
// 将捕获区域 clamp 到“虚拟屏幕”范围,避免在屏幕边缘 BitBlt 失败
|
||||
let v_left = unsafe { GetSystemMetrics(SM_XVIRTUALSCREEN) };
|
||||
let v_top = unsafe { GetSystemMetrics(SM_YVIRTUALSCREEN) };
|
||||
let v_w = unsafe { GetSystemMetrics(SM_CXVIRTUALSCREEN) };
|
||||
let v_h = unsafe { GetSystemMetrics(SM_CYVIRTUALSCREEN) };
|
||||
|
||||
// 如果请求区域比虚拟屏幕还大,直接报错(避免溢出/异常)
|
||||
if width > v_w || height > v_h {
|
||||
return Err(AppError::ScreenAccessFailed("捕获区域超出屏幕范围".to_string()));
|
||||
}
|
||||
|
||||
let max_x = v_left + v_w - width;
|
||||
let max_y = v_top + v_h - height;
|
||||
let x = x.clamp(v_left, max_x);
|
||||
let y = y.clamp(v_top, max_y);
|
||||
|
||||
unsafe {
|
||||
// 屏幕 DC
|
||||
let screen_dc = GetDC(HWND::default());
|
||||
if screen_dc.is_invalid() {
|
||||
return Err(AppError::ScreenAccessFailed("无法获取屏幕设备上下文".to_string()));
|
||||
}
|
||||
|
||||
// 内存 DC + 位图
|
||||
let mem_dc = CreateCompatibleDC(screen_dc);
|
||||
if mem_dc.is_invalid() {
|
||||
ReleaseDC(HWND::default(), screen_dc);
|
||||
return Err(AppError::ScreenAccessFailed("无法创建兼容设备上下文".to_string()));
|
||||
}
|
||||
|
||||
let bitmap: HBITMAP = CreateCompatibleBitmap(screen_dc, width, height);
|
||||
if bitmap.is_invalid() {
|
||||
let _ = DeleteDC(mem_dc);
|
||||
ReleaseDC(HWND::default(), screen_dc);
|
||||
return Err(AppError::ScreenAccessFailed("无法创建兼容位图".to_string()));
|
||||
}
|
||||
|
||||
let old_obj: HGDIOBJ = SelectObject(mem_dc, bitmap);
|
||||
|
||||
// 拷贝屏幕到位图
|
||||
let ok = BitBlt(mem_dc, 0, 0, width, height, screen_dc, x, y, SRCCOPY);
|
||||
|
||||
// 释放 screen dc(尽早)
|
||||
ReleaseDC(HWND::default(), screen_dc);
|
||||
|
||||
if ok.is_err() {
|
||||
// 恢复/清理
|
||||
let _ = SelectObject(mem_dc, old_obj);
|
||||
let _ = DeleteObject(bitmap);
|
||||
let _ = DeleteDC(mem_dc);
|
||||
return Err(AppError::ScreenAccessFailed("BitBlt 捕获失败".to_string()));
|
||||
}
|
||||
|
||||
// 准备 BITMAPINFO(32-bit BGRA),并用负高度得到“自顶向下”顺序
|
||||
let mut bmi = BITMAPINFO {
|
||||
bmiHeader: BITMAPINFOHEADER {
|
||||
biSize: std::mem::size_of::<BITMAPINFOHEADER>() as u32,
|
||||
biWidth: width,
|
||||
biHeight: -height, // top-down
|
||||
biPlanes: 1,
|
||||
biBitCount: 32,
|
||||
biCompression: BI_RGB.0 as u32,
|
||||
biSizeImage: 0,
|
||||
biXPelsPerMeter: 0,
|
||||
biYPelsPerMeter: 0,
|
||||
biClrUsed: 0,
|
||||
biClrImportant: 0,
|
||||
},
|
||||
bmiColors: [Default::default(); 1],
|
||||
};
|
||||
|
||||
let mut bgra = vec![0u8; (width as usize) * (height as usize) * 4];
|
||||
let lines = GetDIBits(
|
||||
mem_dc,
|
||||
bitmap,
|
||||
0,
|
||||
height as u32,
|
||||
Some(bgra.as_mut_ptr() as *mut _),
|
||||
&mut bmi,
|
||||
DIB_RGB_COLORS,
|
||||
);
|
||||
|
||||
// 恢复/清理 GDI 对象
|
||||
let _ = SelectObject(mem_dc, old_obj);
|
||||
let _ = DeleteObject(bitmap);
|
||||
let _ = DeleteDC(mem_dc);
|
||||
|
||||
if lines == 0 {
|
||||
return Err(AppError::ScreenAccessFailed("GetDIBits 读取失败".to_string()));
|
||||
}
|
||||
|
||||
// BGRA -> RGBA(给前端 canvas 更直接)
|
||||
for px in bgra.chunks_exact_mut(4) {
|
||||
let b = px[0];
|
||||
let r = px[2];
|
||||
px[0] = r;
|
||||
px[2] = b;
|
||||
// px[1] = g, px[3] = a 保持
|
||||
}
|
||||
|
||||
Ok(bgra)
|
||||
}
|
||||
}
|
||||
}
|
||||
54
src-tauri/src/utils/shortcut.rs
Normal file
54
src-tauri/src/utils/shortcut.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
//! 全局快捷键工具
|
||||
//!
|
||||
//! 将快捷键注册逻辑从 `lib.rs` 拆分出来,避免入口文件过于拥挤。
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tauri::Manager;
|
||||
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, Modifiers, Shortcut, ShortcutState};
|
||||
|
||||
/// 注册全局快捷键
|
||||
///
|
||||
/// - `Alt+Space`: 切换主窗口显示/隐藏
|
||||
pub fn register_global_shortcuts(app: &tauri::App) -> Result<(), String> {
|
||||
let shortcut = Shortcut::new(Some(Modifiers::ALT), Code::Space);
|
||||
let app_handle = app.handle().clone();
|
||||
let is_processing = Arc::new(AtomicBool::new(false));
|
||||
let is_processing_clone = is_processing.clone();
|
||||
|
||||
app.global_shortcut()
|
||||
.on_shortcut(shortcut, move |_app_handle, _shortcut, event| {
|
||||
// 忽略按键释放事件
|
||||
if event.state == ShortcutState::Released {
|
||||
return;
|
||||
}
|
||||
|
||||
// 防止重复触发
|
||||
if is_processing_clone.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
is_processing_clone.store(true, Ordering::SeqCst);
|
||||
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
let _ = window.is_visible().and_then(|is_visible| {
|
||||
if is_visible {
|
||||
window.hide()
|
||||
} else {
|
||||
window.show().and_then(|_| window.set_focus())
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 延迟重置处理标志,防止快速重复触发
|
||||
let is_processing_reset = is_processing_clone.clone();
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
is_processing_reset.store(false, Ordering::SeqCst);
|
||||
});
|
||||
})
|
||||
.map_err(|e| format!("注册全局快捷键失败: {}", e))?;
|
||||
|
||||
println!("全局快捷键 Alt+Space 注册成功");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "tauri-app",
|
||||
"productName": "CmdRs",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.shenjianz.tauri-app",
|
||||
"build": {
|
||||
@@ -12,11 +12,19 @@
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "tauri-app",
|
||||
"width": 800,
|
||||
"height": 600,
|
||||
"minWidth": 600,
|
||||
"minHeight": 500
|
||||
"title": "CmdRs - 功能集合",
|
||||
"label": "main",
|
||||
"width": 1200,
|
||||
"height": 800,
|
||||
"minWidth": 800,
|
||||
"minHeight": 600,
|
||||
"decorations": true,
|
||||
"transparent": false,
|
||||
"alwaysOnTop": false,
|
||||
"skipTaskbar": false,
|
||||
"visible": false,
|
||||
"center": true,
|
||||
"resizable": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
|
||||
Reference in New Issue
Block a user