2024-09-20 09:52:13 +08:00
|
|
|
|
use crate::steam;
|
|
|
|
|
|
use crate::tool::*;
|
2025-11-05 11:19:43 +08:00
|
|
|
|
use crate::tool::updater::{check_update, download_update, install_update, UpdateInfo};
|
2025-03-23 21:55:17 +08:00
|
|
|
|
use crate::vdf::preset;
|
2025-03-27 13:32:30 +08:00
|
|
|
|
use crate::vdf::preset::VideoConfig;
|
2025-03-20 14:13:03 +08:00
|
|
|
|
use crate::wrap_err;
|
|
|
|
|
|
use anyhow::Result;
|
2025-03-29 01:12:03 +08:00
|
|
|
|
use std::fs::File;
|
|
|
|
|
|
use std::fs;
|
2025-11-05 02:24:17 +08:00
|
|
|
|
use std::path::Path;
|
|
|
|
|
|
use std::io::{BufRead, BufReader};
|
2025-11-08 18:09:35 +08:00
|
|
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
|
|
|
|
use std::sync::{Arc, OnceLock};
|
2025-03-29 01:12:03 +08:00
|
|
|
|
use tauri::path::BaseDirectory;
|
|
|
|
|
|
use tauri::Manager;
|
2025-11-08 15:43:44 +08:00
|
|
|
|
use serde::{Deserialize, Serialize};
|
2025-03-29 01:12:03 +08:00
|
|
|
|
|
|
|
|
|
|
// use tauri_plugin_shell::ShellExt;
|
2025-03-20 14:13:03 +08:00
|
|
|
|
|
|
|
|
|
|
// pub type Result<T, String = ()> = Result<T, String>;
|
2024-09-20 09:52:13 +08:00
|
|
|
|
|
2025-11-08 18:09:35 +08:00
|
|
|
|
// 全局下载取消标志
|
|
|
|
|
|
static DOWNLOAD_CANCELLED: OnceLock<Arc<AtomicBool>> = OnceLock::new();
|
|
|
|
|
|
|
|
|
|
|
|
fn get_download_cancelled() -> Arc<AtomicBool> {
|
|
|
|
|
|
DOWNLOAD_CANCELLED.get_or_init(|| Arc::new(AtomicBool::new(false))).clone()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-09-20 09:52:13 +08:00
|
|
|
|
#[tauri::command]
|
2025-03-20 14:13:03 +08:00
|
|
|
|
pub fn greet(name: &str) -> Result<String, String> {
|
|
|
|
|
|
Ok(format!("Hello, {}! You've been greeted from Rust!", name))
|
2024-09-20 09:52:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 工具
|
|
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
2025-03-20 14:13:03 +08:00
|
|
|
|
pub fn launch_game(steam_path: &str, launch_option: &str, server: &str) -> Result<String, String> {
|
2025-03-29 01:12:03 +08:00
|
|
|
|
println!(
|
|
|
|
|
|
"{}: launching game on server: {}, with launch Option {}",
|
|
|
|
|
|
steam_path, server, launch_option
|
|
|
|
|
|
);
|
2025-03-28 13:22:26 +08:00
|
|
|
|
// wrap_err!(steam::launch_game(steam_path, launch_option, server));
|
2025-03-29 01:12:03 +08:00
|
|
|
|
// 如果有错误,打印出来
|
|
|
|
|
|
if let Err(e) = steam::launch_game(steam_path, launch_option, server) {
|
|
|
|
|
|
println!("Error: {}", e);
|
|
|
|
|
|
return Err(e.to_string());
|
|
|
|
|
|
}
|
|
|
|
|
|
// steam::launch_game(steam_path, launch_option, server);
|
2024-09-20 09:52:13 +08:00
|
|
|
|
|
2025-03-20 14:13:03 +08:00
|
|
|
|
Ok(format!(
|
2024-09-20 09:52:13 +08:00
|
|
|
|
"Launching game on server: {}, with launch Option {}",
|
|
|
|
|
|
server, launch_option
|
2025-03-20 14:13:03 +08:00
|
|
|
|
))
|
2024-09-20 09:52:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
2025-03-20 14:13:03 +08:00
|
|
|
|
pub fn kill_game() -> Result<String, String> {
|
|
|
|
|
|
Ok(common::kill("cs2.exe"))
|
2024-09-20 09:52:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-05 11:19:43 +08:00
|
|
|
|
#[tauri::command]
|
|
|
|
|
|
pub fn check_process_running(process_name: &str) -> Result<bool, String> {
|
|
|
|
|
|
Ok(common::check_process_running(process_name))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-09-20 09:52:13 +08:00
|
|
|
|
#[tauri::command]
|
2025-03-20 14:13:03 +08:00
|
|
|
|
pub fn kill_steam() -> Result<String, String> {
|
|
|
|
|
|
Ok(common::kill("steam.exe"))
|
2024-09-20 09:52:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Steam
|
|
|
|
|
|
#[tauri::command]
|
2025-03-20 14:13:03 +08:00
|
|
|
|
pub fn get_steam_path() -> Result<String, String> {
|
|
|
|
|
|
wrap_err!(steam::path::get_steam_path())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
2025-03-23 01:08:50 +08:00
|
|
|
|
pub fn get_cs_path(name: &str, steam_dir: &str) -> Result<String, String> {
|
|
|
|
|
|
wrap_err!(steam::path::get_cs_path(name, steam_dir))
|
2024-09-20 09:52:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-21 16:02:47 +08:00
|
|
|
|
#[tauri::command]
|
|
|
|
|
|
pub fn open_path(path: &str) -> Result<(), String> {
|
|
|
|
|
|
wrap_err!(common::open_path(path))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-20 14:13:03 +08:00
|
|
|
|
#[tauri::command]
|
2025-03-22 21:06:20 +08:00
|
|
|
|
pub fn get_powerplan() -> Result<i32, String> {
|
2025-03-20 14:13:03 +08:00
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
|
let powerplan = powerplan::get_powerplan()?;
|
|
|
|
|
|
|
2025-03-26 03:00:18 +08:00
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
2025-03-27 11:30:03 +08:00
|
|
|
|
let powerplan = powerplan::PowerPlanMode::Other as i32;
|
2025-03-24 09:55:21 +08:00
|
|
|
|
|
2025-03-20 14:13:03 +08:00
|
|
|
|
Ok(powerplan)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
2025-03-22 21:06:20 +08:00
|
|
|
|
pub fn set_powerplan(plan: i32) -> Result<(), String> {
|
2025-03-20 14:13:03 +08:00
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
|
powerplan::set_powerplan(plan)?;
|
|
|
|
|
|
|
2025-03-22 21:06:20 +08:00
|
|
|
|
Ok(())
|
2025-03-20 14:13:03 +08:00
|
|
|
|
}
|
2025-03-22 17:50:35 +08:00
|
|
|
|
|
2025-03-23 21:55:17 +08:00
|
|
|
|
#[tauri::command]
|
|
|
|
|
|
pub fn get_steam_users(steam_dir: &str) -> Result<Vec<preset::User>, String> {
|
2025-03-26 03:00:18 +08:00
|
|
|
|
wrap_err!(preset::get_users(steam_dir))
|
2025-03-23 21:55:17 +08:00
|
|
|
|
}
|
2024-09-20 09:52:13 +08:00
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
2025-03-20 14:13:03 +08:00
|
|
|
|
pub fn set_auto_login_user(user: &str) -> Result<String, String> {
|
2024-09-20 09:52:13 +08:00
|
|
|
|
#[cfg(target_os = "windows")]
|
2025-03-20 14:13:03 +08:00
|
|
|
|
steam::reg::set_auto_login_user(user)?;
|
2024-09-20 09:52:13 +08:00
|
|
|
|
|
2025-03-20 14:13:03 +08:00
|
|
|
|
Ok(format!("Set auto login user to {}", user))
|
2024-09-20 09:52:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-05 11:32:43 +08:00
|
|
|
|
#[tauri::command]
|
|
|
|
|
|
pub fn start_watch_loginusers(app: tauri::AppHandle, steam_dir: String) -> Result<(), String> {
|
|
|
|
|
|
wrap_err!(steam::watch::start_watch_loginusers(app, steam_dir))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-06 03:07:38 +08:00
|
|
|
|
#[tauri::command]
|
|
|
|
|
|
pub fn start_watch_cs2_video(
|
|
|
|
|
|
app: tauri::AppHandle,
|
|
|
|
|
|
steam_dir: String,
|
|
|
|
|
|
steam_id32: u32,
|
|
|
|
|
|
) -> Result<(), String> {
|
|
|
|
|
|
wrap_err!(steam::watch::start_watch_cs2_video(app, steam_dir, steam_id32))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
|
pub fn stop_watch_cs2_video() -> Result<(), String> {
|
|
|
|
|
|
steam::watch::stop_watch_cs2_video();
|
|
|
|
|
|
Ok(())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-27 13:32:30 +08:00
|
|
|
|
#[tauri::command]
|
|
|
|
|
|
pub fn get_cs2_video_config(steam_dir: &str, steam_id32: u32) -> Result<VideoConfig, String> {
|
|
|
|
|
|
let p = format!(
|
|
|
|
|
|
"{}/userdata/{}/730/local/cfg/cs2_video.txt",
|
|
|
|
|
|
steam_dir, steam_id32
|
|
|
|
|
|
);
|
|
|
|
|
|
let video = preset::get_cs2_video(p.as_str()).map_err(|e| e.to_string())?;
|
|
|
|
|
|
Ok(video)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
|
pub fn set_cs2_video_config(
|
|
|
|
|
|
steam_dir: &str,
|
|
|
|
|
|
steam_id32: u32,
|
|
|
|
|
|
video_config: VideoConfig,
|
|
|
|
|
|
) -> Result<(), String> {
|
|
|
|
|
|
let p = format!(
|
|
|
|
|
|
"{}/userdata/{}/730/local/cfg/cs2_video.txt",
|
|
|
|
|
|
steam_dir, steam_id32
|
|
|
|
|
|
);
|
|
|
|
|
|
preset::set_cs2_video(p.as_str(), video_config).map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-09-20 09:52:13 +08:00
|
|
|
|
#[tauri::command]
|
2025-03-20 14:13:03 +08:00
|
|
|
|
pub fn check_path(path: &str) -> Result<bool, String> {
|
|
|
|
|
|
Ok(std::path::Path::new(&path).exists())
|
2024-09-20 09:52:13 +08:00
|
|
|
|
}
|
2025-03-29 01:12:03 +08:00
|
|
|
|
|
2025-11-06 23:11:41 +08:00
|
|
|
|
#[tauri::command]
|
|
|
|
|
|
pub fn check_steam_dir_valid(steam_dir: &str) -> Result<bool, String> {
|
|
|
|
|
|
use std::path::Path;
|
|
|
|
|
|
|
|
|
|
|
|
let path = Path::new(steam_dir);
|
|
|
|
|
|
if !path.exists() {
|
|
|
|
|
|
return Ok(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否存在 steam.exe 或 config 目录(至少有一个即可)
|
|
|
|
|
|
let steam_exe = path.join("steam.exe");
|
|
|
|
|
|
let config_dir = path.join("config");
|
|
|
|
|
|
|
|
|
|
|
|
Ok(steam_exe.exists() || config_dir.exists())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-29 01:12:03 +08:00
|
|
|
|
///// 录像
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
|
pub async fn analyze_replay(app: tauri::AppHandle, path: &str) -> Result<String, String> {
|
|
|
|
|
|
// 检测文件是否存在
|
|
|
|
|
|
if !std::path::Path::new(&path).exists() {
|
|
|
|
|
|
return Err("文件不存在".to_string());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取应用配置目录
|
|
|
|
|
|
let config_dir = app
|
|
|
|
|
|
.path()
|
|
|
|
|
|
.resolve("metadata", BaseDirectory::AppConfig)
|
|
|
|
|
|
.expect("无法获取配置目录");
|
|
|
|
|
|
|
|
|
|
|
|
// 确保 metadata 文件夹存在
|
|
|
|
|
|
if !config_dir.exists() {
|
|
|
|
|
|
fs::create_dir_all(&config_dir).expect("无法创建 metadata 文件夹");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 提取文件名部分
|
|
|
|
|
|
let file_name = std::path::Path::new(path)
|
|
|
|
|
|
.file_name()
|
|
|
|
|
|
.and_then(|name| name.to_str())
|
|
|
|
|
|
.unwrap_or("default_filename");
|
|
|
|
|
|
|
|
|
|
|
|
// 拼接输出文件路径
|
|
|
|
|
|
let output_path = config_dir.join(format!("{}.json", file_name));
|
|
|
|
|
|
|
|
|
|
|
|
// 确保输出文件存在,如果不存在则创建空文件
|
|
|
|
|
|
if !output_path.exists() {
|
|
|
|
|
|
File::create(&output_path).expect("无法创建输出文件");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 调用项目绑定cli程序
|
|
|
|
|
|
let cli_path = app
|
|
|
|
|
|
.path()
|
|
|
|
|
|
.resolve("resources/csda", BaseDirectory::Resource)
|
|
|
|
|
|
.expect("analyzer not found");
|
|
|
|
|
|
println!("cli path: {}", cli_path.display());
|
|
|
|
|
|
|
|
|
|
|
|
let output = std::process::Command::new(cli_path)
|
|
|
|
|
|
.arg("-demo-path")
|
|
|
|
|
|
.arg(path)
|
|
|
|
|
|
.arg("-format")
|
|
|
|
|
|
.arg("json")
|
|
|
|
|
|
.arg("-minify")
|
|
|
|
|
|
.arg("-output")
|
|
|
|
|
|
.arg(output_path.to_str().expect("路径转换失败"))
|
|
|
|
|
|
.output()
|
|
|
|
|
|
.expect("Failed to execute command");
|
|
|
|
|
|
|
|
|
|
|
|
// 获取输出
|
|
|
|
|
|
let output_str = String::from_utf8_lossy(&output.stdout);
|
|
|
|
|
|
|
|
|
|
|
|
// 打印输出
|
|
|
|
|
|
println!("{}", output_str);
|
|
|
|
|
|
|
|
|
|
|
|
// 返回结果
|
|
|
|
|
|
Ok(output_str.to_string())
|
|
|
|
|
|
}
|
2025-11-05 02:24:17 +08:00
|
|
|
|
|
|
|
|
|
|
// 帧数测试相关
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
|
pub fn get_console_log_path(cs_path: &str) -> Result<String, String> {
|
|
|
|
|
|
// cs_path 是类似 "game\bin\win64" 的路径,需要向上找到 game\csgo\console.log
|
|
|
|
|
|
let path = Path::new(cs_path);
|
|
|
|
|
|
// 向上找到 game 目录
|
|
|
|
|
|
if let Some(game_dir) = path.ancestors().find(|p| {
|
|
|
|
|
|
p.file_name()
|
|
|
|
|
|
.and_then(|n| n.to_str())
|
|
|
|
|
|
.map(|n| n == "game")
|
|
|
|
|
|
.unwrap_or(false)
|
|
|
|
|
|
}) {
|
|
|
|
|
|
let console_log_path = game_dir.join("csgo").join("console.log");
|
|
|
|
|
|
Ok(console_log_path.to_string_lossy().to_string())
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Err("无法找到 game 目录".to_string())
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
|
pub fn read_vprof_report(console_log_path: &str) -> Result<String, String> {
|
|
|
|
|
|
let path = Path::new(console_log_path);
|
|
|
|
|
|
|
|
|
|
|
|
if !path.exists() {
|
|
|
|
|
|
return Err("console.log 文件不存在".to_string());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let file = File::open(path).map_err(|e| format!("无法打开文件: {}", e))?;
|
|
|
|
|
|
let reader = BufReader::new(file);
|
|
|
|
|
|
|
|
|
|
|
|
let mut vprof_lines = Vec::new();
|
|
|
|
|
|
let mut in_vprof_section = false;
|
|
|
|
|
|
let mut empty_line_count = 0;
|
|
|
|
|
|
|
|
|
|
|
|
for line_result in reader.lines() {
|
|
|
|
|
|
let line = line_result.map_err(|e| format!("读取行错误: {}", e))?;
|
|
|
|
|
|
|
|
|
|
|
|
// 检测 [VProf] 标记
|
|
|
|
|
|
if line.contains("[VProf]") {
|
|
|
|
|
|
in_vprof_section = true;
|
|
|
|
|
|
empty_line_count = 0;
|
|
|
|
|
|
vprof_lines.push(line.clone());
|
|
|
|
|
|
} else if in_vprof_section {
|
|
|
|
|
|
// 如果在 VProf 部分中
|
|
|
|
|
|
if line.trim().is_empty() {
|
|
|
|
|
|
empty_line_count += 1;
|
|
|
|
|
|
// 如果遇到两个连续的空行,结束 VProf 部分
|
|
|
|
|
|
if empty_line_count >= 2 {
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
vprof_lines.push(line.clone());
|
|
|
|
|
|
} else {
|
|
|
|
|
|
empty_line_count = 0;
|
|
|
|
|
|
vprof_lines.push(line.clone());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if vprof_lines.is_empty() {
|
|
|
|
|
|
return Err("未找到 [VProf] 报告".to_string());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Ok(vprof_lines.join("\n"))
|
|
|
|
|
|
}
|
2025-11-05 11:19:43 +08:00
|
|
|
|
|
|
|
|
|
|
// 更新相关命令
|
|
|
|
|
|
|
2025-11-08 18:09:35 +08:00
|
|
|
|
/// 检查更新(支持 GitHub Release 和自定义端点)
|
2025-11-05 11:19:43 +08:00
|
|
|
|
#[tauri::command]
|
|
|
|
|
|
pub async fn check_app_update(
|
|
|
|
|
|
app: tauri::AppHandle,
|
2025-11-08 18:09:35 +08:00
|
|
|
|
endpoint: Option<String>,
|
|
|
|
|
|
use_mirror: Option<bool>,
|
|
|
|
|
|
include_prerelease: Option<bool>,
|
2025-11-05 11:19:43 +08:00
|
|
|
|
) -> Result<Option<UpdateInfo>, String> {
|
|
|
|
|
|
let current_version = app.package_info().version.to_string();
|
2025-11-08 18:09:35 +08:00
|
|
|
|
let use_mirror = use_mirror.unwrap_or(false);
|
|
|
|
|
|
let include_prerelease = include_prerelease.unwrap_or(false);
|
|
|
|
|
|
|
2025-11-08 18:16:24 +08:00
|
|
|
|
// println!("[检查更新命令] 当前应用版本: {}", current_version);
|
|
|
|
|
|
// println!("[检查更新命令] 使用镜像: {}", use_mirror);
|
|
|
|
|
|
// println!("[检查更新命令] 包含预发布版本: {}", include_prerelease);
|
|
|
|
|
|
// if let Some(ref ep) = endpoint {
|
|
|
|
|
|
// println!("[检查更新命令] 自定义端点: {}", ep);
|
|
|
|
|
|
// }
|
2025-11-08 18:09:35 +08:00
|
|
|
|
|
|
|
|
|
|
// 从环境变量获取 GitHub 仓库信息,如果没有则使用默认值
|
|
|
|
|
|
const DEFAULT_GITHUB_REPO: &str = "plsgo/cstb";
|
|
|
|
|
|
let github_repo_str = std::env::var("GITHUB_REPO").ok();
|
|
|
|
|
|
let github_repo = github_repo_str.as_deref().unwrap_or(DEFAULT_GITHUB_REPO);
|
2025-11-08 18:16:24 +08:00
|
|
|
|
// println!("[检查更新命令] GitHub 仓库: {}", github_repo);
|
2025-11-08 18:09:35 +08:00
|
|
|
|
|
|
|
|
|
|
let result = wrap_err!(check_update(
|
|
|
|
|
|
endpoint.as_deref(),
|
|
|
|
|
|
¤t_version,
|
|
|
|
|
|
use_mirror,
|
|
|
|
|
|
Some(github_repo),
|
|
|
|
|
|
include_prerelease
|
|
|
|
|
|
).await)?;
|
|
|
|
|
|
|
2025-11-08 18:16:24 +08:00
|
|
|
|
// if result.is_some() {
|
|
|
|
|
|
// println!("[检查更新命令] ✓ 返回更新信息");
|
|
|
|
|
|
// } else {
|
|
|
|
|
|
// println!("[检查更新命令] ✗ 无更新可用");
|
|
|
|
|
|
// }
|
2025-11-05 11:19:43 +08:00
|
|
|
|
|
2025-11-08 18:09:35 +08:00
|
|
|
|
Ok(result)
|
2025-11-05 11:19:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 下载更新
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
|
pub async fn download_app_update(
|
|
|
|
|
|
app: tauri::AppHandle,
|
|
|
|
|
|
download_url: String,
|
|
|
|
|
|
) -> Result<String, String> {
|
2025-11-08 18:09:35 +08:00
|
|
|
|
// 重置取消标志
|
|
|
|
|
|
let cancelled = get_download_cancelled();
|
|
|
|
|
|
cancelled.store(false, Ordering::Relaxed);
|
2025-11-05 11:19:43 +08:00
|
|
|
|
|
2025-11-08 18:09:35 +08:00
|
|
|
|
let path = wrap_err!(download_update(&app, &download_url, cancelled).await)?;
|
2025-11-05 11:19:43 +08:00
|
|
|
|
Ok(path.to_string_lossy().to_string())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-08 18:09:35 +08:00
|
|
|
|
/// 取消下载
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
|
pub fn cancel_download_update() -> Result<(), String> {
|
|
|
|
|
|
let cancelled = get_download_cancelled();
|
|
|
|
|
|
cancelled.store(true, Ordering::Relaxed);
|
|
|
|
|
|
Ok(())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-05 11:19:43 +08:00
|
|
|
|
/// 安装更新
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
|
pub fn install_app_update(installer_path: String) -> Result<(), String> {
|
|
|
|
|
|
wrap_err!(install_update(&installer_path))
|
|
|
|
|
|
}
|
2025-11-08 13:24:00 +08:00
|
|
|
|
|
|
|
|
|
|
/// 获取 PowerShell Get-ComputerInfo 信息(异步版本)
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
|
pub async fn get_computer_info() -> Result<serde_json::Value, String> {
|
|
|
|
|
|
use tokio::process::Command;
|
|
|
|
|
|
|
|
|
|
|
|
// 异步执行 PowerShell 命令获取计算机信息并转换为 JSON
|
|
|
|
|
|
let output = Command::new("powershell")
|
|
|
|
|
|
.args(&[
|
|
|
|
|
|
"-NoProfile",
|
|
|
|
|
|
"-Command",
|
|
|
|
|
|
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-ComputerInfo | Select-Object OsName, OSDisplayVersion, BiosSMBIOSBIOSVersion, CsManufacturer, CsName | ConvertTo-Json -Compress"
|
|
|
|
|
|
])
|
|
|
|
|
|
.output()
|
|
|
|
|
|
.await
|
|
|
|
|
|
.map_err(|e| format!("执行 PowerShell 命令失败: {}", e))?;
|
|
|
|
|
|
|
|
|
|
|
|
if !output.status.success() {
|
|
|
|
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
|
|
|
|
return Err(format!("PowerShell 命令执行失败: {}", stderr));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理 PowerShell 输出,移除 BOM 和空白字符
|
|
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
|
|
|
|
let cleaned = stdout.trim().trim_start_matches('\u{feff}'); // 移除 BOM
|
|
|
|
|
|
|
|
|
|
|
|
// 如果输出为空,返回空对象
|
|
|
|
|
|
if cleaned.is_empty() {
|
|
|
|
|
|
return Ok(serde_json::json!({}));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-08 15:43:44 +08:00
|
|
|
|
let mut json: serde_json::Value = serde_json::from_str(cleaned)
|
2025-11-08 13:24:00 +08:00
|
|
|
|
.map_err(|e| format!("解析 JSON 失败: {},原始输出: {}", e, cleaned))?;
|
|
|
|
|
|
|
2025-11-08 15:43:44 +08:00
|
|
|
|
// 对于 Windows 11,优先使用 OSDisplayVersion 获取版本代码(如 25H2)
|
|
|
|
|
|
// 如果 OSDisplayVersion 存在且包含版本代码,提取它
|
|
|
|
|
|
if let Some(os_display_version) = json.get("OSDisplayVersion").and_then(|v| v.as_str()) {
|
|
|
|
|
|
// OSDisplayVersion 格式可能是 "25H2" 或 "Windows 11 版本 25H2" 等
|
|
|
|
|
|
// 尝试提取版本代码(如 25H2)
|
|
|
|
|
|
if let Some(capture) = regex::Regex::new(r"(\d+H\d+)")
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.and_then(|re| re.captures(os_display_version))
|
|
|
|
|
|
{
|
|
|
|
|
|
if let Some(version_code) = capture.get(1) {
|
|
|
|
|
|
json["ReleaseId"] = serde_json::Value::String(version_code.as_str().to_string());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果没有从 OSDisplayVersion 获取到版本代码,尝试从注册表获取 ReleaseId
|
|
|
|
|
|
if !json.get("ReleaseId").and_then(|v| v.as_str()).is_some() {
|
|
|
|
|
|
let release_id_output = Command::new("powershell")
|
|
|
|
|
|
.args(&[
|
|
|
|
|
|
"-NoProfile",
|
|
|
|
|
|
"-Command",
|
|
|
|
|
|
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; try { (Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion').ReleaseId } catch { $null }"
|
|
|
|
|
|
])
|
|
|
|
|
|
.output()
|
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
|
|
if let Ok(release_output) = release_id_output {
|
|
|
|
|
|
if release_output.status.success() {
|
|
|
|
|
|
let release_str = String::from_utf8_lossy(&release_output.stdout).trim().to_string();
|
|
|
|
|
|
if !release_str.is_empty() {
|
|
|
|
|
|
json["ReleaseId"] = serde_json::Value::String(release_str);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-08 13:24:00 +08:00
|
|
|
|
Ok(json)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 获取 PowerShell Get-ComputerInfo 信息(非 Windows 平台返回空对象)
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
|
|
|
|
pub async fn get_computer_info() -> Result<serde_json::Value, String> {
|
|
|
|
|
|
Ok(serde_json::json!({}))
|
2025-11-08 15:43:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// GPU 信息结构体
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
|
|
pub struct GpuInfo {
|
|
|
|
|
|
vendor: String,
|
|
|
|
|
|
model: String,
|
|
|
|
|
|
family: String,
|
|
|
|
|
|
device_id: String,
|
|
|
|
|
|
total_vram: u64,
|
|
|
|
|
|
used_vram: u64,
|
|
|
|
|
|
load_pct: u32,
|
|
|
|
|
|
temperature: f64,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 辅助函数:格式化字节数
|
|
|
|
|
|
fn format_bytes(bytes: u64) -> String {
|
|
|
|
|
|
let gb = bytes as f64 / 1024.0 / 1024.0 / 1024.0;
|
|
|
|
|
|
if gb >= 1.0 {
|
|
|
|
|
|
format!("{:.2}GB", gb)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let mb = bytes as f64 / 1024.0 / 1024.0;
|
|
|
|
|
|
format!("{:.2}MB", mb)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 获取 GPU 信息
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
|
pub fn get_gpu_info() -> Result<Option<GpuInfo>, String> {
|
|
|
|
|
|
use gfxinfo::active_gpu;
|
|
|
|
|
|
|
|
|
|
|
|
match active_gpu() {
|
|
|
|
|
|
Ok(gpu) => {
|
|
|
|
|
|
let info = gpu.info();
|
|
|
|
|
|
let temp = info.temperature() as f64 / 1000.0;
|
|
|
|
|
|
|
|
|
|
|
|
Ok(Some(GpuInfo {
|
|
|
|
|
|
vendor: gpu.vendor().to_string(),
|
|
|
|
|
|
model: gpu.model().to_string(),
|
|
|
|
|
|
family: gpu.family().to_string(),
|
|
|
|
|
|
device_id: gpu.device_id().to_string(),
|
|
|
|
|
|
total_vram: info.total_vram(),
|
|
|
|
|
|
used_vram: info.used_vram(),
|
|
|
|
|
|
load_pct: info.load_pct(),
|
|
|
|
|
|
temperature: temp,
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(e) => {
|
|
|
|
|
|
println!("✗ GPU 信息获取失败: {}", e);
|
|
|
|
|
|
Ok(None)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-08 16:34:37 +08:00
|
|
|
|
/// 内存信息结构体
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
|
|
pub struct MemoryInfo {
|
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
|
capacity: Option<u64>, // 容量(字节)
|
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
|
manufacturer: Option<String>,
|
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
|
speed: Option<u32>, // MHz,实际频率 ConfiguredClockSpeed
|
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
|
default_speed: Option<u32>, // MHz,默认频率 Speed(如果存在)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 显示器信息结构体
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
|
|
pub struct MonitorInfo {
|
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
|
manufacturer: Option<String>,
|
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
|
model: Option<String>,
|
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
|
name: Option<String>,
|
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
|
refresh_rate: Option<u32>, // Hz
|
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
|
resolution_width: Option<u32>,
|
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
|
resolution_height: Option<u32>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 主板信息结构体
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
|
|
pub struct MotherboardInfo {
|
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
|
manufacturer: Option<String>, // 制造商
|
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
|
model: Option<String>, // 型号
|
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
|
version: Option<String>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 获取内存信息(Windows)
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
|
pub async fn get_memory_info() -> Result<Vec<MemoryInfo>, String> {
|
|
|
|
|
|
use tokio::process::Command;
|
|
|
|
|
|
|
|
|
|
|
|
// 执行 PowerShell 命令获取内存信息
|
|
|
|
|
|
let output = Command::new("powershell")
|
|
|
|
|
|
.args(&[
|
|
|
|
|
|
"-NoProfile",
|
|
|
|
|
|
"-Command",
|
|
|
|
|
|
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-WmiObject Win32_PhysicalMemory | Select-Object Capacity, Manufacturer, ConfiguredClockSpeed, Speed | ConvertTo-Json -Compress"
|
|
|
|
|
|
])
|
|
|
|
|
|
.output()
|
|
|
|
|
|
.await
|
|
|
|
|
|
.map_err(|e| format!("执行 PowerShell 命令失败: {}", e))?;
|
|
|
|
|
|
|
|
|
|
|
|
if !output.status.success() {
|
|
|
|
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
|
|
|
|
return Err(format!("PowerShell 命令执行失败: {}", stderr));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
|
|
|
|
let cleaned = stdout.trim().trim_start_matches('\u{feff}');
|
|
|
|
|
|
|
|
|
|
|
|
if cleaned.is_empty() {
|
|
|
|
|
|
return Ok(vec![]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// PowerShell 可能返回数组或单个对象
|
|
|
|
|
|
let json: serde_json::Value = serde_json::from_str(cleaned)
|
|
|
|
|
|
.map_err(|e| format!("解析 JSON 失败: {},原始输出: {}", e, cleaned))?;
|
|
|
|
|
|
|
|
|
|
|
|
let mut memory_list = Vec::new();
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(array) = json.as_array() {
|
|
|
|
|
|
// 如果是数组
|
|
|
|
|
|
for item in array {
|
|
|
|
|
|
memory_list.push(parse_memory_info(item));
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 如果是单个对象
|
|
|
|
|
|
memory_list.push(parse_memory_info(&json));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Ok(memory_list)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 解析内存信息
|
|
|
|
|
|
fn parse_memory_info(json: &serde_json::Value) -> MemoryInfo {
|
|
|
|
|
|
// 容量(字节)
|
|
|
|
|
|
let capacity = json.get("Capacity")
|
|
|
|
|
|
.and_then(|v| v.as_u64());
|
|
|
|
|
|
|
|
|
|
|
|
let manufacturer = json.get("Manufacturer")
|
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
|
.map(|s| s.trim().to_string())
|
|
|
|
|
|
.filter(|s| !s.is_empty());
|
|
|
|
|
|
|
|
|
|
|
|
// 实际频率:优先使用 ConfiguredClockSpeed
|
|
|
|
|
|
let speed = json.get("ConfiguredClockSpeed")
|
|
|
|
|
|
.and_then(|v| v.as_u64())
|
|
|
|
|
|
.map(|v| v as u32);
|
|
|
|
|
|
|
|
|
|
|
|
// 默认频率:Speed(如果存在)
|
|
|
|
|
|
let default_speed = json.get("Speed")
|
|
|
|
|
|
.and_then(|v| v.as_u64())
|
|
|
|
|
|
.map(|v| v as u32);
|
|
|
|
|
|
|
|
|
|
|
|
MemoryInfo {
|
|
|
|
|
|
capacity,
|
|
|
|
|
|
manufacturer,
|
|
|
|
|
|
speed,
|
|
|
|
|
|
default_speed,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 获取内存信息(非 Windows 平台返回空)
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
|
|
|
|
pub async fn get_memory_info() -> Result<Vec<MemoryInfo>, String> {
|
|
|
|
|
|
Ok(vec![])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 获取显示器信息(Windows)
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
|
pub async fn get_monitor_info() -> Result<Vec<MonitorInfo>, String> {
|
|
|
|
|
|
use tokio::process::Command;
|
|
|
|
|
|
|
|
|
|
|
|
// 执行 PowerShell 命令获取显示器信息
|
|
|
|
|
|
let output = Command::new("powershell")
|
|
|
|
|
|
.args(&[
|
|
|
|
|
|
"-NoProfile",
|
|
|
|
|
|
"-Command",
|
|
|
|
|
|
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-CimInstance -Namespace root/wmi -ClassName WmiMonitorID | ForEach-Object { [PSCustomObject]@{ Manufacturer = [System.Text.Encoding]::ASCII.GetString($_.ManufacturerName) -replace \"`0\"; Model = [System.Text.Encoding]::ASCII.GetString($_.UserFriendlyName) -replace \"`0\" } } | ConvertTo-Json -Compress"
|
|
|
|
|
|
])
|
|
|
|
|
|
.output()
|
|
|
|
|
|
.await
|
|
|
|
|
|
.map_err(|e| format!("执行 PowerShell 命令失败: {}", e))?;
|
|
|
|
|
|
|
|
|
|
|
|
if !output.status.success() {
|
|
|
|
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
|
|
|
|
return Err(format!("PowerShell 命令执行失败: {}", stderr));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
|
|
|
|
let cleaned = stdout.trim().trim_start_matches('\u{feff}');
|
|
|
|
|
|
|
|
|
|
|
|
if cleaned.is_empty() {
|
|
|
|
|
|
return Ok(vec![]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let json: serde_json::Value = serde_json::from_str(cleaned)
|
|
|
|
|
|
.map_err(|e| format!("解析 JSON 失败: {},原始输出: {}", e, cleaned))?;
|
|
|
|
|
|
|
|
|
|
|
|
let mut monitor_list = Vec::new();
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(array) = json.as_array() {
|
|
|
|
|
|
for item in array {
|
|
|
|
|
|
monitor_list.push(parse_monitor_info(item));
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
monitor_list.push(parse_monitor_info(&json));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 尝试获取刷新率和分辨率信息
|
2025-11-08 18:09:35 +08:00
|
|
|
|
let _display_output = Command::new("powershell")
|
2025-11-08 16:34:37 +08:00
|
|
|
|
.args(&[
|
|
|
|
|
|
"-NoProfile",
|
|
|
|
|
|
"-Command",
|
|
|
|
|
|
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-CimInstance -Namespace root/wmi -ClassName WmiMonitorBasicDisplayParams | Select-Object MaxHorizontalImageSize, MaxVerticalImageSize | ConvertTo-Json -Compress"
|
|
|
|
|
|
])
|
|
|
|
|
|
.output()
|
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
|
|
// 获取刷新率信息
|
2025-11-08 18:09:35 +08:00
|
|
|
|
let _refresh_output = Command::new("powershell")
|
2025-11-08 16:34:37 +08:00
|
|
|
|
.args(&[
|
|
|
|
|
|
"-NoProfile",
|
|
|
|
|
|
"-Command",
|
|
|
|
|
|
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-CimInstance -Namespace root/wmi -ClassName WmiMonitorListedSupportedSourceModes | Select-Object -First 1 -ExpandProperty ModeTimings | Select-Object -First 1 -ExpandProperty RefreshRate | ConvertTo-Json -Compress"
|
|
|
|
|
|
])
|
|
|
|
|
|
.output()
|
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
|
|
// 获取当前显示器的分辨率和刷新率
|
|
|
|
|
|
let current_display_output = Command::new("powershell")
|
|
|
|
|
|
.args(&[
|
|
|
|
|
|
"-NoProfile",
|
|
|
|
|
|
"-Command",
|
|
|
|
|
|
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Add-Type -AssemblyName System.Windows.Forms; $screens = [System.Windows.Forms.Screen]::AllScreens; $screens | ForEach-Object { [PSCustomObject]@{ Width = $_.Bounds.Width; Height = $_.Bounds.Height; Primary = $_.Primary } } | ConvertTo-Json -Compress"
|
|
|
|
|
|
])
|
|
|
|
|
|
.output()
|
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
|
|
// 合并显示器信息
|
|
|
|
|
|
if let Ok(display_result) = current_display_output {
|
|
|
|
|
|
if display_result.status.success() {
|
|
|
|
|
|
let display_str = String::from_utf8_lossy(&display_result.stdout).to_string();
|
|
|
|
|
|
let display_str = display_str.trim().trim_start_matches('\u{feff}').to_string();
|
|
|
|
|
|
if let Ok(display_json) = serde_json::from_str::<serde_json::Value>(&display_str) {
|
|
|
|
|
|
if let Some(displays) = display_json.as_array() {
|
|
|
|
|
|
for (i, display) in displays.iter().enumerate() {
|
|
|
|
|
|
if i < monitor_list.len() {
|
|
|
|
|
|
if let Some(width) = display.get("Width").and_then(|v| v.as_u64()) {
|
|
|
|
|
|
monitor_list[i].resolution_width = Some(width as u32);
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(height) = display.get("Height").and_then(|v| v.as_u64()) {
|
|
|
|
|
|
monitor_list[i].resolution_height = Some(height as u32);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if let Some(display) = display_json.as_object() {
|
|
|
|
|
|
if monitor_list.len() > 0 {
|
|
|
|
|
|
if let Some(width) = display.get("Width").and_then(|v| v.as_u64()) {
|
|
|
|
|
|
monitor_list[0].resolution_width = Some(width as u32);
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(height) = display.get("Height").and_then(|v| v.as_u64()) {
|
|
|
|
|
|
monitor_list[0].resolution_height = Some(height as u32);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Ok(monitor_list)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 解析显示器信息
|
|
|
|
|
|
fn parse_monitor_info(json: &serde_json::Value) -> MonitorInfo {
|
|
|
|
|
|
let manufacturer = json.get("Manufacturer")
|
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
|
.map(|s| s.trim().to_string())
|
|
|
|
|
|
.filter(|s| !s.is_empty());
|
|
|
|
|
|
|
|
|
|
|
|
let model = json.get("Model")
|
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
|
.map(|s| s.trim().to_string())
|
|
|
|
|
|
.filter(|s| !s.is_empty());
|
|
|
|
|
|
|
|
|
|
|
|
// 组合制造商和型号作为名称
|
|
|
|
|
|
let name = match (&manufacturer, &model) {
|
|
|
|
|
|
(Some(mfg), Some(model_name)) => Some(format!("{} {}", mfg, model_name)),
|
|
|
|
|
|
(Some(mfg), None) => Some(mfg.clone()),
|
|
|
|
|
|
(None, Some(model_name)) => Some(model_name.clone()),
|
|
|
|
|
|
(None, None) => None,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
MonitorInfo {
|
|
|
|
|
|
manufacturer,
|
|
|
|
|
|
model,
|
|
|
|
|
|
name,
|
|
|
|
|
|
refresh_rate: None, // 需要从其他来源获取
|
|
|
|
|
|
resolution_width: None,
|
|
|
|
|
|
resolution_height: None,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-08 15:43:44 +08:00
|
|
|
|
/// 获取显示器信息(非 Windows 平台返回空)
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
|
|
|
|
pub async fn get_monitor_info() -> Result<Vec<MonitorInfo>, String> {
|
|
|
|
|
|
Ok(vec![])
|
2025-11-08 16:34:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 获取主板信息(Windows)
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
|
pub async fn get_motherboard_info() -> Result<MotherboardInfo, String> {
|
|
|
|
|
|
use tokio::process::Command;
|
|
|
|
|
|
|
|
|
|
|
|
// 执行 PowerShell 命令获取主板信息
|
|
|
|
|
|
let output = Command::new("powershell")
|
|
|
|
|
|
.args(&[
|
|
|
|
|
|
"-NoProfile",
|
|
|
|
|
|
"-Command",
|
|
|
|
|
|
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-CimInstance -ClassName Win32_BaseBoard | Select-Object Manufacturer, Product, Version | ConvertTo-Json -Compress"
|
|
|
|
|
|
])
|
|
|
|
|
|
.output()
|
|
|
|
|
|
.await
|
|
|
|
|
|
.map_err(|e| format!("执行 PowerShell 命令失败: {}", e))?;
|
|
|
|
|
|
|
|
|
|
|
|
if !output.status.success() {
|
|
|
|
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
|
|
|
|
return Err(format!("PowerShell 命令执行失败: {}", stderr));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
|
|
|
|
let cleaned = stdout.trim().trim_start_matches('\u{feff}');
|
|
|
|
|
|
|
|
|
|
|
|
if cleaned.is_empty() {
|
|
|
|
|
|
return Ok(MotherboardInfo {
|
|
|
|
|
|
manufacturer: None,
|
|
|
|
|
|
model: None,
|
|
|
|
|
|
version: None,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let json: serde_json::Value = serde_json::from_str(cleaned)
|
|
|
|
|
|
.map_err(|e| format!("解析 JSON 失败: {},原始输出: {}", e, cleaned))?;
|
|
|
|
|
|
|
|
|
|
|
|
// 分别获取制造商和型号
|
|
|
|
|
|
let manufacturer = json.get("Manufacturer")
|
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
|
.map(|s| s.trim().to_string())
|
|
|
|
|
|
.filter(|s| !s.is_empty());
|
|
|
|
|
|
|
|
|
|
|
|
let model = json.get("Product")
|
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
|
.map(|s| s.trim().to_string())
|
|
|
|
|
|
.filter(|s| !s.is_empty());
|
|
|
|
|
|
|
|
|
|
|
|
Ok(MotherboardInfo {
|
|
|
|
|
|
manufacturer,
|
|
|
|
|
|
model,
|
|
|
|
|
|
version: json.get("Version")
|
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
|
.map(|s| s.trim().to_string())
|
|
|
|
|
|
.filter(|s| !s.is_empty()),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 获取主板信息(非 Windows 平台返回空)
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
|
|
|
|
pub async fn get_motherboard_info() -> Result<MotherboardInfo, String> {
|
|
|
|
|
|
Ok(MotherboardInfo {
|
|
|
|
|
|
manufacturer: None,
|
|
|
|
|
|
model: None,
|
|
|
|
|
|
version: None,
|
|
|
|
|
|
})
|
2025-11-08 13:24:00 +08:00
|
|
|
|
}
|