Compare commits
16 Commits
4fc501f941
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
daa0178185 | ||
|
|
4be0fdcb74 | ||
|
|
93164a86d8 | ||
|
|
0ff1014e62 | ||
|
|
26d04daceb | ||
|
|
79a796f037 | ||
|
|
9e6ad62a91 | ||
|
|
06e84856dd | ||
|
|
ea3bdd80e0 | ||
|
|
4fe4150069 | ||
|
|
1ad180a413 | ||
|
|
936c58dcc8 | ||
|
|
c01c1a33d4 | ||
|
|
b553fb495c | ||
|
|
bae7716a1f | ||
|
|
19b18d091a |
BIN
app-icon.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
@@ -2,9 +2,8 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<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" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Tauri + Vue 3 App</title>
|
<title>System Doctor</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 2.5 KiB |
@@ -1 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
136
src-tauri/Cargo.lock
generated
@@ -622,6 +622,27 @@ dependencies = [
|
|||||||
"syn 2.0.111",
|
"syn 2.0.111",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "csv"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938"
|
||||||
|
dependencies = [
|
||||||
|
"csv-core",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "csv-core"
|
||||||
|
version = "0.1.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ctor"
|
name = "ctor"
|
||||||
version = "0.2.9"
|
version = "0.2.9"
|
||||||
@@ -667,6 +688,15 @@ dependencies = [
|
|||||||
"syn 2.0.111",
|
"syn 2.0.111",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "debugid"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d"
|
||||||
|
dependencies = [
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.5.5"
|
version = "0.5.5"
|
||||||
@@ -833,6 +863,15 @@ version = "1.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encoding_rs"
|
||||||
|
version = "0.8.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "endi"
|
name = "endi"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -1976,6 +2015,15 @@ version = "2.7.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memmap2"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "43a5a03cefb0d953ec0be133036f14e109412fa594edc2f77227249db66cc3ed"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memoffset"
|
name = "memoffset"
|
||||||
version = "0.9.1"
|
version = "0.9.1"
|
||||||
@@ -1991,6 +2039,40 @@ version = "0.3.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minidump"
|
||||||
|
version = "0.19.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c671544a05d0e8daa3018c8fb6687c11935c4ae8f122de8f2386c2896b4e9b8"
|
||||||
|
dependencies = [
|
||||||
|
"debugid",
|
||||||
|
"encoding_rs",
|
||||||
|
"memmap2",
|
||||||
|
"minidump-common",
|
||||||
|
"num-traits",
|
||||||
|
"range-map",
|
||||||
|
"scroll",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"time",
|
||||||
|
"tracing",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minidump-common"
|
||||||
|
version = "0.19.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3dbc11dfb55b3b7b5684fb16d98e0fc9d1e93a64d6b00bf383eabfc4541aaac2"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.10.0",
|
||||||
|
"debugid",
|
||||||
|
"num-derive",
|
||||||
|
"num-traits",
|
||||||
|
"range-map",
|
||||||
|
"scroll",
|
||||||
|
"smart-default",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
@@ -2103,6 +2185,17 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-derive"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.111",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
@@ -2904,6 +2997,15 @@ dependencies = [
|
|||||||
"rand_core 0.5.1",
|
"rand_core 0.5.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "range-map"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "12a5a2d6c7039059af621472a4389be1215a816df61aa4d531cfe85264aee95f"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "raw-window-handle"
|
name = "raw-window-handle"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
@@ -3134,6 +3236,26 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scroll"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04c565b551bafbef4157586fa379538366e4385d42082f255bfd96e4fe8519da"
|
||||||
|
dependencies = [
|
||||||
|
"scroll_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scroll_derive"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.111",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "selectors"
|
name = "selectors"
|
||||||
version = "0.24.0"
|
version = "0.24.0"
|
||||||
@@ -3388,6 +3510,17 @@ version = "1.15.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smart-default"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.111",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
@@ -3575,6 +3708,8 @@ name = "system-doctor"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"csv",
|
||||||
|
"minidump",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
@@ -4189,6 +4324,7 @@ version = "0.1.41"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"log",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tracing-attributes",
|
"tracing-attributes",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
|
|||||||
@@ -1,19 +1,12 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "system-doctor"
|
name = "system-doctor"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "A Tauri App"
|
description = "System diagnosis"
|
||||||
authors = ["you"]
|
authors = ["Julian"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# 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]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2", features = [] }
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
@@ -26,6 +19,8 @@ sysinfo = "0.30"
|
|||||||
wmi = "0.13"
|
wmi = "0.13"
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
minidump = "0.19"
|
||||||
|
csv = "1.3"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# this feature is used for production builds or when `devPath` points to the filesystem
|
# this feature is used for production builds or when `devPath` points to the filesystem
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 974 B After Width: | Height: | Size: 1.7 KiB |
BIN
src-tauri/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 903 B After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
</adaptive-icon>
|
||||||
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 74 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#fff</color>
|
||||||
|
</resources>
|
||||||
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 80 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@1x.png
Normal file
|
After Width: | Height: | Size: 859 B |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x-1.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@3x.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@1x.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x-1.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@3x.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@1x.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x-1.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@3x.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
src-tauri/icons/ios/AppIcon-512@2x.png
Normal file
|
After Width: | Height: | Size: 215 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@2x.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@3x.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@1x.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@2x.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
@@ -1,14 +0,0 @@
|
|||||||
// 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");
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
// src-tauri/src/main.rs
|
|
||||||
|
|
||||||
// 全局允许非标准命名风格
|
// 全局允许非标准命名风格
|
||||||
#![allow(non_camel_case_types, non_snake_case)]
|
#![allow(non_camel_case_types, non_snake_case)]
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
@@ -8,13 +6,17 @@ use serde::Serialize;
|
|||||||
use sysinfo::{System, Disks};
|
use sysinfo::{System, Disks};
|
||||||
use wmi::{COMLibrary, WMIConnection};
|
use wmi::{COMLibrary, WMIConnection};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
// 引入 tauri::Emitter 用于发送事件 (Tauri v2)
|
use std::path::Path;
|
||||||
|
use std::io::Read;
|
||||||
|
use std::process::Command;
|
||||||
use tauri::Emitter;
|
use tauri::Emitter;
|
||||||
use chrono::{Duration, FixedOffset, Local, NaiveDate, TimeZone};
|
use chrono::{Duration, FixedOffset, Local, NaiveDate, TimeZone, DateTime};
|
||||||
|
use minidump::{Minidump, MinidumpException, MinidumpSystemInfo};
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
// --- 1. 数据结构 (保持不变,用于序列化部分数据) ---
|
// --- 1. 数据结构 (保持不变) ---
|
||||||
|
|
||||||
#[derive(Serialize, Clone)] // 增加 Clone trait 方便使用
|
#[derive(Serialize, Clone)]
|
||||||
struct HardwareSummary {
|
struct HardwareSummary {
|
||||||
cpu_name: String,
|
cpu_name: String,
|
||||||
sys_vendor: String,
|
sys_vendor: String,
|
||||||
@@ -67,51 +69,39 @@ struct BatteryInfo {
|
|||||||
explanation: String,
|
explanation: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Clone)]
|
||||||
|
struct BsodFileItem {
|
||||||
|
filename: String,
|
||||||
|
path: String,
|
||||||
|
size_kb: u64,
|
||||||
|
created_time: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Clone)]
|
||||||
|
struct BsodAnalysisReport {
|
||||||
|
crash_reason: String,
|
||||||
|
crash_address: String,
|
||||||
|
bug_check_code: String,
|
||||||
|
crashing_thread: Option<String>,
|
||||||
|
human_analysis: String,
|
||||||
|
recommendation: String,
|
||||||
|
}
|
||||||
|
|
||||||
// --- WMI 反序列化结构 ---
|
// --- WMI 反序列化结构 ---
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Debug)]
|
#[derive(serde::Deserialize, Debug)]
|
||||||
struct Win32_DiskDrive {
|
struct Win32_DiskDrive { Model: Option<String>, Status: Option<String> }
|
||||||
Model: Option<String>,
|
|
||||||
Status: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Debug)]
|
#[derive(serde::Deserialize, Debug)]
|
||||||
struct Win32_NTLogEvent {
|
struct Win32_NTLogEvent { TimeGenerated: String, EventCode: u32, SourceName: String, Message: Option<String> }
|
||||||
TimeGenerated: String,
|
|
||||||
EventCode: u32,
|
|
||||||
SourceName: String,
|
|
||||||
Message: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Debug)]
|
#[derive(serde::Deserialize, Debug)]
|
||||||
struct Win32_PnPEntity {
|
struct Win32_PnPEntity { Name: Option<String>, ConfigManagerErrorCode: Option<u32> }
|
||||||
Name: Option<String>,
|
|
||||||
ConfigManagerErrorCode: Option<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Debug)]
|
#[derive(serde::Deserialize, Debug)]
|
||||||
struct Win32_Battery {
|
struct Win32_Battery { DesignCapacity: Option<u32>, FullChargeCapacity: Option<u32>, BatteryStatus: Option<u16> }
|
||||||
DesignCapacity: Option<u32>,
|
|
||||||
FullChargeCapacity: Option<u32>,
|
|
||||||
BatteryStatus: Option<u16>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Debug)]
|
#[derive(serde::Deserialize, Debug)]
|
||||||
struct Win32_BIOS {
|
struct Win32_BIOS { SMBIOSBIOSVersion: Option<String> }
|
||||||
SMBIOSBIOSVersion: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Debug)]
|
#[derive(serde::Deserialize, Debug)]
|
||||||
struct Win32_BaseBoard {
|
struct Win32_BaseBoard { Manufacturer: Option<String>, Product: Option<String> }
|
||||||
Manufacturer: Option<String>,
|
|
||||||
Product: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Debug)]
|
#[derive(serde::Deserialize, Debug)]
|
||||||
struct Win32_ComputerSystem {
|
struct Win32_ComputerSystem { Manufacturer: Option<String>, Model: Option<String> }
|
||||||
Manufacturer: Option<String>,
|
|
||||||
Model: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 辅助函数 ---
|
// --- 辅助函数 ---
|
||||||
fn get_wmi_query_time(days_ago: i64) -> String {
|
fn get_wmi_query_time(days_ago: i64) -> String {
|
||||||
@@ -120,9 +110,7 @@ fn get_wmi_query_time(days_ago: i64) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn format_wmi_time(wmi_str: &str) -> String {
|
fn format_wmi_time(wmi_str: &str) -> String {
|
||||||
if wmi_str.len() < 25 {
|
if wmi_str.len() < 25 { return wmi_str.to_string(); }
|
||||||
return wmi_str.to_string();
|
|
||||||
}
|
|
||||||
let year = wmi_str[0..4].parse::<i32>().unwrap_or(1970);
|
let year = wmi_str[0..4].parse::<i32>().unwrap_or(1970);
|
||||||
let month = wmi_str[4..6].parse::<u32>().unwrap_or(1);
|
let month = wmi_str[4..6].parse::<u32>().unwrap_or(1);
|
||||||
let day = wmi_str[6..8].parse::<u32>().unwrap_or(1);
|
let day = wmi_str[6..8].parse::<u32>().unwrap_or(1);
|
||||||
@@ -141,27 +129,371 @@ fn format_wmi_time(wmi_str: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 核心逻辑:分步流式传输 ---
|
// [扩展] 翻译 BugCheck Code (u32) - 增加了更多常见代码
|
||||||
|
fn translate_bugcheck_u32(code: u32) -> (String, String) {
|
||||||
|
match code {
|
||||||
|
// 驱动与内存相关
|
||||||
|
0x0000000A => (
|
||||||
|
"IRQL_NOT_LESS_OR_EQUAL (0x0A)".to_string(),
|
||||||
|
"驱动程序使用了不正确的内存地址。通常由有缺陷的驱动程序(如杀毒软件、虚拟光驱驱动)或硬件兼容性问题引起。建议更新所有驱动程序。"
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
0x000000D1 => (
|
||||||
|
"DRIVER_IRQL_NOT_LESS_OR_EQUAL (0xD1)".to_string(),
|
||||||
|
"驱动程序尝试访问未分页内存。这是最常见的蓝屏之一,通常是驱动程序冲突或损坏。请检查最近安装的硬件驱动(显卡、网卡等)。"
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
0x0000007E | 0x1000007E => (
|
||||||
|
"SYSTEM_THREAD_EXCEPTION_NOT_HANDLED (0x7E)".to_string(),
|
||||||
|
"系统线程抛出了未捕获的异常。可能是显卡驱动不兼容,或者BIOS设置问题。建议重装显卡驱动,或更新 BIOS。"
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
0x0000001E => (
|
||||||
|
"KMODE_EXCEPTION_NOT_HANDLED (0x1E)".to_string(),
|
||||||
|
"内核模式程序生成了处理器无法捕获的异常。通常是硬件兼容性问题或驱动程序错误。建议检查是否有新安装的硬件。"
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
0x00000050 => (
|
||||||
|
"PAGE_FAULT_IN_NONPAGED_AREA (0x50)".to_string(),
|
||||||
|
"试图访问无效的内存地址。极大概率是内存条故障(接触不良或损坏),或者是防病毒软件/驱动程序冲突。建议运行内存诊断。"
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
0x0000001A => (
|
||||||
|
"MEMORY_MANAGEMENT (0x1A)".to_string(),
|
||||||
|
"严重的内存管理错误。高度怀疑内存条物理故障。建议立即运行 Windows 内存诊断工具,或者尝试拔插/更换内存条。"
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 硬件与系统核心相关
|
||||||
|
0x00000124 => (
|
||||||
|
"WHEA_UNCORRECTABLE_ERROR (0x124)".to_string(),
|
||||||
|
"硬件发生无法纠正的物理错误。这是纯硬件故障。通常是 CPU 电压不足(缩缸)、超频失败、过热,或者主板/PCIe设备故障。"
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
0x00000101 => (
|
||||||
|
"CLOCK_WATCHDOG_TIMEOUT (0x101)".to_string(),
|
||||||
|
"CPU 核心死锁。系统检测到某个 CPU 核心在规定时间内没有响应。通常是因为 CPU 超频不稳定、电压不足或过热。"
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
0x000000EF => (
|
||||||
|
"CRITICAL_PROCESS_DIED (0xEF)".to_string(),
|
||||||
|
"Windows 核心进程意外终止。通常是系统文件严重损坏或硬盘出现坏道导致无法读取系统文件。建议运行 'sfc /scannow'。"
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
0x00000133 => (
|
||||||
|
"DPC_WATCHDOG_VIOLATION (0x133)".to_string(),
|
||||||
|
"DPC 看门狗超时。系统被某个驱动程序(通常是 SSD 固件、SATA 驱动或无线网卡驱动)卡死太久。建议更新 SSD 固件和主板芯片组驱动。"
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
0x00000139 => (
|
||||||
|
"KERNEL_SECURITY_CHECK_FAILURE (0x139)".to_string(),
|
||||||
|
"内核检测到关键数据结构损坏。通常由内存错误、驱动程序 Bug 或病毒导致。建议检查内存和更新驱动。"
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 显卡与电源相关
|
||||||
|
0x00000116 => (
|
||||||
|
"VIDEO_TDR_FAILURE (0x116)".to_string(),
|
||||||
|
"显卡响应超时。显卡驱动崩溃且无法恢复。如果你在玩游戏,可能是显卡超频不稳定、电源功率不足或散热硅脂干了。"
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
0x00000119 => (
|
||||||
|
"VIDEO_SCHEDULER_INTERNAL_ERROR (0x119)".to_string(),
|
||||||
|
"显卡调度器内部错误。这表明显卡驱动发送了无效数据,或者是显卡显存(VRAM)出现故障。"
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
0x0000009F => (
|
||||||
|
"DRIVER_POWER_STATE_FAILURE (0x9F)".to_string(),
|
||||||
|
"驱动程序电源状态故障。通常发生在电脑休眠、睡眠或唤醒时。某个设备的驱动程序没有正确响应电源指令。建议检查电源管理设置。"
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 存储与启动相关
|
||||||
|
0x0000007B => (
|
||||||
|
"INACCESSIBLE_BOOT_DEVICE (0x7B)".to_string(),
|
||||||
|
"无法访问启动设备。通常是 BIOS 中的硬盘模式设置错误(如 AHCI/RAID 切换)或引导扇区损坏。也可能是硬盘物理连接松动。"
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
0x00000024 => (
|
||||||
|
"NTFS_FILE_SYSTEM (0x24)".to_string(),
|
||||||
|
"NTFS 文件系统损坏。这通常意味着硬盘出现坏道或文件系统逻辑错误。建议立即运行 'chkdsk /f /r' 修复磁盘。"
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
0x00000154 => (
|
||||||
|
"UNEXPECTED_STORE_EXCEPTION (0x154)".to_string(),
|
||||||
|
"系统存储组件检测到意外异常。通常指向系统盘(SSD/HDD)出现故障或连接线松动,导致系统无法读取关键数据。"
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 系统服务异常
|
||||||
|
0x0000003B => (
|
||||||
|
"SYSTEM_SERVICE_EXCEPTION (0x3B)".to_string(),
|
||||||
|
"系统服务执行异常。通常与图形驱动程序、系统文件损坏或过时的驱动有关。"
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
_ => (
|
||||||
|
format!("未知错误代码: 0x{:X}", code),
|
||||||
|
"此代码较为罕见,建议手动在搜索引擎中搜索此错误代码。通用排查步骤:1. 更新所有驱动;2. 运行内存诊断;3. 检查系统文件完整性。"
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn translate_bugcheck_str(code_str: &str) -> (String, String) {
|
||||||
|
let clean_code = code_str.trim().replace("0x", "");
|
||||||
|
if let Ok(code) = u32::from_str_radix(&clean_code, 16) {
|
||||||
|
return translate_bugcheck_u32(code);
|
||||||
|
}
|
||||||
|
(format!("错误代码: {}", code_str), "无法识别的错误代码,建议手动搜索。".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
// [公共逻辑] Rust 原生分析
|
||||||
|
fn analyze_dump_data_native<T>(dump: Minidump<T>) -> Result<BsodAnalysisReport, String>
|
||||||
|
where T: Deref<Target = [u8]>
|
||||||
|
{
|
||||||
|
let (exception_code, exception_address) = match dump.get_stream::<MinidumpException>() {
|
||||||
|
Ok(stream) => (
|
||||||
|
stream.raw.exception_record.exception_code,
|
||||||
|
stream.raw.exception_record.exception_address
|
||||||
|
),
|
||||||
|
Err(_) => (0, 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
let sys_info_str = match dump.get_stream::<MinidumpSystemInfo>() {
|
||||||
|
Ok(info) => format!("Windows Build {}", info.raw.build_number),
|
||||||
|
Err(_) => "Unknown OS".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if exception_code == 0 {
|
||||||
|
return Err("未找到异常记录".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let (reason_str, recommend) = translate_bugcheck_u32(exception_code);
|
||||||
|
|
||||||
|
Ok(BsodAnalysisReport {
|
||||||
|
crash_reason: reason_str,
|
||||||
|
crash_address: format!("0x{:X}", exception_address),
|
||||||
|
bug_check_code: format!("0x{:X} ({})", exception_code, sys_info_str),
|
||||||
|
crashing_thread: None,
|
||||||
|
human_analysis: "通过 Rust 原生库分析得出。".to_string(),
|
||||||
|
recommendation: recommend,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// [修复] BlueScreenView 外部调用分析 (使用有损 UTF-8 转换)
|
||||||
|
fn analyze_with_bluescreenview(dump_path: &Path) -> Result<BsodAnalysisReport, String> {
|
||||||
|
let bsv_exe = "BlueScreenView.exe";
|
||||||
|
|
||||||
|
let mut temp_csv_path = std::env::temp_dir();
|
||||||
|
temp_csv_path.push(format!("bsod_report_{}.csv", chrono::Utc::now().timestamp_millis()));
|
||||||
|
|
||||||
|
// BlueScreenView.exe /LoadFrom 3 /SingleDumpFile <DumpFilePath> /scomma <OutFile>
|
||||||
|
let status = Command::new(bsv_exe)
|
||||||
|
.arg("/LoadFrom")
|
||||||
|
.arg("3")
|
||||||
|
.arg("/SingleDumpFile")
|
||||||
|
.arg(dump_path.to_string_lossy().to_string())
|
||||||
|
.arg("/scomma")
|
||||||
|
.arg(temp_csv_path.to_string_lossy().to_string())
|
||||||
|
.status();
|
||||||
|
|
||||||
|
if status.is_err() {
|
||||||
|
return Err("未找到 BlueScreenView.exe,或没有以管理员运行。".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// [修改] 使用 fs::read 读取原始字节,然后有损转换为 String
|
||||||
|
// 这样即使文件是 UTF-16 或者包含非法 UTF-8 字符,也不会 panic
|
||||||
|
let content_bytes = fs::read(&temp_csv_path).map_err(|_| "BlueScreenView 分析未生成有效数据。".to_string())?;
|
||||||
|
let _ = fs::remove_file(temp_csv_path);
|
||||||
|
|
||||||
|
// 1. 简单判断:如果包含很多 0 字节,可能是 UTF-16,尝试手动过滤 0 (简单粗暴但对 CSV 有效)
|
||||||
|
// 2. 或者直接 lossy 转换,虽然 UTF-16 会乱码,但 CSV crate 有时能容忍乱码分隔符
|
||||||
|
// 最稳妥的方式其实是先尝试 UTF-16LE 转换逻辑(但我们刚才移除了 encoding_rs),
|
||||||
|
// 所以这里我们用一个简单的技巧:如果文件头两个字节是 FF FE,手动跳过 BOM
|
||||||
|
|
||||||
|
let content_string = if content_bytes.len() > 2 && content_bytes[0] == 0xFF && content_bytes[1] == 0xFE {
|
||||||
|
// 简易 UTF-16LE 转 UTF-8 (只针对 ASCII 字符有效,对中文可能会乱码,但在这个场景下够用了)
|
||||||
|
// BlueScreenView 的列名和关键 hex 代码都是 ASCII
|
||||||
|
let mut s = String::new();
|
||||||
|
for i in (2..content_bytes.len()).step_by(2) {
|
||||||
|
if i+1 < content_bytes.len() {
|
||||||
|
// 取低位字节,忽略高位 0x00
|
||||||
|
let c = content_bytes[i] as char;
|
||||||
|
s.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s
|
||||||
|
} else {
|
||||||
|
String::from_utf8_lossy(&content_bytes).to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut rdr = csv::ReaderBuilder::new()
|
||||||
|
.has_headers(false)
|
||||||
|
.from_reader(content_string.as_bytes());
|
||||||
|
|
||||||
|
for result in rdr.records() {
|
||||||
|
if let Ok(record) = result {
|
||||||
|
if record.len() > 15 {
|
||||||
|
// 跳过 Header 行
|
||||||
|
if &record[0] == "Dump File" { continue; }
|
||||||
|
|
||||||
|
let bug_check_string = &record[2];
|
||||||
|
let bug_check_code = &record[3];
|
||||||
|
let caused_by_driver = &record[8];
|
||||||
|
let crash_addr = if record.len() > 15 { &record[15] } else { "N/A" };
|
||||||
|
|
||||||
|
let (human, recommend) = translate_bugcheck_str(bug_check_code);
|
||||||
|
|
||||||
|
let final_reason = if !bug_check_string.is_empty() { bug_check_string.to_string() } else { human };
|
||||||
|
|
||||||
|
return Ok(BsodAnalysisReport {
|
||||||
|
crash_reason: final_reason,
|
||||||
|
crash_address: crash_addr.to_string(),
|
||||||
|
bug_check_code: bug_check_code.to_string(),
|
||||||
|
crashing_thread: Some(format!("Driver: {}", caused_by_driver)),
|
||||||
|
human_analysis: format!("BlueScreenView 分析结果:崩溃可能由驱动程序 [{}] 引起。", caused_by_driver),
|
||||||
|
recommendation: recommend,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err("BlueScreenView 输出为空或格式无法识别。".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:检查文件头签名并返回类型
|
||||||
|
enum DumpType {
|
||||||
|
Minidump,
|
||||||
|
KernelDump, // PAGEDU64 / PAGE
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_dump_signature(sig: &[u8]) -> DumpType {
|
||||||
|
if sig.len() < 4 { return DumpType::Unknown; }
|
||||||
|
if sig.starts_with(b"MDMP") { return DumpType::Minidump; }
|
||||||
|
if sig.starts_with(b"PAGE") { return DumpType::KernelDump; }
|
||||||
|
DumpType::Unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 命令:列出 Minidump 文件 ---
|
||||||
|
#[tauri::command]
|
||||||
|
async fn list_minidumps() -> Result<Vec<BsodFileItem>, String> {
|
||||||
|
let path = Path::new("C:\\Windows\\Minidump");
|
||||||
|
let mut files = Vec::new();
|
||||||
|
|
||||||
|
if path.exists() {
|
||||||
|
if let Ok(entries) = fs::read_dir(path) {
|
||||||
|
for entry in entries {
|
||||||
|
if let Ok(entry) = entry {
|
||||||
|
let metadata = entry.metadata().ok();
|
||||||
|
let created = metadata.as_ref()
|
||||||
|
.and_then(|m| m.modified().ok())
|
||||||
|
.map(|t| {
|
||||||
|
let dt: DateTime<Local> = t.into();
|
||||||
|
dt.format("%Y-%m-%d %H:%M:%S").to_string()
|
||||||
|
})
|
||||||
|
.unwrap_or("Unknown".to_string());
|
||||||
|
|
||||||
|
let size = metadata.as_ref().map(|m| m.len() / 1024).unwrap_or(0);
|
||||||
|
|
||||||
|
files.push(BsodFileItem {
|
||||||
|
filename: entry.file_name().to_string_lossy().to_string(),
|
||||||
|
path: entry.path().to_string_lossy().to_string(),
|
||||||
|
size_kb: size,
|
||||||
|
created_time: created,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
files.sort_by(|a, b| b.created_time.cmp(&a.created_time));
|
||||||
|
Ok(files)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 命令:分析指定路径的 Minidump ---
|
||||||
|
#[tauri::command]
|
||||||
|
async fn analyze_minidump(filepath: String) -> Result<BsodAnalysisReport, String> {
|
||||||
|
let path = Path::new(&filepath);
|
||||||
|
|
||||||
|
let mut file = fs::File::open(path).map_err(|e| format!("无法打开文件: {}", e))?;
|
||||||
|
let mut header = [0u8; 4];
|
||||||
|
|
||||||
|
let dump_type = if let Ok(_) = file.read_exact(&mut header) {
|
||||||
|
check_dump_signature(&header)
|
||||||
|
} else {
|
||||||
|
DumpType::Unknown
|
||||||
|
};
|
||||||
|
|
||||||
|
match dump_type {
|
||||||
|
DumpType::Minidump => {
|
||||||
|
if let Ok(dump) = Minidump::read_path(path) {
|
||||||
|
if let Ok(report) = analyze_dump_data_native(dump) {
|
||||||
|
return Ok(report);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
analyze_with_bluescreenview(path)
|
||||||
|
},
|
||||||
|
DumpType::KernelDump => {
|
||||||
|
analyze_with_bluescreenview(path)
|
||||||
|
},
|
||||||
|
DumpType::Unknown => Err("无效的文件签名。不是有效的 Windows Dump 文件。".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 命令:分析二进制内容的 Minidump
|
||||||
|
#[tauri::command]
|
||||||
|
async fn analyze_minidump_bytes(file_content: Vec<u8>) -> Result<BsodAnalysisReport, String> {
|
||||||
|
if file_content.is_empty() {
|
||||||
|
return Err("导入的文件内容为空".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let dump_type = check_dump_signature(&file_content);
|
||||||
|
|
||||||
|
match dump_type {
|
||||||
|
DumpType::Minidump => {
|
||||||
|
let native_result = Minidump::read(file_content.clone())
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
.and_then(|dump| analyze_dump_data_native(dump));
|
||||||
|
|
||||||
|
if let Ok(report) = native_result {
|
||||||
|
return Ok(report);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DumpType::KernelDump => {
|
||||||
|
// 直接回退
|
||||||
|
},
|
||||||
|
DumpType::Unknown => return Err("无效的文件签名。请确认这是 .dmp 文件。".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut temp_dump_path = std::env::temp_dir();
|
||||||
|
temp_dump_path.push(format!("temp_dump_{}.dmp", chrono::Utc::now().timestamp_millis()));
|
||||||
|
|
||||||
|
if let Err(_) = fs::write(&temp_dump_path, &file_content) {
|
||||||
|
return Err("无法写入临时文件进行分析".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let bsv_result = analyze_with_bluescreenview(&temp_dump_path);
|
||||||
|
|
||||||
|
let _ = fs::remove_file(temp_dump_path);
|
||||||
|
|
||||||
|
bsv_result
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 现有命令:run_diagnosis (保持不变) ---
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn run_diagnosis(window: tauri::Window) -> Result<(), String> {
|
async fn run_diagnosis(window: tauri::Window) -> Result<(), String> {
|
||||||
// 使用 spawn 在后台线程运行,避免阻塞 Tauri 主线程
|
|
||||||
// 注意:这里不等待 join,而是让它在后台跑,通过 window.emit 发送进度
|
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
// 1. 硬件概览 (最快)
|
// ... (保持原有逻辑不变,占位符) ...
|
||||||
{
|
{
|
||||||
let mut sys = System::new();
|
let mut sys = System::new();
|
||||||
sys.refresh_memory();
|
sys.refresh_memory();
|
||||||
sys.refresh_cpu();
|
sys.refresh_cpu();
|
||||||
|
|
||||||
let wmi_con = WMIConnection::new(COMLibrary::new().unwrap()).ok();
|
let wmi_con = WMIConnection::new(COMLibrary::new().unwrap()).ok();
|
||||||
|
|
||||||
let mut bios_ver = "Unknown".to_string();
|
let mut bios_ver = "Unknown".to_string();
|
||||||
let mut mobo_vendor = "Unknown".to_string();
|
let mut mobo_vendor = "Unknown".to_string();
|
||||||
let mut mobo_product = "Unknown".to_string();
|
let mut mobo_product = "Unknown".to_string();
|
||||||
let mut sys_vendor = "Unknown".to_string();
|
let mut sys_vendor = "Unknown".to_string();
|
||||||
let mut sys_product = "Unknown".to_string();
|
let mut sys_product = "Unknown".to_string();
|
||||||
|
|
||||||
if let Some(con) = &wmi_con {
|
if let Some(con) = &wmi_con {
|
||||||
if let Ok(results) = con.raw_query::<Win32_BIOS>("SELECT SMBIOSBIOSVersion FROM Win32_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_default(); }
|
if let Some(bios) = results.first() { bios_ver = bios.SMBIOSBIOSVersion.clone().unwrap_or_default(); }
|
||||||
@@ -179,8 +511,6 @@ async fn run_diagnosis(window: tauri::Window) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// C盘
|
|
||||||
let mut c_total = 0u64;
|
let mut c_total = 0u64;
|
||||||
let mut c_used = 0u64;
|
let mut c_used = 0u64;
|
||||||
let disks = Disks::new_with_refreshed_list();
|
let disks = Disks::new_with_refreshed_list();
|
||||||
@@ -192,32 +522,18 @@ async fn run_diagnosis(window: tauri::Window) -> Result<(), String> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let cpu_brand = if let Some(cpu) = sys.cpus().first() { cpu.brand().trim().to_string() } else { "Unknown CPU".to_string() };
|
||||||
let cpu_brand = if let Some(cpu) = sys.cpus().first() {
|
|
||||||
cpu.brand().trim().to_string()
|
|
||||||
} else {
|
|
||||||
"Unknown CPU".to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let hardware = HardwareSummary {
|
let hardware = HardwareSummary {
|
||||||
cpu_name: cpu_brand,
|
cpu_name: cpu_brand, sys_vendor, sys_product, mobo_vendor, mobo_product,
|
||||||
sys_vendor, sys_product, mobo_vendor, mobo_product,
|
|
||||||
memory_total_gb: sys.total_memory() / 1024 / 1024 / 1024,
|
memory_total_gb: sys.total_memory() / 1024 / 1024 / 1024,
|
||||||
memory_used_gb: sys.used_memory() / 1024 / 1024 / 1024,
|
memory_used_gb: sys.used_memory() / 1024 / 1024 / 1024,
|
||||||
os_version: System::long_os_version().unwrap_or("Unknown".to_string()),
|
os_version: System::long_os_version().unwrap_or("Unknown".to_string()),
|
||||||
bios_version: bios_ver,
|
bios_version: bios_ver,
|
||||||
c_drive_total_gb: c_total,
|
c_drive_total_gb: c_total, c_drive_used_gb: c_used,
|
||||||
c_drive_used_gb: c_used,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 发送硬件事件
|
|
||||||
let _ = window.emit("report-hardware", hardware);
|
let _ = window.emit("report-hardware", hardware);
|
||||||
}
|
}
|
||||||
|
|
||||||
// WMI 连接复用 (如果需要) 或者重新建立
|
|
||||||
let wmi_con = WMIConnection::new(COMLibrary::new().unwrap()).ok();
|
let wmi_con = WMIConnection::new(COMLibrary::new().unwrap()).ok();
|
||||||
|
|
||||||
// 2. 存储设备 (较快)
|
|
||||||
{
|
{
|
||||||
let mut storage = Vec::new();
|
let mut storage = Vec::new();
|
||||||
if let Some(con) = &wmi_con {
|
if let Some(con) = &wmi_con {
|
||||||
@@ -241,8 +557,6 @@ async fn run_diagnosis(window: tauri::Window) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
let _ = window.emit("report-storage", storage);
|
let _ = window.emit("report-storage", storage);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 驱动 (快)
|
|
||||||
{
|
{
|
||||||
let mut driver_issues = Vec::new();
|
let mut driver_issues = Vec::new();
|
||||||
if let Some(con) = &wmi_con {
|
if let Some(con) = &wmi_con {
|
||||||
@@ -258,32 +572,25 @@ async fn run_diagnosis(window: tauri::Window) -> Result<(), String> {
|
|||||||
};
|
};
|
||||||
driver_issues.push(DriverIssue {
|
driver_issues.push(DriverIssue {
|
||||||
device_name: dev.Name.unwrap_or("未知设备".to_string()),
|
device_name: dev.Name.unwrap_or("未知设备".to_string()),
|
||||||
error_code: code,
|
error_code: code, description: desc.to_string(),
|
||||||
description: desc.to_string(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let _ = window.emit("report-drivers", driver_issues);
|
let _ = window.emit("report-drivers", driver_issues);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Minidump (快)
|
|
||||||
{
|
{
|
||||||
let mut minidump = MinidumpInfo { found: false, count: 0, explanation: "无蓝屏记录".to_string() };
|
let mut minidump = MinidumpInfo { found: false, count: 0, explanation: "无蓝屏记录".to_string() };
|
||||||
if let Ok(entries) = fs::read_dir("C:\\Windows\\Minidump") {
|
if let Ok(entries) = fs::read_dir("C:\\Windows\\Minidump") {
|
||||||
let count = entries.count();
|
let count = entries.count();
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
minidump = MinidumpInfo {
|
minidump = MinidumpInfo {
|
||||||
found: true,
|
found: true, count, explanation: format!("发现 {} 次蓝屏崩溃", count),
|
||||||
count,
|
|
||||||
explanation: format!("发现 {} 次蓝屏崩溃", count),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let _ = window.emit("report-minidumps", minidump);
|
let _ = window.emit("report-minidumps", minidump);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 电池 (快)
|
|
||||||
{
|
{
|
||||||
let mut battery_info = None;
|
let mut battery_info = None;
|
||||||
if let Some(con) = &wmi_con {
|
if let Some(con) = &wmi_con {
|
||||||
@@ -295,26 +602,16 @@ async fn run_diagnosis(window: tauri::Window) -> Result<(), String> {
|
|||||||
if design > 0 {
|
if design > 0 {
|
||||||
let health = ((full as f64 / design as f64) * 100.0) as u32;
|
let health = ((full as f64 / design as f64) * 100.0) as u32;
|
||||||
let ac_plugged = status == 2 || status == 6 || status == 1;
|
let ac_plugged = status == 2 || status == 6 || status == 1;
|
||||||
let explain = if health < 60 {
|
let explain = if health < 60 { "电池老化严重,建议更换,否则可能导致供电不稳。".to_string() } else { "电池状态良好。".to_string() };
|
||||||
"电池老化严重,建议更换,否则可能导致供电不稳。".to_string()
|
|
||||||
} else {
|
|
||||||
"电池状态良好。".to_string()
|
|
||||||
};
|
|
||||||
battery_info = Some(BatteryInfo {
|
battery_info = Some(BatteryInfo {
|
||||||
health_percentage: health,
|
health_percentage: health, is_ac_connected: ac_plugged, explanation: explain,
|
||||||
is_ac_connected: ac_plugged,
|
|
||||||
explanation: explain,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 即使是 None 也要发,以便前端知道检查完成了
|
|
||||||
// 这里为了简化,直接发 Option
|
|
||||||
let _ = window.emit("report-battery", battery_info);
|
let _ = window.emit("report-battery", battery_info);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 日志 (最慢,放在最后)
|
|
||||||
{
|
{
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
if let Some(con) = &wmi_con {
|
if let Some(con) = &wmi_con {
|
||||||
@@ -323,12 +620,10 @@ async fn run_diagnosis(window: tauri::Window) -> Result<(), String> {
|
|||||||
"SELECT TimeGenerated, EventCode, SourceName, Message FROM Win32_NTLogEvent WHERE Logfile = 'System' AND TimeGenerated >= '{}' AND (EventCode = 41 OR EventCode = 18 OR EventCode = 19 OR EventCode = 7 OR EventCode = 1001 OR EventCode = 4101)",
|
"SELECT TimeGenerated, EventCode, SourceName, Message FROM Win32_NTLogEvent WHERE Logfile = 'System' AND TimeGenerated >= '{}' AND (EventCode = 41 OR EventCode = 18 OR EventCode = 19 OR EventCode = 7 OR EventCode = 1001 OR EventCode = 4101)",
|
||||||
start_time_str
|
start_time_str
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Ok(results) = con.raw_query::<Win32_NTLogEvent>(&query) {
|
if let Ok(results) = con.raw_query::<Win32_NTLogEvent>(&query) {
|
||||||
for event in results {
|
for event in results {
|
||||||
let mut is_target_event = false;
|
let mut is_target_event = false;
|
||||||
let mut hint = String::new();
|
let mut hint = String::new();
|
||||||
|
|
||||||
if event.EventCode == 41 && event.SourceName == "Microsoft-Windows-Kernel-Power" {
|
if event.EventCode == 41 && event.SourceName == "Microsoft-Windows-Kernel-Power" {
|
||||||
is_target_event = true; hint = "系统意外断电 (电源/强关)".to_string();
|
is_target_event = true; hint = "系统意外断电 (电源/强关)".to_string();
|
||||||
} else if (event.EventCode == 18 || event.EventCode == 19) && event.SourceName == "Microsoft-Windows-WHEA-Logger" {
|
} else if (event.EventCode == 18 || event.EventCode == 19) && event.SourceName == "Microsoft-Windows-WHEA-Logger" {
|
||||||
@@ -340,7 +635,6 @@ async fn run_diagnosis(window: tauri::Window) -> Result<(), String> {
|
|||||||
} else if event.EventCode == 4101 && event.SourceName == "Display" {
|
} else if event.EventCode == 4101 && event.SourceName == "Display" {
|
||||||
is_target_event = true; hint = "显卡驱动停止响应并已恢复 (TDR)".to_string();
|
is_target_event = true; hint = "显卡驱动停止响应并已恢复 (TDR)".to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_target_event {
|
if is_target_event {
|
||||||
events.push(SystemEvent {
|
events.push(SystemEvent {
|
||||||
time_generated: format_wmi_time(&event.TimeGenerated),
|
time_generated: format_wmi_time(&event.TimeGenerated),
|
||||||
@@ -350,23 +644,20 @@ async fn run_diagnosis(window: tauri::Window) -> Result<(), String> {
|
|||||||
analysis_hint: hint,
|
analysis_hint: hint,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if events.len() >= 10 { break; }
|
// [修改] 移除了数量限制,让它获取所有符合条件的日志
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let _ = window.emit("report-events", events);
|
let _ = window.emit("report-events", events);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. 全部完成信号
|
|
||||||
let _ = window.emit("diagnosis-finished", ());
|
let _ = window.emit("diagnosis-finished", ());
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.invoke_handler(tauri::generate_handler![run_diagnosis])
|
.invoke_handler(tauri::generate_handler![run_diagnosis, list_minidumps, analyze_minidump, analyze_minidump_bytes])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
@@ -12,9 +12,9 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "system-doctor",
|
"title": "系统检查 v0.1",
|
||||||
"width": 800,
|
"width": 1200,
|
||||||
"height": 600
|
"height": 800
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
|
|||||||
747
src/App.vue
@@ -1,108 +1,91 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="app-layout">
|
||||||
<!-- 顶部标题栏 -->
|
<!-- 左侧侧边栏 -->
|
||||||
<header class="header-bar">
|
<aside class="sidebar">
|
||||||
<div class="brand">
|
<div class="brand">
|
||||||
<span class="icon">🩺</span>
|
<span class="icon">🩺</span>
|
||||||
<h1>静态排查仪表盘</h1>
|
<h1>体检中心</h1>
|
||||||
</div>
|
</div>
|
||||||
<span class="version-tag">Powered by Rust & Tauri</span>
|
<nav class="nav-menu">
|
||||||
</header>
|
<button
|
||||||
|
class="nav-item"
|
||||||
|
:class="{ active: currentTab === 'overview' }"
|
||||||
|
@click="currentTab = 'overview'"
|
||||||
|
>
|
||||||
|
<span class="nav-icon">📊</span> 健康概览
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="nav-item"
|
||||||
|
:class="{ active: currentTab === 'bsod' }"
|
||||||
|
@click="currentTab = 'bsod'"
|
||||||
|
>
|
||||||
|
<span class="nav-icon">☠️</span> 蓝屏分析
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="nav-item"
|
||||||
|
:class="{ active: currentTab === 'logs' }"
|
||||||
|
@click="currentTab = 'logs'"
|
||||||
|
>
|
||||||
|
<span class="nav-icon">⚡</span> 系统日志
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<span class="version">Generated by AI</span>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
<!-- 工具栏:左侧主操作,右侧辅助操作 -->
|
<!-- 右侧主内容区 -->
|
||||||
<div class="toolbar">
|
<main class="main-content">
|
||||||
<div class="left-actions">
|
|
||||||
|
<!-- TAB 1: 概览 -->
|
||||||
|
<div v-if="currentTab === 'overview'" class="tab-view overview-view">
|
||||||
|
<div class="view-header">
|
||||||
|
<h2>系统健康概览</h2>
|
||||||
|
<div class="actions-row">
|
||||||
<button @click="startScan" :disabled="loading" class="primary-btn" :class="{ 'scanning': loading }">
|
<button @click="startScan" :disabled="loading" class="primary-btn" :class="{ 'scanning': loading }">
|
||||||
<span v-if="loading" class="icon-spin">🔄</span>
|
<span v-if="loading" class="icon-spin">🔄</span>
|
||||||
<span v-if="loading">正在排查...</span>
|
<span v-else>🔍 全面体检</span>
|
||||||
<template v-else>
|
|
||||||
<span class="btn-icon">🔍</span> 开始全面体检
|
|
||||||
</template>
|
|
||||||
</button>
|
</button>
|
||||||
|
<div class="sub-actions">
|
||||||
|
<button class="icon-btn" @click="triggerImport" :disabled="loading">📂 导入</button>
|
||||||
|
<button class="icon-btn" @click="exportReport" :disabled="!isReportValid || loading">💾 导出</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="right-actions">
|
|
||||||
<button class="icon-btn" @click="triggerImport" :disabled="loading" title="导入报告">
|
|
||||||
📂 导入
|
|
||||||
</button>
|
|
||||||
<button class="icon-btn" @click="exportReport" :disabled="!isReportValid || loading" title="导出报告">
|
|
||||||
💾 导出
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 隐藏的文件输入框 -->
|
<!-- 状态提示 -->
|
||||||
|
<div v-if="errorMsg" class="message-box error">⚠️ {{ errorMsg }}</div>
|
||||||
|
|
||||||
<input type="file" ref="fileInput" @change="handleFileImport" accept=".json" style="display: none" />
|
<input type="file" ref="fileInput" @change="handleFileImport" accept=".json" style="display: none" />
|
||||||
|
|
||||||
<!-- 错误提示 (保留在原本位置或顶部) -->
|
<!-- 卡片展示区 -->
|
||||||
<div v-if="errorMsg" class="inline-error">
|
|
||||||
⚠️ {{ errorMsg }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 右上角 Toast 通知 (通用化) -->
|
|
||||||
<transition name="toast-slide">
|
|
||||||
<div v-if="toast.show" class="toast-notification" :class="toast.type">
|
|
||||||
<div class="toast-content">
|
|
||||||
<span class="check-icon">{{ toast.type === 'error' ? '❌' : '✅' }}</span>
|
|
||||||
<div class="toast-text">
|
|
||||||
<h4>{{ toast.title }}</h4>
|
|
||||||
<p>{{ toast.message }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="toast-progress"></div>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
|
|
||||||
<!-- 结果显示区域 -->
|
|
||||||
<div v-if="report.hardware" class="dashboard fade-in">
|
<div v-if="report.hardware" class="dashboard fade-in">
|
||||||
|
<!-- 1. 硬件概览 (强制独占一行) -->
|
||||||
<!-- 1. 硬件概览 -->
|
<div class="card summary slide-up">
|
||||||
<div v-if="report.hardware" class="card summary slide-up">
|
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2>🖥️ 硬件概览与资源占用</h2>
|
<h3>🖥️ 硬件概览与资源占用</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid-container-summary">
|
<div class="grid-container-summary">
|
||||||
<div class="basic-info">
|
<div class="basic-info">
|
||||||
<div class="info-item">
|
<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>
|
||||||
<span class="label">CPU 型号</span>
|
<div class="info-item"><span class="label">System</span><span class="value text-ellipsis">{{ report.hardware.sys_vendor }} {{ report.hardware.sys_product }}</span></div>
|
||||||
<span class="value text-ellipsis" :title="report.hardware.cpu_name">{{ report.hardware.cpu_name }}</span>
|
<div class="info-item"><span class="label">Mobo</span><span class="value text-ellipsis">{{ report.hardware.mobo_vendor }} {{ report.hardware.mobo_product }}</span></div>
|
||||||
|
<div class="info-item"><span class="label">BIOS</span><span class="value text-ellipsis">{{ report.hardware.bios_version }}</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
|
||||||
<span class="label">整机型号</span>
|
|
||||||
<span class="value text-ellipsis">{{ report.hardware.sys_vendor }} {{ report.hardware.sys_product }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="label">主板型号</span>
|
|
||||||
<span class="value text-ellipsis">{{ 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">
|
<div class="usage-bars">
|
||||||
<div class="usage-item">
|
<div class="usage-item">
|
||||||
<div class="progress-label">
|
<div class="progress-label">
|
||||||
<span>C盘空间 (已用 {{ report.hardware.c_drive_used_gb }}GB / 总计 {{ report.hardware.c_drive_total_gb }}GB)</span>
|
<span class="text-ellipsis">C盘 ({{ report.hardware.c_drive_used_gb }}/{{ report.hardware.c_drive_total_gb }}GB)</span>
|
||||||
<strong>{{ calculatePercent(report.hardware.c_drive_used_gb, report.hardware.c_drive_total_gb) }}%</strong>
|
<strong>{{ calculatePercent(report.hardware.c_drive_used_gb, report.hardware.c_drive_total_gb) }}%</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-bar-large">
|
<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 class="fill" :style="getProgressStyle(report.hardware.c_drive_used_gb, report.hardware.c_drive_total_gb)"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="usage-item">
|
<div class="usage-item">
|
||||||
<div class="progress-label">
|
<div class="progress-label">
|
||||||
<span>内存占用 (已用 {{ report.hardware.memory_used_gb }}GB / 总计 {{ report.hardware.memory_total_gb }}GB)</span>
|
<span class="text-ellipsis">内存 ({{ report.hardware.memory_used_gb }}/{{ report.hardware.memory_total_gb }}GB)</span>
|
||||||
<strong>{{ calculatePercent(report.hardware.memory_used_gb, report.hardware.memory_total_gb) }}%</strong>
|
<strong>{{ calculatePercent(report.hardware.memory_used_gb, report.hardware.memory_total_gb) }}%</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-bar-large">
|
<div class="progress-bar-large"><div class="fill" :style="getProgressStyle(report.hardware.memory_used_gb, report.hardware.memory_total_gb)"></div></div>
|
||||||
<div class="fill" :style="getProgressStyle(report.hardware.memory_used_gb, report.hardware.memory_total_gb)"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -111,125 +94,209 @@
|
|||||||
<!-- 2. 驱动状态 -->
|
<!-- 2. 驱动状态 -->
|
||||||
<div v-if="report.drivers" class="card slide-up" :class="{ danger: report.drivers.length > 0, success: report.drivers.length === 0 }">
|
<div v-if="report.drivers" class="card slide-up" :class="{ danger: report.drivers.length > 0, success: report.drivers.length === 0 }">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2>🔌 驱动与设备状态</h2>
|
<h3>🔌 驱动状态</h3>
|
||||||
<span class="badge" :class="report.drivers.length > 0 ? 'badge-red' : 'badge-green'">
|
<span class="badge" :class="report.drivers.length > 0 ? 'badge-red' : 'badge-green'">
|
||||||
{{ report.drivers.length > 0 ? '发现异常' : '正常' }}
|
{{ report.drivers.length > 0 ? '异常' : '正常' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="report.drivers.length === 0" class="good-news">✅ 设备管理器中未发现带有黄色感叹号或错误的设备。</div>
|
<div v-if="report.drivers.length > 0" class="list-container">
|
||||||
<div v-else class="list-container">
|
|
||||||
<div v-for="(drv, idx) in report.drivers" :key="idx" class="list-item error-item">
|
<div v-for="(drv, idx) in report.drivers" :key="idx" class="list-item error-item">
|
||||||
<div class="item-header">
|
<div class="item-header">
|
||||||
<strong>{{ drv.device_name }}</strong>
|
<strong class="text-ellipsis" :title="drv.device_name">{{ drv.device_name }}</strong>
|
||||||
<span class="code-tag">Code {{ drv.error_code }}</span>
|
<span class="code-tag">Code {{ drv.error_code }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="description">{{ drv.description }}</p>
|
<p class="description">{{ drv.description }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="good-news">设备管理器无异常。</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 3. 电池健康 -->
|
<!-- 3. 电池健康 -->
|
||||||
<div v-if="report.battery" class="card slide-up" :class="{ danger: report.battery.health_percentage < 60 }">
|
<div v-if="report.battery" class="card slide-up" :class="{ danger: report.battery.health_percentage < 60 }">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2>🔋 电池健康度 (寿命)</h2>
|
<h3>🔋 电池健康</h3>
|
||||||
<span class="badge" :class="report.battery.is_ac_connected ? 'badge-blue' : 'badge-gray'">
|
<span class="badge badge-blue">{{ report.battery.health_percentage }}%</span>
|
||||||
{{ report.battery.is_ac_connected ? '⚡ 已连接电源' : '🔋 使用电池中' }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="content-box">
|
<div class="content-box">
|
||||||
<div class="progress-wrapper">
|
<div class="progress-bar"><div class="fill" :style="{ width: report.battery.health_percentage + '%', background: getBatteryColor(report.battery.health_percentage) }"></div></div>
|
||||||
<div class="progress-label">
|
<p class="description mt-2">{{ report.battery.explanation }}</p>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 4. 硬盘健康 -->
|
<!-- 4. 硬盘健康 -->
|
||||||
<div v-if="report.storage" class="card slide-up" :class="{ danger: hasStorageDanger }">
|
<div v-if="report.storage" class="card slide-up" :class="{ danger: hasStorageDanger }">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2>💾 硬盘健康度 (S.M.A.R.T)</h2>
|
<h3>💾 硬盘 S.M.A.R.T</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="list-container">
|
<div class="list-container">
|
||||||
<div v-for="(disk, index) in report.storage" :key="index" class="list-item">
|
<div v-for="(disk, index) in report.storage" :key="index" class="list-item">
|
||||||
<div class="item-header">
|
<div class="item-header">
|
||||||
<strong>{{ disk.model }}</strong>
|
<strong class="text-ellipsis" :title="disk.model">{{ disk.model }}</strong>
|
||||||
<span class="status-text" :class="disk.is_danger ? 'text-red' : 'text-green'">
|
<span class="status-text" :class="disk.is_danger ? 'text-red' : 'text-green'">{{ disk.health_status }}</span>
|
||||||
{{ disk.health_status }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="description">{{ disk.human_explanation }}</p>
|
<p class="description">{{ disk.human_explanation }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 5. 蓝屏分析 -->
|
<!-- [已移除] 5. 蓝屏分析卡片 -->
|
||||||
<div v-if="report.minidumps" class="card slide-up" :class="{ danger: report.minidumps.found, success: !report.minidumps.found }">
|
<!-- [已移除] 6. 关键日志卡片 -->
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 6. 关键日志 -->
|
<!-- TAB 2: 蓝屏分析 -->
|
||||||
<div v-if="report.events" class="card slide-up" :class="{ danger: report.events.length > 0 }">
|
<div v-if="currentTab === 'bsod'" class="tab-view bsod-view">
|
||||||
<div class="card-header">
|
<div class="view-header">
|
||||||
<h2>⚡ 关键供电与硬件日志 (Event Log)</h2>
|
<h2>蓝屏死机分析 (Minidump)</h2>
|
||||||
|
<!-- 按钮组 -->
|
||||||
|
<div class="actions-row">
|
||||||
|
<button class="secondary-btn" @click="triggerBsodImport" :disabled="bsodAnalyzing">📂 导入文件</button>
|
||||||
|
<button class="secondary-btn" @click="loadMinidumps" :disabled="bsodLoading">🔄 刷新列表</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="report.events.length === 0" class="good-news">✅ 近期日志中未发现 Kernel-Power(41) 或 WHEA(18/19) 等致命错误。</div>
|
</div>
|
||||||
<div v-else class="list-container">
|
|
||||||
<div v-for="(evt, idx) in report.events" :key="idx" class="list-item warning-item">
|
<!-- 隐藏的 BSOD 文件输入框 (添加 multiple) -->
|
||||||
<div class="item-header">
|
<input type="file" ref="bsodFileInput" @change="handleBsodFileImport" accept=".dmp" style="display: none" multiple />
|
||||||
|
|
||||||
|
<div class="bsod-layout">
|
||||||
|
<div class="bsod-list-panel">
|
||||||
|
<div v-if="bsodList.length === 0 && !bsodLoading" class="empty-state">
|
||||||
|
未发现 Minidump 文件。<br>系统可能未启用转储或已被清理。
|
||||||
|
</div>
|
||||||
|
<div v-else class="file-list">
|
||||||
|
<div
|
||||||
|
v-for="file in bsodList"
|
||||||
|
:key="file.path"
|
||||||
|
class="file-item"
|
||||||
|
:class="{ active: selectedBsod?.path === file.path, imported: !!file.fileRef }"
|
||||||
|
@click="analyzeBsod(file)"
|
||||||
|
>
|
||||||
|
<div class="file-icon">{{ file.fileRef ? '📨' : '📄' }}</div>
|
||||||
|
<div class="file-info">
|
||||||
|
<div class="file-name">{{ file.filename }}</div>
|
||||||
|
<div class="file-meta">{{ file.created_time }} · {{ file.size_kb }}KB</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bsod-detail-panel">
|
||||||
|
<div v-if="bsodAnalyzing" class="analyzing-state">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>正在解析 Dump 文件,可能需要几秒钟...</p>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="bsodResult" class="analysis-result fade-in">
|
||||||
|
<div class="result-header">
|
||||||
|
<h3>{{ bsodResult.crash_reason }}</h3>
|
||||||
|
<span class="code-pill">{{ bsodResult.bug_check_code }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="human-analysis card-section">
|
||||||
|
<h4>🤖 智能分析</h4>
|
||||||
|
<p class="human-text">{{ bsodResult.human_analysis }}</p>
|
||||||
|
<div class="recommendation">
|
||||||
|
<strong>建议操作:</strong> {{ bsodResult.recommendation }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tech-details card-section">
|
||||||
|
<h4>🔬 技术细节</h4>
|
||||||
|
<div class="detail-grid">
|
||||||
|
<div class="d-item"><span class="d-label">崩溃地址</span>{{ bsodResult.crash_address }}</div>
|
||||||
|
<div class="d-item"><span class="d-label">相关线程</span>{{ bsodResult.crashing_thread || 'N/A' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="empty-detail">
|
||||||
|
👈 请从左侧选择一个蓝屏文件开始分析,<br>或点击上方“导入文件”分析外部文件
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TAB 3: 系统日志 -->
|
||||||
|
<div v-if="currentTab === 'logs'" class="tab-view logs-view">
|
||||||
|
<div class="view-header">
|
||||||
|
<h2>系统关键日志 (近30天)</h2>
|
||||||
|
<div class="actions-row">
|
||||||
|
<button class="secondary-btn" @click="startScan" :disabled="loading">🔄 刷新日志</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="logs-container fade-in">
|
||||||
|
<div v-if="!report.events" class="empty-state">
|
||||||
|
<span v-if="loading">正在读取日志记录...</span>
|
||||||
|
<span v-else>请先点击“全面体检”或“刷新日志”以获取数据。</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="report.events.length === 0" class="good-news large">
|
||||||
|
✅ 过去30天内未发现致命错误。
|
||||||
|
</div>
|
||||||
|
<div v-else class="log-list-full">
|
||||||
|
<div v-for="(evt, idx) in report.events" :key="idx" class="log-card warning-item">
|
||||||
|
<div class="log-header">
|
||||||
|
<div class="log-meta">
|
||||||
<span class="event-id">ID: {{ evt.event_id }}</span>
|
<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>
|
||||||
<span class="event-time">{{ evt.time_generated }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<span class="event-source">{{ evt.source }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="log-body">
|
||||||
<p class="description highlight">{{ evt.analysis_hint }}</p>
|
<p class="description highlight">{{ evt.analysis_hint }}</p>
|
||||||
<p v-if="evt.message" class="description raw-message">{{ evt.message }}</p>
|
<p v-if="evt.message" class="description raw-message">{{ evt.message }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Toast 通知 -->
|
||||||
|
<transition name="toast-slide">
|
||||||
|
<div v-if="toast.show" class="toast-notification" :class="toast.type">
|
||||||
|
<div class="toast-content">
|
||||||
|
<span class="check-icon">{{ toast.type === 'error' ? '❌' : '✅' }}</span>
|
||||||
|
<div class="toast-text"><h4>{{ toast.title }}</h4><p>{{ toast.message }}</p></div>
|
||||||
|
</div>
|
||||||
|
<div class="toast-progress"></div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onUnmounted, reactive } from 'vue';
|
import { ref, computed, onUnmounted, reactive, onMounted, watch } from 'vue';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { listen } from '@tauri-apps/api/event';
|
import { listen } from '@tauri-apps/api/event';
|
||||||
|
|
||||||
const emptyReport = () => ({
|
// --- 状态管理 ---
|
||||||
hardware: null, storage: null, events: null, minidumps: null, drivers: null, battery: null
|
const currentTab = ref('overview'); // 'overview' | 'bsod' | 'logs'
|
||||||
});
|
|
||||||
|
|
||||||
|
// 概览相关
|
||||||
|
const emptyReport = () => ({ hardware: null, storage: null, events: null, minidumps: null, drivers: null, battery: null });
|
||||||
const report = ref(emptyReport());
|
const report = ref(emptyReport());
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const errorMsg = ref('');
|
const errorMsg = ref('');
|
||||||
|
// [移除] scanFinished 状态变量,因为不再需要显示 div 提示
|
||||||
|
// const scanFinished = ref(false);
|
||||||
const fileInput = ref(null);
|
const fileInput = ref(null);
|
||||||
let unlistenFns = [];
|
let unlistenFns = [];
|
||||||
let toastTimer = null;
|
|
||||||
|
|
||||||
// 通用 Toast 状态
|
// BSOD 相关
|
||||||
const toast = reactive({
|
const bsodList = ref([]);
|
||||||
show: false,
|
const selectedBsod = ref(null);
|
||||||
title: '',
|
const bsodResult = ref(null);
|
||||||
message: '',
|
const bsodLoading = ref(false);
|
||||||
type: 'success' // 'success' | 'error'
|
const bsodAnalyzing = ref(false);
|
||||||
});
|
const bsodFileInput = ref(null); // 新增 BSOD 文件输入引用
|
||||||
|
|
||||||
|
const toast = reactive({ show: false, title: '', message: '', type: 'success' });
|
||||||
|
let toastTimer = null;
|
||||||
|
|
||||||
const hasStorageDanger = computed(() => report.value?.storage?.some(d => d.is_danger) || false);
|
const hasStorageDanger = computed(() => report.value?.storage?.some(d => d.is_danger) || false);
|
||||||
const isReportValid = computed(() => report.value && (report.value.hardware || report.value.storage));
|
const isReportValid = computed(() => report.value && (report.value.hardware || report.value.storage));
|
||||||
|
|
||||||
// --- 辅助函数 ---
|
// --- 辅助函数 (保留) ---
|
||||||
function calculatePercent(used, total) { return (!total || total === 0) ? 0 : Math.round((used / total) * 100); }
|
function calculatePercent(used, total) { return (!total || total === 0) ? 0 : Math.round((used / total) * 100); }
|
||||||
function getProgressStyle(used, total) {
|
function getProgressStyle(used, total) {
|
||||||
const percent = calculatePercent(used, total);
|
const percent = calculatePercent(used, total);
|
||||||
@@ -239,272 +306,254 @@ function getProgressStyle(used, total) {
|
|||||||
function getBatteryColor(p) { return p < 50 ? '#ff4757' : (p < 80 ? '#ffa502' : '#2ed573'); }
|
function getBatteryColor(p) { return p < 50 ? '#ff4757' : (p < 80 ? '#ffa502' : '#2ed573'); }
|
||||||
function formatTime(t) { return !t ? "Unknown Time" : t.replace('T', ' ').substring(0, 19); }
|
function formatTime(t) { return !t ? "Unknown Time" : t.replace('T', ' ').substring(0, 19); }
|
||||||
|
|
||||||
// --- 导出/导入 ---
|
// JS日期格式化函数:yyyy-MM-dd HH:mm:ss
|
||||||
|
function formatJsDate(date) {
|
||||||
|
const y = date.getFullYear();
|
||||||
|
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const d = String(date.getDate()).padStart(2, '0');
|
||||||
|
const h = String(date.getHours()).padStart(2, '0');
|
||||||
|
const min = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
const s = String(date.getSeconds()).padStart(2, '0');
|
||||||
|
return `${y}-${m}-${d} ${h}:${min}:${s}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 概览:导出/导入 ---
|
||||||
async function exportReport() {
|
async function exportReport() {
|
||||||
if (!isReportValid.value) return;
|
if (!isReportValid.value) return;
|
||||||
const fileName = `SystemDoctor_Report_${new Date().toISOString().slice(0, 19).replace(/[:T]/g, "-")}.json`;
|
const fileName = `SystemDoctor_Report_${new Date().toISOString().slice(0, 19).replace(/[:T]/g, "-")}.json`;
|
||||||
const content = JSON.stringify(report.value, null, 2);
|
const content = JSON.stringify(report.value, null, 2);
|
||||||
|
if ('showSaveFilePicker' in window) {
|
||||||
// 尝试使用现代文件系统 API (显示保存对话框)
|
|
||||||
if (window.showSaveFilePicker) {
|
|
||||||
try {
|
try {
|
||||||
const handle = await window.showSaveFilePicker({
|
const handle = await window.showSaveFilePicker({ suggestedName: fileName, types: [{ description: 'JSON Report', accept: { 'application/json': ['.json'] }, }], });
|
||||||
suggestedName: fileName,
|
const writable = await handle.createWritable(); await writable.write(content); await writable.close();
|
||||||
types: [{
|
triggerToast('导出成功', '文件已保存到指定位置', 'success'); return;
|
||||||
description: 'JSON Report',
|
} catch (err) { if (err.name === 'AbortError') return; }
|
||||||
accept: { 'application/json': ['.json'] },
|
}
|
||||||
}],
|
try {
|
||||||
});
|
const blob = new Blob([content], { type: "application/json" }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = fileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url);
|
||||||
const writable = await handle.createWritable();
|
triggerToast('导出成功', '已保存到默认下载目录', 'success');
|
||||||
await writable.write(content);
|
} catch (err) { triggerToast('导出失败', err.message, 'error'); }
|
||||||
await writable.close();
|
}
|
||||||
triggerToast('导出成功', '文件已保存到指定位置', 'success');
|
function triggerImport() { fileInput.value.click(); }
|
||||||
|
function handleFileImport(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { try { const json = JSON.parse(e.target.result); if (json && (json.hardware || json.storage)) { report.value = json; triggerToast('导入成功', '已加载历史报告'); } else { triggerToast('导入失败', '文件格式错误', 'error'); } } catch (err) { triggerToast('解析失败', err.message, 'error'); } }; reader.readAsText(file); event.target.value = ''; }
|
||||||
|
|
||||||
|
async function startScan() {
|
||||||
|
// 只有在从未扫描过时清空,或者强制刷新
|
||||||
|
// report.value = emptyReport();
|
||||||
|
errorMsg.value = '';
|
||||||
|
loading.value = true;
|
||||||
|
if (unlistenFns.length > 0) { for (const fn of unlistenFns) fn(); unlistenFns = []; }
|
||||||
|
try {
|
||||||
|
unlistenFns.push(
|
||||||
|
await listen('report-hardware', (e) => report.value.hardware = e.payload),
|
||||||
|
await listen('report-storage', (e) => report.value.storage = e.payload),
|
||||||
|
await listen('report-drivers', (e) => report.value.drivers = e.payload),
|
||||||
|
await listen('report-minidumps', (e) => report.value.minidumps = e.payload),
|
||||||
|
await listen('report-battery', (e) => report.value.battery = e.payload),
|
||||||
|
await listen('report-events', (e) => report.value.events = e.payload),
|
||||||
|
await listen('diagnosis-finished', () => { loading.value = false; triggerToast('体检完成', '所有项目检查完毕', 'success'); })
|
||||||
|
);
|
||||||
|
await invoke('run_diagnosis');
|
||||||
|
} catch (e) { loading.value = false; errorMsg.value = e; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- BSOD 功能 ---
|
||||||
|
async function loadMinidumps() { bsodLoading.value = true; try { bsodList.value = await invoke('list_minidumps'); } catch (e) { triggerToast('加载失败', e, 'error'); } finally { bsodLoading.value = false; } }
|
||||||
|
|
||||||
|
async function analyzeBsod(file) {
|
||||||
|
if (bsodAnalyzing.value) return;
|
||||||
|
|
||||||
|
// 提前检查文件大小,避免读取几百MB的 Kernel Dump 导致卡死
|
||||||
|
if (file.fileRef && file.fileRef.size > 5 * 1024 * 1024) { // 5MB 限制
|
||||||
|
triggerToast('文件过大', '仅支持 <5MB 的 Minidump 文件,不支持完整的 MEMORY.DMP', 'error');
|
||||||
return;
|
return;
|
||||||
} catch (err) {
|
|
||||||
// 用户取消不报错,其他错误降级处理
|
|
||||||
if (err.name !== 'AbortError') {
|
|
||||||
console.warn('Native save dialog failed, falling back to download link', err);
|
|
||||||
} else {
|
|
||||||
return; // 用户取消
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 降级方案:创建链接直接下载 (默认下载文件夹)
|
selectedBsod.value = file;
|
||||||
|
bsodResult.value = null;
|
||||||
|
bsodAnalyzing.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const blob = new Blob([content], { type: "application/json" });
|
// 判断是导入的文件还是本地文件
|
||||||
const url = URL.createObjectURL(blob);
|
if (file.fileRef) {
|
||||||
const link = document.createElement('a');
|
// 导入的文件:读取 ArrayBuffer 并传给后端
|
||||||
link.href = url;
|
const arrayBuffer = await file.fileRef.arrayBuffer();
|
||||||
link.download = fileName;
|
const bytes = new Uint8Array(arrayBuffer);
|
||||||
document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url);
|
const byteArray = Array.from(bytes);
|
||||||
triggerToast('导出成功', '文件已保存到默认下载目录', 'success');
|
bsodResult.value = await invoke('analyze_minidump_bytes', { fileContent: byteArray });
|
||||||
} catch (err) {
|
} else {
|
||||||
errorMsg.value = "导出失败: " + err.message;
|
// 本地文件:传路径
|
||||||
triggerToast('导出失败', err.message, 'error');
|
bsodResult.value = await invoke('analyze_minidump', { filepath: file.path });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 优化错误提示
|
||||||
|
let errorMsg = e;
|
||||||
|
if (typeof e === 'string') {
|
||||||
|
if (e.includes("Header mismatch")) {
|
||||||
|
errorMsg = "格式错误:这不是有效的 Minidump。请勿上传 MEMORY.DMP (完整转储)。";
|
||||||
|
} else if (e.includes("No Exception Stream")) {
|
||||||
|
errorMsg = "无法提取错误:该文件可能已损坏或不包含异常流。";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
triggerToast('分析失败', errorMsg, 'error');
|
||||||
|
} finally {
|
||||||
|
bsodAnalyzing.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function triggerImport() { fileInput.value.click(); }
|
// [新增] BSOD 导入功能 (支持多选)
|
||||||
function handleFileImport(event) {
|
function triggerBsodImport() { bsodFileInput.value.click(); }
|
||||||
const file = event.target.files[0];
|
function handleBsodFileImport(event) {
|
||||||
if (!file) return;
|
const files = event.target.files;
|
||||||
const reader = new FileReader();
|
if (!files || files.length === 0) return;
|
||||||
reader.onload = (e) => {
|
|
||||||
try {
|
let importedCount = 0;
|
||||||
const json = JSON.parse(e.target.result);
|
for (let i = files.length - 1; i >= 0; i--) {
|
||||||
if (json && (json.hardware || json.storage)) {
|
const file = files[i];
|
||||||
report.value = json;
|
|
||||||
errorMsg.value = '';
|
if (!file.name.toLowerCase().endsWith('.dmp')) {
|
||||||
triggerToast('导入成功', '已加载历史报告数据', 'success');
|
continue;
|
||||||
} else {
|
|
||||||
errorMsg.value = "无效的报告文件。";
|
|
||||||
triggerToast('导入失败', '文件格式不正确', 'error');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
errorMsg.value = "解析失败: " + err.message;
|
|
||||||
triggerToast('导入失败', err.message, 'error');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newItem = {
|
||||||
|
filename: file.name,
|
||||||
|
path: `imported-${file.name}-${Date.now()}-${i}`,
|
||||||
|
size_kb: Math.round(file.size / 1024),
|
||||||
|
// [修改] 使用自定义格式化函数,替代 toLocaleString
|
||||||
|
created_time: formatJsDate(new Date(file.lastModified)),
|
||||||
|
fileRef: file
|
||||||
};
|
};
|
||||||
reader.readAsText(file);
|
bsodList.value.unshift(newItem);
|
||||||
|
importedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (importedCount > 0) {
|
||||||
|
triggerToast('导入成功', `已添加 ${importedCount} 个文件到列表`, 'success');
|
||||||
|
} else {
|
||||||
|
triggerToast('导入忽略', '未选择有效的 .dmp 文件', 'error');
|
||||||
|
}
|
||||||
event.target.value = '';
|
event.target.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 扫描逻辑 ---
|
watch(currentTab, (newVal) => { if (newVal === 'bsod' && bsodList.value.length === 0) loadMinidumps(); });
|
||||||
async function startScan() {
|
|
||||||
report.value = emptyReport();
|
|
||||||
errorMsg.value = '';
|
|
||||||
loading.value = true;
|
|
||||||
|
|
||||||
if (unlistenFns.length > 0) { for (const fn of unlistenFns) fn(); unlistenFns = []; }
|
function triggerToast(title, message, type = 'success') { toast.title = title; toast.message = message; toast.type = type; toast.show = true; if (toastTimer) clearTimeout(toastTimer); toastTimer = setTimeout(() => { toast.show = false; }, 3000); }
|
||||||
|
onUnmounted(() => { for (const fn of unlistenFns) fn(); if (toastTimer) clearTimeout(toastTimer); });
|
||||||
try {
|
|
||||||
const l1 = await listen('report-hardware', (e) => report.value.hardware = e.payload);
|
|
||||||
const l2 = await listen('report-storage', (e) => report.value.storage = e.payload);
|
|
||||||
const l3 = await listen('report-drivers', (e) => report.value.drivers = e.payload);
|
|
||||||
const l4 = await listen('report-minidumps', (e) => report.value.minidumps = e.payload);
|
|
||||||
const l5 = await listen('report-battery', (e) => report.value.battery = e.payload);
|
|
||||||
const l6 = await listen('report-events', (e) => report.value.events = e.payload);
|
|
||||||
const l7 = await listen('diagnosis-finished', () => {
|
|
||||||
loading.value = false;
|
|
||||||
triggerToast('扫描已完成', '所有硬件与日志检查完毕', 'success');
|
|
||||||
});
|
|
||||||
unlistenFns.push(l1, l2, l3, l4, l5, l6, l7);
|
|
||||||
await invoke('run_diagnosis');
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
errorMsg.value = "启动扫描失败: " + e;
|
|
||||||
loading.value = false;
|
|
||||||
triggerToast('扫描失败', '无法启动后台诊断程序', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Toast 逻辑 ---
|
|
||||||
function triggerToast(title, message, type = 'success') {
|
|
||||||
toast.title = title;
|
|
||||||
toast.message = message;
|
|
||||||
toast.type = type;
|
|
||||||
toast.show = true;
|
|
||||||
|
|
||||||
if (toastTimer) clearTimeout(toastTimer);
|
|
||||||
toastTimer = setTimeout(() => {
|
|
||||||
toast.show = false;
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
for (const fn of unlistenFns) fn();
|
|
||||||
if (toastTimer) clearTimeout(toastTimer);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body { margin: 0; padding: 0; background-color: #f4f6f9; overflow: hidden; }
|
||||||
margin: 0; padding: 0; background-color: #f4f6f9; /* 更柔和的背景 */
|
|
||||||
overflow-y: scroll;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 容器调整 */
|
.app-layout { display: flex; height: 100vh; width: 100vw; overflow: hidden; background-color: #f4f6f9; }
|
||||||
.container {
|
.sidebar { width: 240px; background: #2c3e50; color: white; display: flex; flex-direction: column; padding: 20px 0; box-shadow: 2px 0 10px rgba(0,0,0,0.1); z-index: 10; }
|
||||||
max-width: 1200px; margin: 0 auto; padding: 20px 30px; box-sizing: border-box;
|
.brand { padding: 0 20px 30px; display: flex; align-items: center; gap: 12px; }
|
||||||
font-family: 'Segoe UI', system-ui, sans-serif; color: #2c3e50;
|
.brand .icon { font-size: 1.8rem; } .brand h1 { font-size: 1.2rem; margin: 0; font-weight: 700; color: white; }
|
||||||
}
|
.nav-menu { flex: 1; display: flex; flex-direction: column; gap: 5px; padding: 0 10px; }
|
||||||
|
.nav-item { background: transparent; border: none; color: #bdc3c7; padding: 12px 15px; font-size: 1rem; cursor: pointer; text-align: left; border-radius: 8px; transition: all 0.2s; display: flex; align-items: center; gap: 12px; }
|
||||||
|
.nav-item:hover { background: rgba(255,255,255,0.1); color: white; }
|
||||||
|
.nav-item.active { background: #2ecc71; color: white; font-weight: 600; box-shadow: 0 4px 10px rgba(46, 204, 113, 0.3); }
|
||||||
|
.nav-icon { font-size: 1.2rem; }
|
||||||
|
.sidebar-footer { padding: 0 20px; text-align: center; color: #7f8c8d; font-size: 0.8rem; }
|
||||||
|
.main-content { flex: 1; overflow-y: auto; padding: 30px 40px; position: relative; }
|
||||||
|
|
||||||
/* --- 顶部标题栏 --- */
|
.view-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px; }
|
||||||
.header-bar {
|
.view-header h2 { margin: 0; font-size: 1.8rem; color: #2c3e50; }
|
||||||
display: flex; justify-content: space-between; align-items: center;
|
.actions-row { display: flex; align-items: center; gap: 15px; }
|
||||||
margin-bottom: 25px; padding-bottom: 15px; border-bottom: 1px solid #e1e4e8;
|
.sub-actions { display: flex; gap: 10px; }
|
||||||
}
|
|
||||||
.brand { display: flex; align-items: center; gap: 10px; }
|
|
||||||
.brand .icon { font-size: 1.8rem; }
|
|
||||||
h1 { font-size: 1.5rem; margin: 0; color: #2c3e50; font-weight: 700; letter-spacing: -0.5px; }
|
|
||||||
.version-tag { font-size: 0.85rem; color: #95a5a6; font-weight: 500; }
|
|
||||||
|
|
||||||
/* --- 工具栏布局 --- */
|
|
||||||
.toolbar {
|
|
||||||
display: flex; justify-content: space-between; align-items: center;
|
|
||||||
margin-bottom: 30px; background: white; padding: 10px 15px;
|
|
||||||
border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.left-actions { display: flex; align-items: center; }
|
|
||||||
.right-actions { display: flex; gap: 10px; }
|
|
||||||
|
|
||||||
/* 主按钮 (绿色强调) */
|
|
||||||
.primary-btn {
|
|
||||||
background: #2ecc71; color: white; border: none;
|
|
||||||
padding: 10px 24px; font-size: 1rem; border-radius: 8px;
|
|
||||||
cursor: pointer; font-weight: 600; display: flex; align-items: center; gap: 8px;
|
|
||||||
transition: background 0.2s, transform 0.1s;
|
|
||||||
}
|
|
||||||
.primary-btn:hover:not(:disabled) { background: #27ae60; transform: translateY(-1px); }
|
|
||||||
.primary-btn:active:not(:disabled) { transform: translateY(1px); }
|
|
||||||
.primary-btn:disabled { background: #bdc3c7; cursor: not-allowed; }
|
|
||||||
.icon-spin { animation: spin 1s linear infinite; }
|
|
||||||
|
|
||||||
/* 辅助按钮 (白色简约) */
|
|
||||||
.icon-btn {
|
|
||||||
background: transparent; border: 1px solid transparent; color: #555;
|
|
||||||
padding: 8px 16px; font-size: 0.95rem; border-radius: 8px;
|
|
||||||
cursor: pointer; transition: all 0.2s;
|
|
||||||
}
|
|
||||||
.icon-btn:hover:not(:disabled) { background: #f0f2f5; color: #2c3e50; }
|
|
||||||
.icon-btn:disabled { color: #ccc; cursor: not-allowed; }
|
|
||||||
|
|
||||||
/* --- Toast 通知 (增强版) --- */
|
|
||||||
.toast-notification {
|
|
||||||
position: fixed; top: 25px; right: 25px; z-index: 9999;
|
|
||||||
background: white; border-left: 5px solid #2ecc71;
|
|
||||||
padding: 15px 20px; border-radius: 8px;
|
|
||||||
box-shadow: 0 5px 20px rgba(0,0,0,0.15);
|
|
||||||
min-width: 280px; overflow: hidden;
|
|
||||||
}
|
|
||||||
.toast-notification.error { border-left-color: #ff4757; }
|
|
||||||
|
|
||||||
.toast-content { display: flex; align-items: flex-start; gap: 12px; }
|
|
||||||
.check-icon { font-size: 1.2rem; }
|
|
||||||
.toast-text h4 { margin: 0 0 4px 0; font-size: 1rem; color: #2c3e50; }
|
|
||||||
.toast-text p { margin: 0; font-size: 0.85rem; color: #7f8c8d; }
|
|
||||||
|
|
||||||
/* 简单的倒计时进度条动画 */
|
|
||||||
.toast-progress {
|
|
||||||
position: absolute; bottom: 0; left: 0; height: 3px; background: #2ecc71;
|
|
||||||
width: 100%; animation: progress 3s linear forwards;
|
|
||||||
}
|
|
||||||
.toast-notification.error .toast-progress { background: #ff4757; }
|
|
||||||
|
|
||||||
@keyframes progress { from { width: 100%; } to { width: 0%; } }
|
|
||||||
|
|
||||||
/* Toast 进出动画 */
|
|
||||||
.toast-slide-enter-active, .toast-slide-leave-active { transition: all 0.3s ease; }
|
|
||||||
.toast-slide-enter-from, .toast-slide-leave-to { transform: translateX(50px); opacity: 0; }
|
|
||||||
|
|
||||||
.inline-error {
|
|
||||||
background: #ffeaea; color: #c0392b; padding: 10px; border-radius: 6px; margin-bottom: 20px; font-size: 0.9rem; font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- 动画与通用 --- */
|
|
||||||
@keyframes spin { 100% { transform: rotate(360deg); } }
|
|
||||||
.dashboard { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 20px; }
|
.dashboard { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 20px; }
|
||||||
.slide-up { animation: slideUp 0.4s ease-out forwards; }
|
.card { background: white; border-radius: 10px; padding: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.03); border: 1px solid #eaecf0; border-top: 3px solid #2ecc71; }
|
||||||
@keyframes slideUp { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
|
|
||||||
|
|
||||||
/* 卡片样式 (微调更紧凑) */
|
|
||||||
.card {
|
|
||||||
background: white; border-radius: 10px; padding: 20px;
|
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.03); border: 1px solid #eaecf0;
|
|
||||||
border-top: 3px solid #2ecc71;
|
|
||||||
}
|
|
||||||
.card:hover { box-shadow: 0 8px 20px rgba(0,0,0,0.06); transform: translateY(-2px); transition: all 0.2s; }
|
|
||||||
.card.danger { border-top-color: #ff4757; }
|
|
||||||
.card.summary { border-top-color: #3498db; grid-column: 1 / -1; }
|
|
||||||
|
|
||||||
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #f0f2f5; }
|
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #f0f2f5; }
|
||||||
h2 { font-size: 1.1rem; margin: 0; font-weight: 600; }
|
.card-header h3 { margin: 0; font-size: 1.1rem; font-weight: 600; }
|
||||||
|
|
||||||
|
.item-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; gap: 12px; }
|
||||||
|
.progress-label { display: flex; justify-content: space-between; font-size: 0.85rem; color: #636e72; gap: 10px; }
|
||||||
|
.text-ellipsis { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
||||||
|
.item-header strong { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; flex: 1; }
|
||||||
|
.status-text, .code-tag, .badge { flex-shrink: 0; white-space: nowrap; }
|
||||||
|
.event-id { font-weight: bold; color: #2c3e50; background: #e2e8f0; padding: 1px 5px; border-radius: 3px; font-size: 0.75rem; flex-shrink: 0; }
|
||||||
|
.event-source { font-size: 0.75rem; color: #7f8c8d; flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin: 0 5px; text-align: left; }
|
||||||
|
.event-time { font-size: 0.75rem; color: #95a5a6; flex-shrink: 0; white-space: nowrap; }
|
||||||
|
.badge { font-size: 0.75rem; padding: 3px 10px; border-radius: 12px; color: white; font-weight: 600; letter-spacing: 0.5px; white-space: nowrap; flex-shrink: 0; }
|
||||||
|
|
||||||
.grid-container-summary { display: grid; grid-template-columns: 1fr 1.5fr; gap: 25px; }
|
.grid-container-summary { display: grid; grid-template-columns: 1fr 1.5fr; gap: 25px; }
|
||||||
@media (max-width: 768px) { .grid-container-summary { grid-template-columns: 1fr; } }
|
@media (max-width: 900px) { .grid-container-summary { grid-template-columns: 1fr; } }
|
||||||
|
|
||||||
.basic-info, .usage-bars { display: flex; flex-direction: column; gap: 12px; }
|
.basic-info, .usage-bars { display: flex; flex-direction: column; gap: 12px; }
|
||||||
.usage-bars { padding-top: 5px; }
|
.usage-bars { padding-top: 5px; }
|
||||||
.usage-item { display: flex; flex-direction: column; gap: 6px; }
|
.usage-item { display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
|
||||||
.info-item { display: flex; flex-direction: column; }
|
.info-item { display: flex; flex-direction: column; }
|
||||||
.label { font-size: 0.75rem; color: #95a5a6; margin-bottom: 2px; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; }
|
.label { font-size: 0.75rem; color: #95a5a6; margin-bottom: 2px; text-transform: uppercase; font-weight: 600; }
|
||||||
.value { font-size: 1rem; font-weight: 500; color: #2c3e50; }
|
.value { font-size: 1rem; font-weight: 500; color: #2c3e50; }
|
||||||
.text-ellipsis { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
||||||
|
|
||||||
.progress-label { display: flex; justify-content: space-between; font-size: 0.85rem; color: #636e72; }
|
|
||||||
.progress-bar-large { width: 100%; height: 16px; background: #f1f3f5; border-radius: 8px; overflow: hidden; }
|
.progress-bar-large { width: 100%; height: 16px; background: #f1f3f5; border-radius: 8px; overflow: hidden; }
|
||||||
.fill { height: 100%; transition: width 0.6s ease; }
|
.fill { height: 100%; transition: width 0.6s ease; }
|
||||||
|
|
||||||
.list-container { display: flex; flex-direction: column; gap: 10px; }
|
.list-container { display: flex; flex-direction: column; gap: 10px; }
|
||||||
.list-item { background: #f8f9fa; padding: 10px 14px; border-radius: 6px; border: 1px solid #edf2f7; }
|
.list-item { background: #f8f9fa; padding: 10px 14px; border-radius: 6px; border: 1px solid #edf2f7; }
|
||||||
.list-item.error-item { background: #fff5f5; border-color: #fed7d7; }
|
.list-item.error-item { background: #fff5f5; border-color: #fed7d7; }
|
||||||
.list-item.warning-item { background: #fffaf0; border-color: #fce588; }
|
.list-item.warning-item { background: #fffaf0; border-color: #fce588; }
|
||||||
|
|
||||||
.item-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; }
|
|
||||||
.description { margin: 0; color: #636e72; font-size: 0.9rem; line-height: 1.4; }
|
.description { margin: 0; color: #636e72; font-size: 0.9rem; line-height: 1.4; }
|
||||||
.description.highlight { color: #d35400; font-weight: 500; }
|
.description.highlight { color: #d35400; font-weight: 500; }
|
||||||
.description.raw-message { margin-top: 6px; font-size: 0.8rem; color: #7f8c8d; font-family: Consolas, monospace; background: #fff; padding: 4px 8px; border-radius: 4px; border: 1px solid #eee; }
|
.description.raw-message { margin-top: 6px; font-size: 0.8rem; color: #7f8c8d; font-family: Consolas, monospace; background: #fff; padding: 4px 8px; border-radius: 4px; border: 1px solid #eee; word-break: break-all; }
|
||||||
|
|
||||||
.status-text { font-weight: 600; font-size: 0.9rem; }
|
|
||||||
.text-green { color: #27ae60; } .text-red { color: #c0392b; }
|
.text-green { color: #27ae60; } .text-red { color: #c0392b; }
|
||||||
|
.badge-red { background-color: #ff4757; } .badge-green { background-color: #2ed573; } .badge-blue { background-color: #3498db; } .badge-gray { background-color: #95a5a6; }
|
||||||
.badge { font-size: 0.7rem; padding: 2px 8px; border-radius: 10px; color: white; font-weight: 600; }
|
|
||||||
.badge-green { background-color: #2ed573; } .badge-red { background-color: #ff4757; } .badge-blue { background-color: #3498db; } .badge-gray { background-color: #95a5a6; }
|
|
||||||
.code-tag { background: #ff4757; color: white; padding: 1px 5px; border-radius: 3px; font-size: 0.75rem; font-weight: bold; }
|
.code-tag { background: #ff4757; color: white; padding: 1px 5px; border-radius: 3px; font-size: 0.75rem; font-weight: bold; }
|
||||||
|
|
||||||
.good-news { color: #27ae60; font-weight: 600; background: #eafaf1; padding: 12px; border-radius: 6px; border: 1px solid #d4efdf; text-align: center; font-size: 0.9rem; }
|
.good-news { color: #27ae60; font-weight: 600; background: #eafaf1; padding: 12px; border-radius: 6px; border: 1px solid #d4efdf; text-align: center; font-size: 0.9rem; }
|
||||||
|
.good-news.large { padding: 40px; font-size: 1.1rem; }
|
||||||
.tip { margin-top: 12px; font-size: 0.85rem; color: #d35400; background: #fff3e0; padding: 10px; border-radius: 6px; border: 1px solid #ffe0b2; }
|
.tip { margin-top: 12px; font-size: 0.85rem; color: #d35400; background: #fff3e0; padding: 10px; border-radius: 6px; border: 1px solid #ffe0b2; }
|
||||||
|
|
||||||
.progress-wrapper { margin-bottom: 12px; }
|
.progress-wrapper { margin-bottom: 12px; }
|
||||||
.progress-bar { width: 100%; height: 8px; background: #f1f3f5; border-radius: 4px; overflow: hidden; }
|
.progress-bar { width: 100%; height: 8px; background: #f1f3f5; border-radius: 4px; overflow: hidden; }
|
||||||
|
.content-box { padding: 5px 0; } .main-text { font-weight: 500; color: #2c3e50; margin-bottom: 8px; }
|
||||||
|
.card.danger { border-top-color: #ff4757; }
|
||||||
|
|
||||||
.event-id { font-weight: bold; color: #2c3e50; background: #e2e8f0; padding: 1px 5px; border-radius: 3px; margin-right: 8px; font-size: 0.75rem; }
|
.card.summary { border-top-color: #3498db; grid-column: 1 / -1; }
|
||||||
.event-time { margin-left: auto; font-size: 0.75rem; color: #95a5a6; }
|
|
||||||
.content-box { padding: 5px 0; }
|
.primary-btn { background: #2ecc71; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-weight: 600; display: flex; align-items: center; gap: 8px; transition: 0.2s; }
|
||||||
.main-text { font-weight: 500; color: #2c3e50; margin-bottom: 8px; }
|
.primary-btn:hover:not(:disabled) { background: #27ae60; } .primary-btn:disabled { background: #bdc3c7; cursor: not-allowed; }
|
||||||
|
.secondary-btn { background: white; border: 1px solid #dcdfe6; color: #606266; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 0.9rem; }
|
||||||
|
.secondary-btn:hover:not(:disabled) { border-color: #2ecc71; color: #2ecc71; }
|
||||||
|
.icon-btn { background: transparent; border: none; cursor: pointer; padding: 8px; color: #7f8c8d; font-weight: 600; }
|
||||||
|
.icon-btn:hover:not(:disabled) { color: #2ecc71; background: #eafaf1; border-radius: 6px; }
|
||||||
|
.toast-notification { position: fixed; top: 20px; right: 20px; background: white; border-left: 5px solid #2ecc71; padding: 15px 20px; border-radius: 8px; box-shadow: 0 5px 20px rgba(0,0,0,0.15); z-index: 100; min-width: 250px; }
|
||||||
|
.toast-notification.error { border-left-color: #ff4757; }
|
||||||
|
.toast-content { display: flex; gap: 12px; } .toast-text h4 { margin: 0 0 4px; font-size: 1rem; } .toast-text p { margin: 0; color: #7f8c8d; font-size: 0.85rem; }
|
||||||
|
.toast-slide-enter-active, .toast-slide-leave-active { transition: all 0.3s; } .toast-slide-enter-from, .toast-slide-leave-to { transform: translateX(100%); opacity: 0; }
|
||||||
|
@keyframes spin { 100% { transform: rotate(360deg); } }
|
||||||
|
.fade-in { animation: fadeIn 0.4s ease-out; } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||||||
|
.slide-up { animation: slideUp 0.4s ease-out forwards; } @keyframes slideUp { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
|
.bsod-layout { display: grid; grid-template-columns: 300px 1fr; gap: 20px; height: calc(100vh - 120px); }
|
||||||
|
.bsod-list-panel { background: white; border-radius: 10px; border: 1px solid #eaecf0; overflow-y: auto; }
|
||||||
|
.bsod-detail-panel { background: white; border-radius: 10px; border: 1px solid #eaecf0; padding: 25px; overflow-y: auto; position: relative; }
|
||||||
|
.file-item { padding: 15px; border-bottom: 1px solid #f0f2f5; cursor: pointer; display: flex; gap: 12px; transition: background 0.2s; }
|
||||||
|
.file-item:hover { background: #f8f9fa; }
|
||||||
|
.file-item.active { background: #eafaf1; border-left: 4px solid #2ecc71; }
|
||||||
|
/* 给导入的文件加一点特殊样式区分(可选) */
|
||||||
|
.file-item.imported .file-name { color: #3498db; }
|
||||||
|
|
||||||
|
.file-icon { font-size: 1.5rem; } .file-name { font-weight: 600; font-size: 0.9rem; color: #2c3e50; margin-bottom: 4px; } .file-meta { font-size: 0.8rem; color: #95a5a6; }
|
||||||
|
.empty-state { padding: 40px; text-align: center; color: #95a5a6; font-size: 0.9rem; line-height: 1.6; }
|
||||||
|
.analyzing-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: #7f8c8d; }
|
||||||
|
.spinner { width: 40px; height: 40px; border: 4px solid #f3f3f3; border-top: 4px solid #2ecc71; border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 15px; }
|
||||||
|
.empty-detail { display: flex; align-items: center; justify-content: center; height: 100%; color: #95a5a6; font-size: 1.1rem; }
|
||||||
|
.result-header { border-bottom: 1px solid #eee; padding-bottom: 15px; margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.result-header h3 { margin: 0; color: #c0392b; font-size: 1.4rem; } .code-pill { background: #2c3e50; color: white; padding: 4px 10px; border-radius: 4px; font-family: monospace; font-weight: bold; }
|
||||||
|
.card-section { margin-bottom: 25px; } .card-section h4 { margin: 0 0 10px 0; font-size: 1rem; color: #7f8c8d; text-transform: uppercase; letter-spacing: 0.5px; border-left: 3px solid #2ecc71; padding-left: 10px; }
|
||||||
|
.human-text { font-size: 1.1rem; color: #2c3e50; line-height: 1.6; margin-bottom: 10px; }
|
||||||
|
.recommendation { background: #eafaf1; color: #27ae60; padding: 15px; border-radius: 8px; font-weight: 500; }
|
||||||
|
.detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; } .d-item { background: #f8f9fa; padding: 10px; border-radius: 6px; display: flex; flex-direction: column; } .d-label { font-size: 0.75rem; color: #95a5a6; margin-bottom: 4px; }
|
||||||
|
.message-box { padding: 10px 15px; border-radius: 6px; margin-bottom: 15px; font-size: 0.9rem; font-weight: 500; }
|
||||||
|
.message-box.success { background-color: #eafaf1; color: #27ae60; border: 1px solid #d4efdf; }
|
||||||
|
.message-box.error { background-color: #ffeaea; color: #c0392b; border: 1px solid #ffcccc; }
|
||||||
|
|
||||||
|
/* 新增日志视图样式 */
|
||||||
|
.logs-view { display: flex; flex-direction: column; height: 100%; }
|
||||||
|
.logs-container { flex: 1; overflow-y: auto; padding-right: 5px; }
|
||||||
|
.log-list-full { display: flex; flex-direction: column; gap: 15px; }
|
||||||
|
.log-card {
|
||||||
|
background: white; border-radius: 8px; padding: 15px 20px;
|
||||||
|
border: 1px solid #eaecf0; box-shadow: 0 2px 6px rgba(0,0,0,0.02);
|
||||||
|
}
|
||||||
|
.log-card.warning-item { border-left: 4px solid #fce588; }
|
||||||
|
.log-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
|
||||||
|
.log-meta { display: flex; align-items: center; gap: 10px; }
|
||||||
</style>
|
</style>
|
||||||
@@ -1 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 496 B |