1125-2142
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
7
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"Vue.volar",
|
||||||
|
"tauri-apps.tauri-vscode",
|
||||||
|
"rust-lang.rust-analyzer"
|
||||||
|
]
|
||||||
|
}
|
||||||
7
README.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Tauri + Vue 3
|
||||||
|
|
||||||
|
This template should help get you started developing with Tauri + Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
- [VS Code](https://code.visualstudio.com/) + [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
||||||
14
index.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Tauri + Vue 3 App</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1518
package-lock.json
generated
Normal file
22
package.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "system-doctor",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"tauri": "tauri"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"@tauri-apps/api": "^2",
|
||||||
|
"@tauri-apps/plugin-opener": "^2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"vite": "^6.0.3",
|
||||||
|
"@tauri-apps/cli": "^2"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
public/tauri.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
|
||||||
|
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
1
public/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
7
src-tauri/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
/target/
|
||||||
|
|
||||||
|
# Generated by Tauri
|
||||||
|
# will have schema files for capabilities auto-completion
|
||||||
|
/gen/schemas
|
||||||
5427
src-tauri/Cargo.lock
generated
Normal file
32
src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
[package]
|
||||||
|
name = "system-doctor"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "A Tauri App"
|
||||||
|
authors = ["you"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
# The `_lib` suffix may seem redundant but it is necessary
|
||||||
|
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||||
|
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||||
|
name = "system_doctor_lib"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tauri = { version = "2", features = [] }
|
||||||
|
tauri-plugin-opener = "2"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
sysinfo = "0.30"
|
||||||
|
wmi = "0.13"
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
# this feature is used for production builds or when `devPath` points to the filesystem
|
||||||
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
3
src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
10
src-tauri/capabilities/default.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "Capability for the main window",
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": [
|
||||||
|
"core:default",
|
||||||
|
"opener:default"
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 974 B |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 903 B |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
14
src-tauri/src/lib.rs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
pub fn run() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_opener::init())
|
||||||
|
.invoke_handler(tauri::generate_handler![greet])
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
|
}
|
||||||
360
src-tauri/src/main.rs
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
// 全局允许非标准命名风格 (因为 WMI 结构体必须匹配 Windows API 的命名)
|
||||||
|
#![allow(non_camel_case_types, non_snake_case)]
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
use sysinfo::{System, Disks};
|
||||||
|
use wmi::{COMLibrary, WMIConnection};
|
||||||
|
use std::fs;
|
||||||
|
// 引入 chrono 用于时间格式化
|
||||||
|
use chrono::{FixedOffset, Local, NaiveDate, TimeZone};
|
||||||
|
|
||||||
|
// --- 1. 数据结构 ---
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct SystemHealthReport {
|
||||||
|
hardware: HardwareSummary,
|
||||||
|
storage: Vec<StorageDevice>,
|
||||||
|
events: Vec<SystemEvent>,
|
||||||
|
minidumps: MinidumpInfo,
|
||||||
|
drivers: Vec<DriverIssue>,
|
||||||
|
battery: Option<BatteryInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct HardwareSummary {
|
||||||
|
cpu_name: String,
|
||||||
|
|
||||||
|
// [新增] 1. 整机信息 (适合品牌机/笔记本)
|
||||||
|
sys_vendor: String,
|
||||||
|
sys_product: String,
|
||||||
|
|
||||||
|
// [新增] 2. 主板信息 (适合 DIY 组装机)
|
||||||
|
mobo_vendor: String,
|
||||||
|
mobo_product: String,
|
||||||
|
|
||||||
|
memory_total_gb: u64,
|
||||||
|
memory_used_gb: u64,
|
||||||
|
os_version: String,
|
||||||
|
bios_version: String,
|
||||||
|
c_drive_total_gb: u64,
|
||||||
|
c_drive_used_gb: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct StorageDevice {
|
||||||
|
model: String,
|
||||||
|
health_status: String,
|
||||||
|
human_explanation: String,
|
||||||
|
is_danger: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct SystemEvent {
|
||||||
|
time_generated: String,
|
||||||
|
event_id: u32,
|
||||||
|
source: String,
|
||||||
|
analysis_hint: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct MinidumpInfo {
|
||||||
|
found: bool,
|
||||||
|
count: usize,
|
||||||
|
explanation: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct DriverIssue {
|
||||||
|
device_name: String,
|
||||||
|
error_code: u32,
|
||||||
|
description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct BatteryInfo {
|
||||||
|
health_percentage: u32,
|
||||||
|
is_ac_connected: bool,
|
||||||
|
explanation: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- WMI 反序列化结构 ---
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug)]
|
||||||
|
struct Win32_DiskDrive {
|
||||||
|
Model: Option<String>,
|
||||||
|
Status: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug)]
|
||||||
|
struct Win32_NTLogEvent {
|
||||||
|
TimeGenerated: String,
|
||||||
|
EventCode: u32,
|
||||||
|
SourceName: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug)]
|
||||||
|
struct Win32_PnPEntity {
|
||||||
|
Name: Option<String>,
|
||||||
|
ConfigManagerErrorCode: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug)]
|
||||||
|
struct Win32_Battery {
|
||||||
|
DesignCapacity: Option<u32>,
|
||||||
|
FullChargeCapacity: Option<u32>,
|
||||||
|
BatteryStatus: Option<u16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug)]
|
||||||
|
struct Win32_BIOS {
|
||||||
|
SMBIOSBIOSVersion: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主板 WMI 结构 (DIY看这个)
|
||||||
|
#[derive(serde::Deserialize, Debug)]
|
||||||
|
struct Win32_BaseBoard {
|
||||||
|
Manufacturer: Option<String>,
|
||||||
|
Product: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// [新增] 整机 WMI 结构 (品牌机看这个)
|
||||||
|
#[derive(serde::Deserialize, Debug)]
|
||||||
|
struct Win32_ComputerSystem {
|
||||||
|
Manufacturer: Option<String>,
|
||||||
|
Model: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 辅助函数:格式化 WMI 时间 ---
|
||||||
|
fn format_wmi_time(wmi_str: &str) -> String {
|
||||||
|
if wmi_str.len() < 25 {
|
||||||
|
return wmi_str.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let year = wmi_str[0..4].parse::<i32>().unwrap_or(1970);
|
||||||
|
let month = wmi_str[4..6].parse::<u32>().unwrap_or(1);
|
||||||
|
let day = wmi_str[6..8].parse::<u32>().unwrap_or(1);
|
||||||
|
let hour = wmi_str[8..10].parse::<u32>().unwrap_or(0);
|
||||||
|
let min = wmi_str[10..12].parse::<u32>().unwrap_or(0);
|
||||||
|
let sec = wmi_str[12..14].parse::<u32>().unwrap_or(0);
|
||||||
|
|
||||||
|
let sign = &wmi_str[21..22];
|
||||||
|
let offset_val = wmi_str[22..25].parse::<i32>().unwrap_or(0);
|
||||||
|
let offset_mins = if sign == "-" { -offset_val } else { offset_val };
|
||||||
|
|
||||||
|
let offset = FixedOffset::east_opt(offset_mins * 60).unwrap_or(FixedOffset::east_opt(0).unwrap());
|
||||||
|
|
||||||
|
let naive_date = NaiveDate::from_ymd_opt(year, month, day).unwrap_or(NaiveDate::from_ymd_opt(1970, 1, 1).unwrap());
|
||||||
|
let naive_dt = naive_date.and_hms_opt(hour, min, sec).unwrap_or_default();
|
||||||
|
|
||||||
|
match offset.from_local_datetime(&naive_dt).single() {
|
||||||
|
Some(dt) => {
|
||||||
|
dt.with_timezone(&Local).format("%Y-%m-%d %H:%M:%S").to_string()
|
||||||
|
},
|
||||||
|
None => format!("{}-{:02}-{:02} {:02}:{:02}:{:02}", year, month, day, hour, min, sec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 核心逻辑 ---
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn run_diagnosis() -> Result<SystemHealthReport, String> {
|
||||||
|
let report = tokio::task::spawn_blocking(|| {
|
||||||
|
let mut sys = System::new_all();
|
||||||
|
sys.refresh_all();
|
||||||
|
|
||||||
|
let wmi_con = WMIConnection::new(COMLibrary::new().unwrap()).ok();
|
||||||
|
|
||||||
|
// 1. 硬件基础信息
|
||||||
|
let mut bios_ver = "Unknown".to_string();
|
||||||
|
let mut mobo_vendor = "Unknown".to_string();
|
||||||
|
let mut mobo_product = "Unknown".to_string();
|
||||||
|
let mut sys_vendor = "Unknown".to_string();
|
||||||
|
let mut sys_product = "Unknown".to_string();
|
||||||
|
|
||||||
|
if let Some(con) = &wmi_con {
|
||||||
|
// 获取 BIOS 版本
|
||||||
|
if let Ok(results) = con.raw_query::<Win32_BIOS>("SELECT SMBIOSBIOSVersion FROM Win32_BIOS") {
|
||||||
|
if let Some(bios) = results.first() {
|
||||||
|
bios_ver = bios.SMBIOSBIOSVersion.clone().unwrap_or("Unknown".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 获取主板信息 (BaseBoard)
|
||||||
|
if let Ok(results) = con.raw_query::<Win32_BaseBoard>("SELECT Manufacturer, Product FROM Win32_BaseBoard") {
|
||||||
|
if let Some(board) = results.first() {
|
||||||
|
mobo_vendor = board.Manufacturer.clone().unwrap_or("Unknown".to_string());
|
||||||
|
mobo_product = board.Product.clone().unwrap_or("Unknown".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// [新增] 获取整机信息 (ComputerSystem)
|
||||||
|
if let Ok(results) = con.raw_query::<Win32_ComputerSystem>("SELECT Manufacturer, Model FROM Win32_ComputerSystem") {
|
||||||
|
if let Some(cs) = results.first() {
|
||||||
|
sys_vendor = cs.Manufacturer.clone().unwrap_or("Unknown".to_string());
|
||||||
|
sys_product = cs.Model.clone().unwrap_or("Unknown".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- C 盘空间检查 ---
|
||||||
|
let mut c_total = 0u64;
|
||||||
|
let mut c_used = 0u64;
|
||||||
|
let disks = Disks::new_with_refreshed_list();
|
||||||
|
for disk in &disks {
|
||||||
|
if disk.mount_point().to_string_lossy().starts_with("C:") {
|
||||||
|
c_total = disk.total_space() / 1024 / 1024 / 1024;
|
||||||
|
let free = disk.available_space() / 1024 / 1024 / 1024;
|
||||||
|
c_used = c_total - free;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cpu_brand = if let Some(cpu) = sys.cpus().first() {
|
||||||
|
cpu.brand().trim().to_string()
|
||||||
|
} else {
|
||||||
|
"Unknown CPU".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let hardware = HardwareSummary {
|
||||||
|
cpu_name: cpu_brand,
|
||||||
|
mobo_vendor,
|
||||||
|
mobo_product,
|
||||||
|
// [新增] 填入整机数据
|
||||||
|
sys_vendor,
|
||||||
|
sys_product,
|
||||||
|
memory_total_gb: sys.total_memory() / 1024 / 1024 / 1024,
|
||||||
|
memory_used_gb: sys.used_memory() / 1024 / 1024 / 1024,
|
||||||
|
os_version: System::long_os_version().unwrap_or("Unknown".to_string()),
|
||||||
|
bios_version: bios_ver,
|
||||||
|
c_drive_total_gb: c_total,
|
||||||
|
c_drive_used_gb: c_used,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. 存储设备健康度
|
||||||
|
let mut storage = Vec::new();
|
||||||
|
if let Some(con) = &wmi_con {
|
||||||
|
let query = "SELECT Model, Status FROM Win32_DiskDrive";
|
||||||
|
if let Ok(results) = con.raw_query::<Win32_DiskDrive>(query) {
|
||||||
|
for disk in results {
|
||||||
|
let status = disk.Status.unwrap_or("Unknown".to_string());
|
||||||
|
let (explanation, is_danger) = match status.as_str() {
|
||||||
|
"OK" => ("健康".to_string(), false),
|
||||||
|
"Pred Fail" => ("预测即将损坏".to_string(), true),
|
||||||
|
_ => ("状态异常".to_string(), true),
|
||||||
|
};
|
||||||
|
storage.push(StorageDevice {
|
||||||
|
model: disk.Model.unwrap_or("Generic Disk".to_string()),
|
||||||
|
health_status: status,
|
||||||
|
human_explanation: explanation,
|
||||||
|
is_danger,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 关键日志
|
||||||
|
let mut events = Vec::new();
|
||||||
|
if let Some(con) = &wmi_con {
|
||||||
|
let query = "SELECT TimeGenerated, EventCode, SourceName FROM Win32_NTLogEvent WHERE Logfile = 'System' AND (EventCode = 41 OR EventCode = 18 OR EventCode = 19)";
|
||||||
|
if let Ok(mut results) = con.raw_query::<Win32_NTLogEvent>(query) {
|
||||||
|
results.truncate(5);
|
||||||
|
for event in results {
|
||||||
|
let hint = match event.EventCode {
|
||||||
|
41 => "系统意外断电 (电源/强关)",
|
||||||
|
18 | 19 => "WHEA 硬件致命错误 (CPU/超频)",
|
||||||
|
_ => "系统关键错误",
|
||||||
|
};
|
||||||
|
events.push(SystemEvent {
|
||||||
|
time_generated: format_wmi_time(&event.TimeGenerated),
|
||||||
|
event_id: event.EventCode,
|
||||||
|
source: event.SourceName,
|
||||||
|
analysis_hint: hint.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Minidump
|
||||||
|
let mut minidump = MinidumpInfo { found: false, count: 0, explanation: "无蓝屏记录".to_string() };
|
||||||
|
if let Ok(entries) = fs::read_dir("C:\\Windows\\Minidump") {
|
||||||
|
let count = entries.count();
|
||||||
|
if count > 0 {
|
||||||
|
minidump = MinidumpInfo {
|
||||||
|
found: true,
|
||||||
|
count,
|
||||||
|
explanation: format!("发现 {} 次蓝屏崩溃", count),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 驱动设备检查
|
||||||
|
let mut driver_issues = Vec::new();
|
||||||
|
if let Some(con) = &wmi_con {
|
||||||
|
let query = "SELECT Name, ConfigManagerErrorCode FROM Win32_PnPEntity WHERE ConfigManagerErrorCode <> 0";
|
||||||
|
if let Ok(results) = con.raw_query::<Win32_PnPEntity>(query) {
|
||||||
|
for dev in results {
|
||||||
|
let code = dev.ConfigManagerErrorCode.unwrap_or(0);
|
||||||
|
let desc = match code {
|
||||||
|
10 => "设备无法启动 (Code 10)。通常是驱动不兼容。",
|
||||||
|
28 => "驱动程序未安装 (Code 28)。",
|
||||||
|
43 => "硬件报告问题已被停止 (Code 43)。显卡常见,可能虚焊。",
|
||||||
|
_ => "设备状态异常。",
|
||||||
|
};
|
||||||
|
driver_issues.push(DriverIssue {
|
||||||
|
device_name: dev.Name.unwrap_or("未知设备".to_string()),
|
||||||
|
error_code: code,
|
||||||
|
description: desc.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 电池健康度
|
||||||
|
let mut battery_info = None;
|
||||||
|
if let Some(con) = &wmi_con {
|
||||||
|
if let Ok(results) = con.raw_query::<Win32_Battery>("SELECT DesignCapacity, FullChargeCapacity, BatteryStatus FROM Win32_Battery") {
|
||||||
|
if let Some(bat) = results.first() {
|
||||||
|
let design = bat.DesignCapacity.unwrap_or(0);
|
||||||
|
let full = bat.FullChargeCapacity.unwrap_or(0);
|
||||||
|
let status = bat.BatteryStatus.unwrap_or(0);
|
||||||
|
|
||||||
|
if design > 0 {
|
||||||
|
let health = ((full as f64 / design as f64) * 100.0) as u32;
|
||||||
|
let ac_plugged = status == 2 || status == 6 || status == 1;
|
||||||
|
|
||||||
|
let explain = if health < 60 {
|
||||||
|
"电池老化严重,建议更换,否则可能导致供电不稳。".to_string()
|
||||||
|
} else {
|
||||||
|
"电池状态良好。".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
battery_info = Some(BatteryInfo {
|
||||||
|
health_percentage: health,
|
||||||
|
is_ac_connected: ac_plugged,
|
||||||
|
explanation: explain,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemHealthReport {
|
||||||
|
hardware,
|
||||||
|
storage,
|
||||||
|
events,
|
||||||
|
minidumps: minidump,
|
||||||
|
drivers: driver_issues,
|
||||||
|
battery: battery_info
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
Ok(report)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.invoke_handler(tauri::generate_handler![run_diagnosis])
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
|
}
|
||||||
35
src-tauri/tauri.conf.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"productName": "system-doctor",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"identifier": "top.volan.system-doctor",
|
||||||
|
"build": {
|
||||||
|
"beforeDevCommand": "npm run dev",
|
||||||
|
"devUrl": "http://localhost:1420",
|
||||||
|
"beforeBuildCommand": "npm run build",
|
||||||
|
"frontendDist": "../dist"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"title": "system-doctor",
|
||||||
|
"width": 800,
|
||||||
|
"height": 600
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": "all",
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
571
src/App.vue
Normal file
@@ -0,0 +1,571 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<header class="header">
|
||||||
|
<h1>🏥 电脑健康体检中心 Pro</h1>
|
||||||
|
<p class="subtitle">静态排查仪表盘 - Powered by Rust & Tauri</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button @click="startScan" :disabled="loading" class="scan-btn" :class="{ 'scanning': loading }">
|
||||||
|
<span v-if="loading">🔄 正在深入排查系统底层...</span>
|
||||||
|
<span v-else>🔍 开始全面体检</span>
|
||||||
|
</button>
|
||||||
|
<div v-if="errorMsg" class="error-box">
|
||||||
|
⚠️ {{ errorMsg }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 结果显示区域 -->
|
||||||
|
<div v-if="report" class="dashboard fade-in">
|
||||||
|
|
||||||
|
<!-- 1. 硬件概览 (带进度条可视化) -->
|
||||||
|
<div class="card summary">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>🖥️ 硬件概览与资源占用</h2>
|
||||||
|
</div>
|
||||||
|
<div class="grid-container-summary">
|
||||||
|
<!-- 左侧基础信息 -->
|
||||||
|
<div class="basic-info">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">CPU 型号</span>
|
||||||
|
<span class="value text-ellipsis" :title="report.hardware.cpu_name">{{ report.hardware.cpu_name }}</span>
|
||||||
|
</div>
|
||||||
|
<!-- 新增:整机信息 (品牌机看这个) -->
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">整机型号 (System)</span>
|
||||||
|
<span class="value text-ellipsis" :title="report.hardware.sys_vendor + ' ' + report.hardware.sys_product">
|
||||||
|
{{ report.hardware.sys_vendor }} {{ report.hardware.sys_product }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- 原有:主板信息 (DIY看这个) -->
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">主板型号 (BaseBoard)</span>
|
||||||
|
<span class="value text-ellipsis" :title="report.hardware.mobo_vendor + ' ' + report.hardware.mobo_product">
|
||||||
|
{{ report.hardware.mobo_vendor }} {{ report.hardware.mobo_product }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">BIOS 版本</span>
|
||||||
|
<span class="value">{{ report.hardware.bios_version }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">操作系统</span>
|
||||||
|
<span class="value">{{ report.hardware.os_version }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧可视化条 -->
|
||||||
|
<div class="usage-bars">
|
||||||
|
<!-- C盘空间可视化 -->
|
||||||
|
<div class="usage-item">
|
||||||
|
<div class="progress-label">
|
||||||
|
<span>C盘空间 (已用 {{ report.hardware.c_drive_used_gb }}GB / 总计 {{ report.hardware.c_drive_total_gb }}GB)</span>
|
||||||
|
<strong>{{ calculatePercent(report.hardware.c_drive_used_gb, report.hardware.c_drive_total_gb) }}%</strong>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar-large">
|
||||||
|
<div class="fill"
|
||||||
|
:style="getProgressStyle(report.hardware.c_drive_used_gb, report.hardware.c_drive_total_gb)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 内存占用可视化 -->
|
||||||
|
<div class="usage-item">
|
||||||
|
<div class="progress-label">
|
||||||
|
<span>内存占用 (已用 {{ report.hardware.memory_used_gb }}GB / 总计 {{ report.hardware.memory_total_gb }}GB)</span>
|
||||||
|
<strong>{{ calculatePercent(report.hardware.memory_used_gb, report.hardware.memory_total_gb) }}%</strong>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar-large">
|
||||||
|
<div class="fill"
|
||||||
|
:style="getProgressStyle(report.hardware.memory_used_gb, report.hardware.memory_total_gb)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. 驱动与设备管理器状态 -->
|
||||||
|
<div class="card" :class="{ danger: report.drivers.length > 0, success: report.drivers.length === 0 }">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>🔌 驱动与设备状态</h2>
|
||||||
|
<span class="badge" :class="report.drivers.length > 0 ? 'badge-red' : 'badge-green'">
|
||||||
|
{{ report.drivers.length > 0 ? '发现异常' : '正常' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="report.drivers.length === 0" class="good-news">
|
||||||
|
✅ 设备管理器中未发现带有黄色感叹号或错误的设备。
|
||||||
|
</div>
|
||||||
|
<div v-else class="list-container">
|
||||||
|
<div v-for="(drv, idx) in report.drivers" :key="idx" class="list-item error-item">
|
||||||
|
<div class="item-header">
|
||||||
|
<strong>{{ drv.device_name }}</strong>
|
||||||
|
<span class="code-tag">Code {{ drv.error_code }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="description">{{ drv.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 3. 电池健康度 (仅笔记本显示) -->
|
||||||
|
<div v-if="report.battery" class="card" :class="{ danger: report.battery.health_percentage < 60 }">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>🔋 电池健康度 (寿命)</h2>
|
||||||
|
<span class="badge" :class="report.battery.is_ac_connected ? 'badge-blue' : 'badge-gray'">
|
||||||
|
{{ report.battery.is_ac_connected ? '⚡ 已连接电源' : '🔋 使用电池中' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="content-box">
|
||||||
|
<div class="progress-wrapper">
|
||||||
|
<div class="progress-label">
|
||||||
|
<span>当前健康度 (相对于设计容量)</span>
|
||||||
|
<strong>{{ report.battery.health_percentage }}%</strong>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="fill"
|
||||||
|
:style="{ width: report.battery.health_percentage + '%', background: getBatteryColor(report.battery.health_percentage) }">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="explanation">{{ report.battery.explanation }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 4. 硬盘健康度 -->
|
||||||
|
<div class="card" :class="{ danger: hasStorageDanger }">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>💾 硬盘健康度 (S.M.A.R.T)</h2>
|
||||||
|
</div>
|
||||||
|
<div class="list-container">
|
||||||
|
<div v-for="(disk, index) in report.storage" :key="index" class="list-item">
|
||||||
|
<div class="item-header">
|
||||||
|
<strong>{{ disk.model }}</strong>
|
||||||
|
<span class="status-text" :class="disk.is_danger ? 'text-red' : 'text-green'">
|
||||||
|
{{ disk.health_status }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="description">{{ disk.human_explanation }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 5. 蓝屏分析 -->
|
||||||
|
<div class="card" :class="{ danger: report.minidumps.found, success: !report.minidumps.found }">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>☠️ 蓝屏死机记录 (BSOD)</h2>
|
||||||
|
</div>
|
||||||
|
<div class="content-box">
|
||||||
|
<p class="main-text">{{ report.minidumps.explanation }}</p>
|
||||||
|
<p v-if="report.minidumps.found" class="tip">
|
||||||
|
💡 建议使用 BlueScreenView 或 WinDbg 工具打开 C:\Windows\Minidump 文件夹下的 .dmp 文件进行深入分析。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 6. 关键系统日志 -->
|
||||||
|
<div class="card" :class="{ danger: report.events.length > 0 }">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>⚡ 关键供电与硬件日志 (Event Log)</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="report.events.length === 0" class="good-news">
|
||||||
|
✅ 近期日志中未发现 Kernel-Power(41) 或 WHEA(18/19) 等致命错误。
|
||||||
|
</div>
|
||||||
|
<div v-else class="list-container">
|
||||||
|
<div v-for="(evt, idx) in report.events" :key="idx" class="list-item warning-item">
|
||||||
|
<div class="item-header">
|
||||||
|
<span class="event-id">ID: {{ evt.event_id }}</span>
|
||||||
|
<span class="event-source">{{ evt.source }}</span>
|
||||||
|
<span class="event-time">{{ formatTime(evt.time_generated) }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="description highlight">{{ evt.analysis_hint }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
|
||||||
|
const report = ref(null);
|
||||||
|
const loading = ref(false);
|
||||||
|
const errorMsg = ref('');
|
||||||
|
|
||||||
|
const hasStorageDanger = computed(() => {
|
||||||
|
return report.value?.storage.some(d => d.is_danger);
|
||||||
|
});
|
||||||
|
|
||||||
|
function calculatePercent(used, total) {
|
||||||
|
if (total === 0) return 0;
|
||||||
|
return Math.round((used / total) * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProgressStyle(used, total) {
|
||||||
|
const percent = calculatePercent(used, total);
|
||||||
|
let color = '#2ed573';
|
||||||
|
|
||||||
|
if (percent >= 90) {
|
||||||
|
color = '#ff4757';
|
||||||
|
} else if (percent >= 75) {
|
||||||
|
color = '#ffa502';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
width: `${percent}%`,
|
||||||
|
background: color
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBatteryColor(percentage) {
|
||||||
|
if (percentage < 50) return '#ff4757';
|
||||||
|
if (percentage < 80) return '#ffa502';
|
||||||
|
return '#2ed573';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(wmiTime) {
|
||||||
|
// 如果是空值或者格式不对,直接返回
|
||||||
|
if (!wmiTime) return "Unknown Time";
|
||||||
|
// 简单的容错处理,如果是标准 ISO 时间或自定义格式
|
||||||
|
return wmiTime.replace('T', ' ').substring(0, 19);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startScan() {
|
||||||
|
loading.value = true;
|
||||||
|
errorMsg.value = '';
|
||||||
|
report.value = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await invoke('run_diagnosis');
|
||||||
|
console.log("诊断结果:", result);
|
||||||
|
report.value = result;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
errorMsg.value = "扫描失败: " + e + " (请确保以管理员身份运行此程序)";
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- 添加全局样式重置 body -->
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow-x: hidden; /* 防止水平滚动条 */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 基础布局 */
|
||||||
|
.container {
|
||||||
|
max-width: 960px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 30px 20px;
|
||||||
|
/* 关键修改:border-box 让 padding 包含在 height 内 */
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
color: #2c3e50;
|
||||||
|
background-color: #f0f2f5;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #2c3e50;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 操作区域 */
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-btn {
|
||||||
|
background: linear-gradient(135deg, #42b983 0%, #2ecc71 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 14px 40px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
border-radius: 50px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 4px 15px rgba(46, 204, 113, 0.25);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-btn:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(46, 204, 113, 0.35);
|
||||||
|
filter: brightness(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-btn:active:not(:disabled) {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-btn:disabled {
|
||||||
|
background: #bdc3c7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
box-shadow: none;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-box {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding: 12px 20px;
|
||||||
|
background-color: #ffeaea;
|
||||||
|
color: #c0392b;
|
||||||
|
border: 1px solid #ffcccc;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 仪表盘网格 */
|
||||||
|
.dashboard {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||||
|
gap: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
animation: fadeIn 0.4s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(15px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片通用样式 */
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0,0,0,0.04);
|
||||||
|
border-top: 4px solid #42b983;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
border-left: 1px solid #eee; border-right: 1px solid #eee; border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 25px rgba(0,0,0,0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.danger { border-top-color: #ff4757; background: #fffbfb; }
|
||||||
|
.card.success { border-top-color: #2ed573; }
|
||||||
|
|
||||||
|
.card.summary {
|
||||||
|
border-top-color: #3742fa;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #f1f2f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 硬件概览布局 */
|
||||||
|
.grid-container-summary {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1.5fr;
|
||||||
|
gap: 30px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.grid-container-summary {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.basic-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #95a5a6;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
.text-ellipsis {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 大型进度条样式 */
|
||||||
|
.usage-bars {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 25px;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-label {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #636e72;
|
||||||
|
}
|
||||||
|
.progress-label strong {
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-large {
|
||||||
|
width: 100%;
|
||||||
|
height: 20px;
|
||||||
|
background: #e6e8ec;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill {
|
||||||
|
height: 100%;
|
||||||
|
transition: width 0.6s cubic-bezier(0.25, 0.8, 0.25, 1), background-color 0.6s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 列表样式 */
|
||||||
|
.list-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
background: #fcfdfe;
|
||||||
|
padding: 12px 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #ececec;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item.error-item { background: #fff5f5; border-color: #ffcccc; }
|
||||||
|
.list-item.warning-item { background: #fffcf0; border-color: #ffeaa7; }
|
||||||
|
|
||||||
|
.item-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin: 0;
|
||||||
|
color: #636e72;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description.highlight { color: #d35400; font-weight: 500; }
|
||||||
|
|
||||||
|
/* 状态文本与徽章 */
|
||||||
|
.status-text { font-weight: 600; font-size: 0.9rem; }
|
||||||
|
.text-green { color: #27ae60; }
|
||||||
|
.text-red { color: #c0392b; }
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
.badge-green { background-color: #2ed573; }
|
||||||
|
.badge-red { background-color: #ff4757; }
|
||||||
|
.badge-blue { background-color: #3742fa; }
|
||||||
|
.badge-gray { background-color: #95a5a6; }
|
||||||
|
|
||||||
|
.code-tag {
|
||||||
|
background: #ff4757; color: white; padding: 2px 6px; border-radius: 4px; font-size: 0.8rem; font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.good-news {
|
||||||
|
color: #27ae60; font-weight: 600; background: #eafaf1; padding: 15px; border-radius: 8px; border: 1px solid #d4efdf; text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
margin-top: 15px; font-size: 0.85rem; color: #d35400; background: #fff3e0; padding: 12px; border-radius: 6px; border: 1px solid #ffe0b2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 电池进度条 */
|
||||||
|
.progress-wrapper { margin-bottom: 15px; }
|
||||||
|
.progress-bar {
|
||||||
|
width: 100%; height: 10px; background: #e6e8ec; border-radius: 5px; overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 日志元数据 */
|
||||||
|
.event-id, .event-source, .event-time {
|
||||||
|
font-size: 0.75rem; color: #7f8c8d;
|
||||||
|
}
|
||||||
|
.event-id {
|
||||||
|
font-weight: bold; color: #2c3e50; background: #dfe6e9; padding: 1px 5px; border-radius: 4px; margin-right: 8px;
|
||||||
|
}
|
||||||
|
.event-time { margin-left: auto; }
|
||||||
|
|
||||||
|
/* 内容盒模型 */
|
||||||
|
.content-box { padding: 5px 0; }
|
||||||
|
.main-text { font-weight: 500; color: #2c3e50; margin-bottom: 10px;}
|
||||||
|
</style>
|
||||||
1
src/assets/vue.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 496 B |
4
src/main.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { createApp } from "vue";
|
||||||
|
import App from "./App.vue";
|
||||||
|
|
||||||
|
createApp(App).mount("#app");
|
||||||
31
vite.config.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import vue from "@vitejs/plugin-vue";
|
||||||
|
|
||||||
|
const host = process.env.TAURI_DEV_HOST;
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig(async () => ({
|
||||||
|
plugins: [vue()],
|
||||||
|
|
||||||
|
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||||
|
//
|
||||||
|
// 1. prevent Vite from obscuring rust errors
|
||||||
|
clearScreen: false,
|
||||||
|
// 2. tauri expects a fixed port, fail if that port is not available
|
||||||
|
server: {
|
||||||
|
port: 1420,
|
||||||
|
strictPort: true,
|
||||||
|
host: host || false,
|
||||||
|
hmr: host
|
||||||
|
? {
|
||||||
|
protocol: "ws",
|
||||||
|
host,
|
||||||
|
port: 1421,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
watch: {
|
||||||
|
// 3. tell Vite to ignore watching `src-tauri`
|
||||||
|
ignored: ["**/src-tauri/**"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||