This commit is contained in:
Julian Freeman
2025-11-26 09:04:52 -04:00
parent 4fc501f941
commit 19b18d091a
4 changed files with 886 additions and 486 deletions

348
src-tauri/Cargo.lock generated
View File

@@ -280,6 +280,24 @@ dependencies = [
"piper", "piper",
] ]
[[package]]
name = "breakpad-symbols"
version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b002797414ffc34425bdf5b21a9e50d102013292625749eeba0a59923176ab05"
dependencies = [
"async-trait",
"cachemap2",
"circular",
"debugid",
"futures-util",
"minidump-common",
"nom",
"range-map",
"thiserror 1.0.69",
"tracing",
]
[[package]] [[package]]
name = "brotli" name = "brotli"
version = "8.0.2" version = "8.0.2"
@@ -328,6 +346,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "cachemap2"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7bba2f68a9fefca870fed897de7c655f9d5c1eaf1cd9517db96c9a3861f648b"
[[package]] [[package]]
name = "cairo-rs" name = "cairo-rs"
version = "0.18.5" version = "0.18.5"
@@ -458,6 +482,12 @@ dependencies = [
"windows-link 0.2.1", "windows-link 0.2.1",
] ]
[[package]]
name = "circular"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fc239e0f6cb375d2402d48afb92f76f5404fd1df208a41930ec81eda078bea"
[[package]] [[package]]
name = "combine" name = "combine"
version = "4.6.7" version = "4.6.7"
@@ -585,6 +615,31 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crossterm"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
dependencies = [
"bitflags 2.10.0",
"crossterm_winapi",
"libc",
"mio 0.8.11",
"parking_lot",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm_winapi"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.7" version = "0.1.7"
@@ -667,6 +722,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 +897,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 +2049,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 +2073,82 @@ 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]]
name = "minidump-processor"
version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76b49bde7c0ae9a7142c540c27c7fc29db2288fd9614f11a9ce57badeb74af43"
dependencies = [
"async-trait",
"breakpad-symbols",
"debugid",
"futures-util",
"memmap2",
"minidump",
"minidump-common",
"minidump-unwind",
"scroll",
"serde",
"serde_json",
"thiserror 1.0.69",
"tracing",
"yaxpeax-x86",
]
[[package]]
name = "minidump-unwind"
version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63aef4cd2e018881680b152296ae28e674242823faa1767b417b6669a1896cdc"
dependencies = [
"async-trait",
"breakpad-symbols",
"minidump",
"minidump-common",
"scroll",
"tracing",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.9" version = "0.8.9"
@@ -2001,6 +2159,18 @@ dependencies = [
"simd-adler32", "simd-adler32",
] ]
[[package]]
name = "mio"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [
"libc",
"log",
"wasi 0.11.1+wasi-snapshot-preview1",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.1.0" version = "1.1.0"
@@ -2088,6 +2258,16 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]] [[package]]
name = "ntapi" name = "ntapi"
version = "0.4.1" version = "0.4.1"
@@ -2103,6 +2283,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 +3095,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 +3334,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"
@@ -3349,6 +3569,27 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-mio"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
dependencies = [
"libc",
"mio 0.8.11",
"signal-hook",
]
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.7" version = "1.4.7"
@@ -3388,6 +3629,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 +3827,8 @@ name = "system-doctor"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"minidump",
"minidump-processor",
"serde", "serde",
"serde_json", "serde_json",
"sysinfo", "sysinfo",
@@ -4009,7 +4263,7 @@ checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
dependencies = [ dependencies = [
"bytes", "bytes",
"libc", "libc",
"mio", "mio 1.1.0",
"parking_lot", "parking_lot",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
@@ -4189,6 +4443,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",
@@ -4876,6 +5131,15 @@ dependencies = [
"windows-targets 0.42.2", "windows-targets 0.42.2",
] ]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.59.0" version = "0.59.0"
@@ -4918,6 +5182,21 @@ dependencies = [
"windows_x86_64_msvc 0.42.2", "windows_x86_64_msvc 0.42.2",
] ]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.52.6" version = "0.52.6"
@@ -4975,6 +5254,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]] [[package]]
name = "windows_aarch64_gnullvm" name = "windows_aarch64_gnullvm"
version = "0.52.6" version = "0.52.6"
@@ -4993,6 +5278,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.52.6" version = "0.52.6"
@@ -5011,6 +5302,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.52.6" version = "0.52.6"
@@ -5041,6 +5338,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.52.6" version = "0.52.6"
@@ -5059,6 +5362,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.52.6" version = "0.52.6"
@@ -5077,6 +5386,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.52.6" version = "0.52.6"
@@ -5095,6 +5410,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.52.6" version = "0.52.6"
@@ -5228,6 +5549,31 @@ dependencies = [
"pkg-config", "pkg-config",
] ]
[[package]]
name = "yaxpeax-arch"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f005c964432a1f9ee04598e094a3eb5f7568f6b33e89a2762d7bef6fbe8b255"
dependencies = [
"crossterm",
"num-traits",
"serde",
"serde_derive",
]
[[package]]
name = "yaxpeax-x86"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9107477944697db42c41326f82d4c65b769b32512cdad1e086f36f0e0f25ff45"
dependencies = [
"cfg-if",
"num-traits",
"serde",
"serde_derive",
"yaxpeax-arch",
]
[[package]] [[package]]
name = "yoke" name = "yoke"
version = "0.8.1" version = "0.8.1"

View File

@@ -26,6 +26,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"
minidump-processor = "0.19"
[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

View File

@@ -8,13 +8,15 @@ 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 tauri::Emitter; use tauri::Emitter;
use chrono::{Duration, FixedOffset, Local, NaiveDate, TimeZone}; use chrono::{Duration, FixedOffset, Local, NaiveDate, TimeZone, DateTime};
// [修改] 只引入 minidump 基础库,移除 processor 依赖以避免 API 冲突
use minidump::{Minidump, MinidumpException, MinidumpSystemInfo};
// --- 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,14 +129,146 @@ fn format_wmi_time(wmi_str: &str) -> String {
} }
} }
// --- 核心逻辑:分步流式传输 --- // --- 新增功能:翻译蓝屏代码为人话 (手动映射常见代码) ---
fn translate_bugcheck_u32(code: u32) -> (String, String) {
// 这些是 Windows 最常见的 BSOD 代码
match code {
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(),
),
0x0000001A => (
"MEMORY_MANAGEMENT (0x1A)".to_string(),
"严重的内存管理错误。高度怀疑内存条物理故障。建议立即运行 Windows 内存诊断工具,或者尝试拔插内存条。"
.to_string(),
),
0x000000EF => (
"CRITICAL_PROCESS_DIED (0xEF)".to_string(),
"Windows 核心进程意外终止。通常是系统文件损坏或硬盘故障。建议运行 'sfc /scannow' 修复系统,并检查硬盘健康度。"
.to_string(),
),
0x00000124 => (
"WHEA_UNCORRECTABLE_ERROR (0x124)".to_string(),
"硬件发生无法纠正的物理错误。这是纯硬件故障。通常是 CPU 电压不足、超频失败、过热,或者主板/PCIe设备如 NVMe 硬盘)故障。"
.to_string(),
),
0x00000116 => (
"VIDEO_TDR_FAILURE (0x116)".to_string(),
"显卡响应超时。显卡驱动崩溃或显卡过热。如果你在玩游戏,可能是显卡超频不稳定或散热硅脂干了。"
.to_string(),
),
0x00000050 => (
"PAGE_FAULT_IN_NONPAGED_AREA (0x50)".to_string(),
"试图访问无效的内存地址。可能是内存条故障,或者是防病毒软件/驱动程序冲突。建议检查内存。"
.to_string(),
),
0x0000000A => (
"IRQL_NOT_LESS_OR_EQUAL (0x0A)".to_string(),
"驱动程序使用了不正确的内存地址。通常由有缺陷的驱动程序或硬件兼容性问题引起。"
.to_string(),
),
0x0000003B => (
"SYSTEM_SERVICE_EXCEPTION (0x3B)".to_string(),
"系统服务执行异常。通常与图形驱动程序或过时的系统文件有关。"
.to_string(),
),
0x00000133 => (
"DPC_WATCHDOG_VIOLATION (0x133)".to_string(),
"DPC 看门狗超时。通常是 SSD 固件过旧或无线网卡驱动冲突导致系统卡死时间过长。"
.to_string(),
),
_ => (
format!("未知错误代码: 0x{:X}", code),
"请尝试在搜索引擎中搜索此错误代码。通用建议:更新驱动、检查内存、扫描病毒。".to_string(),
),
}
}
// --- 命令:列出 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 {
// [修复] 使用 .as_ref() 防止 metadata 所有权被移动
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());
// [修复] 使用 .as_ref() 再次访问 metadata
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);
// 1. 读取文件 (使用基础 minidump 库)
let dump = Minidump::read_path(path).map_err(|e| format!("无法读取文件: {}", e))?;
// 2. 直接获取异常流 (Exception Stream)
let exception_stream = dump.get_stream::<MinidumpException>()
.map_err(|_| "无法找到异常信息流 (No Exception Stream)".to_string())?;
// [修复] 使用 .raw 访问内部原始结构
let exception_code = exception_stream.raw.exception_record.exception_code;
let exception_address = exception_stream.raw.exception_record.exception_address;
// 3. 尝试获取系统信息 (OS Version)
let sys_info_str = match dump.get_stream::<MinidumpSystemInfo>() {
// [修复] 使用 .raw 访问 build_number
Ok(info) => format!("Windows Build {}", info.raw.build_number),
Err(_) => "Unknown OS".to_string(),
};
// 4. 翻译
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: "根据错误代码自动匹配的分析结果。".to_string(),
recommendation: recommend,
})
}
// --- 现有命令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. 硬件概览 (最快) // 1. 硬件概览
{ {
let mut sys = System::new(); let mut sys = System::new();
sys.refresh_memory(); sys.refresh_memory();
@@ -180,7 +300,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();
@@ -209,15 +328,12 @@ async fn run_diagnosis(window: tauri::Window) -> Result<(), String> {
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. 存储设备 (较快) // 2. 存储设备
{ {
let mut storage = Vec::new(); let mut storage = Vec::new();
if let Some(con) = &wmi_con { if let Some(con) = &wmi_con {
@@ -242,7 +358,7 @@ async fn run_diagnosis(window: tauri::Window) -> Result<(), String> {
let _ = window.emit("report-storage", storage); let _ = window.emit("report-storage", storage);
} }
// 3. 驱动 (快) // 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 {
@@ -267,7 +383,7 @@ async fn run_diagnosis(window: tauri::Window) -> Result<(), String> {
let _ = window.emit("report-drivers", driver_issues); let _ = window.emit("report-drivers", driver_issues);
} }
// 4. Minidump (快) // 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") {
@@ -283,7 +399,7 @@ async fn run_diagnosis(window: tauri::Window) -> Result<(), String> {
let _ = window.emit("report-minidumps", minidump); let _ = window.emit("report-minidumps", minidump);
} }
// 5. 电池 (快) // 5. 电池
{ {
let mut battery_info = None; let mut battery_info = None;
if let Some(con) = &wmi_con { if let Some(con) = &wmi_con {
@@ -309,12 +425,10 @@ async fn run_diagnosis(window: tauri::Window) -> Result<(), String> {
} }
} }
} }
// 即使是 None 也要发,以便前端知道检查完成了
// 这里为了简化,直接发 Option
let _ = window.emit("report-battery", battery_info); let _ = window.emit("report-battery", battery_info);
} }
// 6. 日志 (最慢,放在最后) // 6. 日志
{ {
let mut events = Vec::new(); let mut events = Vec::new();
if let Some(con) = &wmi_con { if let Some(con) = &wmi_con {
@@ -366,7 +480,7 @@ async fn run_diagnosis(window: tauri::Window) -> Result<(), String> {
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])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }

View File

@@ -1,235 +1,254 @@
<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' }"
<div class="toolbar"> @click="currentTab = 'overview'"
<div class="left-actions"> >
<button @click="startScan" :disabled="loading" class="primary-btn" :class="{ 'scanning': loading }"> <span class="nav-icon">📊</span> 静态概览
<span v-if="loading" class="icon-spin">🔄</span>
<span v-if="loading">正在排查...</span>
<template v-else>
<span class="btn-icon">🔍</span> 开始全面体检
</template>
</button> </button>
<button
class="nav-item"
:class="{ active: currentTab === 'bsod' }"
@click="currentTab = 'bsod'"
>
<span class="nav-icon"></span> 蓝屏分析
</button>
</nav>
<div class="sidebar-footer">
<span class="version">Pro v1.2</span>
</div>
</aside>
<!-- 右侧主内容区 -->
<main class="main-content">
<!-- 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 }">
<span v-if="loading" class="icon-spin">🔄</span>
<span v-else>🔍 全面体检</span>
</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>
<!-- 状态提示 -->
<div v-if="errorMsg" class="message-box error"> {{ errorMsg }}</div>
<div v-if="scanFinished" class="message-box success"> 扫描完成</div>
<input type="file" ref="fileInput" @change="handleFileImport" accept=".json" style="display: none" />
<!-- 卡片展示区 -->
<div v-if="report.hardware" class="dashboard fade-in">
<!-- 硬件卡片 -->
<div class="card summary slide-up">
<div class="card-header"><h3>🖥 硬件与资源</h3></div>
<div class="grid-container-summary">
<div class="basic-info">
<div class="info-item"><span class="label">CPU</span><span class="value text-ellipsis" :title="report.hardware.cpu_name">{{ report.hardware.cpu_name }}</span></div>
<div class="info-item"><span class="label">System</span><span class="value text-ellipsis">{{ report.hardware.sys_vendor }} {{ report.hardware.sys_product }}</span></div>
<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 class="usage-bars">
<div class="usage-item">
<div class="progress-label">
<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>
</div>
<div class="progress-bar-large"><div class="fill" :style="getProgressStyle(report.hardware.c_drive_used_gb, report.hardware.c_drive_total_gb)"></div></div>
</div>
<div class="usage-item">
<div class="progress-label">
<span 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>
</div>
<div class="progress-bar-large"><div class="fill" :style="getProgressStyle(report.hardware.memory_used_gb, report.hardware.memory_total_gb)"></div></div>
</div>
</div>
</div>
</div>
<!-- 驱动状态卡片 -->
<div v-if="report.drivers" class="card slide-up" :class="{ danger: report.drivers.length > 0, success: report.drivers.length === 0 }">
<div class="card-header"><h3>🔌 驱动状态</h3><span class="badge" :class="report.drivers.length > 0 ? 'badge-red' : 'badge-green'">{{ report.drivers.length > 0 ? '异常' : '正常' }}</span></div>
<div v-if="report.drivers.length > 0" class="list-container">
<div v-for="(drv, idx) in report.drivers" :key="idx" class="list-item error-item">
<div class="item-header">
<strong class="text-ellipsis" :title="drv.device_name">{{ drv.device_name }}</strong>
<span class="code-tag">Code {{ drv.error_code }}</span>
</div>
<p class="description">{{ drv.description }}</p>
</div>
</div>
<div v-else class="good-news">设备管理器无异常</div>
</div>
<!-- 电池卡片 -->
<div v-if="report.battery" class="card slide-up" :class="{ danger: report.battery.health_percentage < 60 }">
<div class="card-header"><h3>🔋 电池健康</h3><span class="badge badge-blue">{{ report.battery.health_percentage }}%</span></div>
<div class="content-box">
<div class="progress-bar"><div class="fill" :style="{ width: report.battery.health_percentage + '%', background: getBatteryColor(report.battery.health_percentage) }"></div></div>
<p class="description mt-2">{{ report.battery.explanation }}</p>
</div>
</div>
<!-- 硬盘卡片 -->
<div v-if="report.storage" class="card slide-up" :class="{ danger: hasStorageDanger }">
<div class="card-header"><h3>💾 硬盘 S.M.A.R.T</h3></div>
<div class="list-container">
<div v-for="(disk, index) in report.storage" :key="index" class="list-item">
<div class="item-header">
<strong class="text-ellipsis" :title="disk.model">{{ disk.model }}</strong>
<span class="status-text" :class="disk.is_danger ? 'text-red' : 'text-green'">{{ disk.health_status }}</span>
</div>
<p class="description">{{ disk.human_explanation }}</p>
</div>
</div>
</div>
<!-- 日志卡片 -->
<div v-if="report.events" class="card slide-up" :class="{ danger: report.events.length > 0 }">
<div class="card-header"><h3> 关键日志</h3></div>
<div v-if="report.events.length > 0" class="list-container">
<div v-for="(evt, idx) in report.events" :key="idx" class="list-item warning-item">
<div class="item-header">
<span class="event-id">ID: {{ evt.event_id }}</span>
<span class="event-source" :title="evt.source">{{ evt.source }}</span>
<span class="event-time">{{ formatTime(evt.time_generated) }}</span>
</div>
<p class="description highlight">{{ evt.analysis_hint }}</p>
<p v-if="evt.message" class="description raw-message">{{ evt.message }}</p>
</div>
</div>
<div v-else class="good-news">无致命错误日志</div>
</div>
</div>
</div> </div>
<div class="right-actions"> <!-- TAB 2: 蓝屏分析 -->
<button class="icon-btn" @click="triggerImport" :disabled="loading" title="导入报告"> <div v-if="currentTab === 'bsod'" class="tab-view bsod-view">
📂 导入 <div class="view-header">
</button> <h2>蓝屏死机分析 (Minidump)</h2>
<button class="icon-btn" @click="exportReport" :disabled="!isReportValid || loading" title="导出报告"> <button class="secondary-btn" @click="loadMinidumps" :disabled="bsodLoading">🔄 刷新列表</button>
💾 导出 </div>
</button>
<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 }"
@click="analyzeBsod(file)"
>
<div class="file-icon">📄</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">
👈 请从左侧选择一个蓝屏文件开始分析
</div>
</div>
</div>
</div> </div>
</div>
<!-- 隐藏的文件输入框 --> </main>
<input type="file" ref="fileInput" @change="handleFileImport" accept=".json" style="display: none" />
<!-- 错误提示 (保留在原本位置或顶部) --> <!-- Toast 通知 -->
<div v-if="errorMsg" class="inline-error">
{{ errorMsg }}
</div>
<!-- 右上角 Toast 通知 (通用化) -->
<transition name="toast-slide"> <transition name="toast-slide">
<div v-if="toast.show" class="toast-notification" :class="toast.type"> <div v-if="toast.show" class="toast-notification" :class="toast.type">
<div class="toast-content"> <div class="toast-content">
<span class="check-icon">{{ toast.type === 'error' ? '❌' : '✅' }}</span> <span class="check-icon">{{ toast.type === 'error' ? '❌' : '✅' }}</span>
<div class="toast-text"> <div class="toast-text"><h4>{{ toast.title }}</h4><p>{{ toast.message }}</p></div>
<h4>{{ toast.title }}</h4>
<p>{{ toast.message }}</p>
</div>
</div> </div>
<div class="toast-progress"></div> <div class="toast-progress"></div>
</div> </div>
</transition> </transition>
<!-- 结果显示区域 -->
<div v-if="report.hardware" class="dashboard fade-in">
<!-- 1. 硬件概览 -->
<div v-if="report.hardware" class="card summary slide-up">
<div class="card-header">
<h2>🖥 硬件概览与资源占用</h2>
</div>
<div class="grid-container-summary">
<div class="basic-info">
<div class="info-item">
<span class="label">CPU 型号</span>
<span class="value text-ellipsis" :title="report.hardware.cpu_name">{{ report.hardware.cpu_name }}</span>
</div>
<div class="info-item">
<span class="label">整机型号</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-item">
<div class="progress-label">
<span>C盘空间 (已用 {{ report.hardware.c_drive_used_gb }}GB / 总计 {{ report.hardware.c_drive_total_gb }}GB)</span>
<strong>{{ calculatePercent(report.hardware.c_drive_used_gb, report.hardware.c_drive_total_gb) }}%</strong>
</div>
<div class="progress-bar-large">
<div class="fill" :style="getProgressStyle(report.hardware.c_drive_used_gb, report.hardware.c_drive_total_gb)"></div>
</div>
</div>
<div class="usage-item">
<div class="progress-label">
<span>内存占用 (已用 {{ report.hardware.memory_used_gb }}GB / 总计 {{ report.hardware.memory_total_gb }}GB)</span>
<strong>{{ calculatePercent(report.hardware.memory_used_gb, report.hardware.memory_total_gb) }}%</strong>
</div>
<div class="progress-bar-large">
<div class="fill" :style="getProgressStyle(report.hardware.memory_used_gb, report.hardware.memory_total_gb)"></div>
</div>
</div>
</div>
</div>
</div>
<!-- 2. 驱动状态 -->
<div v-if="report.drivers" class="card slide-up" :class="{ danger: report.drivers.length > 0, success: report.drivers.length === 0 }">
<div class="card-header">
<h2>🔌 驱动与设备状态</h2>
<span class="badge" :class="report.drivers.length > 0 ? 'badge-red' : 'badge-green'">
{{ report.drivers.length > 0 ? '发现异常' : '正常' }}
</span>
</div>
<div v-if="report.drivers.length === 0" class="good-news"> 设备管理器中未发现带有黄色感叹号或错误的设备</div>
<div v-else class="list-container">
<div v-for="(drv, idx) in report.drivers" :key="idx" class="list-item error-item">
<div class="item-header">
<strong>{{ drv.device_name }}</strong>
<span class="code-tag">Code {{ drv.error_code }}</span>
</div>
<p class="description">{{ drv.description }}</p>
</div>
</div>
</div>
<!-- 3. 电池健康 -->
<div v-if="report.battery" class="card slide-up" :class="{ danger: report.battery.health_percentage < 60 }">
<div class="card-header">
<h2>🔋 电池健康度 (寿命)</h2>
<span class="badge" :class="report.battery.is_ac_connected ? 'badge-blue' : 'badge-gray'">
{{ report.battery.is_ac_connected ? '⚡ 已连接电源' : '🔋 使用电池中' }}
</span>
</div>
<div class="content-box">
<div class="progress-wrapper">
<div class="progress-label">
<span>当前健康度 (相对于设计容量)</span>
<strong>{{ report.battery.health_percentage }}%</strong>
</div>
<div class="progress-bar">
<div class="fill" :style="{ width: report.battery.health_percentage + '%', background: getBatteryColor(report.battery.health_percentage) }"></div>
</div>
</div>
<p class="explanation">{{ report.battery.explanation }}</p>
</div>
</div>
<!-- 4. 硬盘健康 -->
<div v-if="report.storage" class="card slide-up" :class="{ danger: hasStorageDanger }">
<div class="card-header">
<h2>💾 硬盘健康度 (S.M.A.R.T)</h2>
</div>
<div class="list-container">
<div v-for="(disk, index) in report.storage" :key="index" class="list-item">
<div class="item-header">
<strong>{{ disk.model }}</strong>
<span class="status-text" :class="disk.is_danger ? 'text-red' : 'text-green'">
{{ disk.health_status }}
</span>
</div>
<p class="description">{{ disk.human_explanation }}</p>
</div>
</div>
</div>
<!-- 5. 蓝屏分析 -->
<div v-if="report.minidumps" class="card slide-up" :class="{ danger: report.minidumps.found, success: !report.minidumps.found }">
<div class="card-header">
<h2> 蓝屏死机记录 (BSOD)</h2>
</div>
<div class="content-box">
<p class="main-text">{{ report.minidumps.explanation }}</p>
<p v-if="report.minidumps.found" class="tip">💡 建议使用 BlueScreenView WinDbg 工具打开 C:\Windows\Minidump 文件夹下的 .dmp 文件进行深入分析</p>
</div>
</div>
<!-- 6. 关键日志 -->
<div v-if="report.events" class="card slide-up" :class="{ danger: report.events.length > 0 }">
<div class="card-header">
<h2> 关键供电与硬件日志 (Event Log)</h2>
</div>
<div v-if="report.events.length === 0" class="good-news"> 近期日志中未发现 Kernel-Power(41) WHEA(18/19) 等致命错误</div>
<div v-else class="list-container">
<div v-for="(evt, idx) in report.events" :key="idx" class="list-item warning-item">
<div class="item-header">
<span class="event-id">ID: {{ evt.event_id }}</span>
<span class="event-source">{{ evt.source }}</span>
<span class="event-time">{{ evt.time_generated }}</span>
</div>
<p class="description highlight">{{ evt.analysis_hint }}</p>
<p v-if="evt.message" class="description raw-message">{{ evt.message }}</p>
</div>
</div>
</div>
</div>
</div> </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'
});
// 概览相关
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('');
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 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 +258,191 @@ 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); }
// --- 导出/导入 --- async function exportReport() { /* ... */ if (!isReportValid.value) return; try { const blob = new Blob([JSON.stringify(report.value, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `SystemDoctor_Report_${new Date().toISOString().slice(0, 19).replace(/[:T]/g, "-")}.json`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); triggerToast('导出成功', '文件已保存'); } catch (err) { triggerToast('导出失败', err.message, 'error'); } }
async function exportReport() {
if (!isReportValid.value) return;
const fileName = `SystemDoctor_Report_${new Date().toISOString().slice(0, 19).replace(/[:T]/g, "-")}.json`;
const content = JSON.stringify(report.value, null, 2);
// 尝试使用现代文件系统 API (显示保存对话框)
if (window.showSaveFilePicker) {
try {
const handle = await window.showSaveFilePicker({
suggestedName: fileName,
types: [{
description: 'JSON Report',
accept: { 'application/json': ['.json'] },
}],
});
const writable = await handle.createWritable();
await writable.write(content);
await writable.close();
triggerToast('导出成功', '文件已保存到指定位置', 'success');
return;
} catch (err) {
// 用户取消不报错,其他错误降级处理
if (err.name !== 'AbortError') {
console.warn('Native save dialog failed, falling back to download link', err);
} else {
return; // 用户取消
}
}
}
// 降级方案:创建链接直接下载 (默认下载文件夹)
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);
triggerToast('导出成功', '文件已保存到默认下载目录', 'success');
} catch (err) {
errorMsg.value = "导出失败: " + err.message;
triggerToast('导出失败', err.message, 'error');
}
}
function triggerImport() { fileInput.value.click(); } function triggerImport() { fileInput.value.click(); }
function handleFileImport(event) { 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; scanFinished.value = false; triggerToast('导入成功', '已加载历史报告'); } else { triggerToast('导入失败', '文件格式错误', 'error'); } } catch (err) { triggerToast('解析失败', err.message, 'error'); } }; reader.readAsText(file); event.target.value = ''; }
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;
errorMsg.value = '';
triggerToast('导入成功', '已加载历史报告数据', 'success');
} else {
errorMsg.value = "无效的报告文件。";
triggerToast('导入失败', '文件格式不正确', 'error');
}
} catch (err) {
errorMsg.value = "解析失败: " + err.message;
triggerToast('导入失败', err.message, 'error');
}
};
reader.readAsText(file);
event.target.value = '';
}
// --- 扫描逻辑 ---
async function startScan() { async function startScan() {
report.value = emptyReport(); report.value = emptyReport(); errorMsg.value = ''; scanFinished.value = false; loading.value = true;
errorMsg.value = '';
loading.value = true;
if (unlistenFns.length > 0) { for (const fn of unlistenFns) fn(); unlistenFns = []; } if (unlistenFns.length > 0) { for (const fn of unlistenFns) fn(); unlistenFns = []; }
try { try {
const l1 = await listen('report-hardware', (e) => report.value.hardware = e.payload); unlistenFns.push(
const l2 = await listen('report-storage', (e) => report.value.storage = e.payload); await listen('report-hardware', (e) => report.value.hardware = e.payload),
const l3 = await listen('report-drivers', (e) => report.value.drivers = e.payload); await listen('report-storage', (e) => report.value.storage = e.payload),
const l4 = await listen('report-minidumps', (e) => report.value.minidumps = e.payload); await listen('report-drivers', (e) => report.value.drivers = e.payload),
const l5 = await listen('report-battery', (e) => report.value.battery = e.payload); await listen('report-minidumps', (e) => report.value.minidumps = e.payload),
const l6 = await listen('report-events', (e) => report.value.events = e.payload); await listen('report-battery', (e) => report.value.battery = e.payload),
const l7 = await listen('diagnosis-finished', () => { await listen('report-events', (e) => report.value.events = e.payload),
loading.value = false; await listen('diagnosis-finished', () => { loading.value = false; scanFinished.value = true; triggerToast('扫描完成', '体检已结束', 'success'); })
triggerToast('扫描已完成', '所有硬件与日志检查完毕', 'success'); );
});
unlistenFns.push(l1, l2, l3, l4, l5, l6, l7);
await invoke('run_diagnosis'); await invoke('run_diagnosis');
} catch (e) { } catch (e) { loading.value = false; errorMsg.value = e; }
console.error(e);
errorMsg.value = "启动扫描失败: " + e;
loading.value = false;
triggerToast('扫描失败', '无法启动后台诊断程序', 'error');
}
} }
// --- Toast 逻辑 --- async function loadMinidumps() { bsodLoading.value = true; try { bsodList.value = await invoke('list_minidumps'); } catch (e) { triggerToast('加载失败', e, 'error'); } finally { bsodLoading.value = false; } }
function triggerToast(title, message, type = 'success') { async function analyzeBsod(file) { if (bsodAnalyzing.value) return; selectedBsod.value = file; bsodResult.value = null; bsodAnalyzing.value = true; try { bsodResult.value = await invoke('analyze_minidump', { filepath: file.path }); } catch (e) { triggerToast('分析失败', e, 'error'); } finally { bsodAnalyzing.value = false; } }
toast.title = title;
toast.message = message;
toast.type = type;
toast.show = true;
if (toastTimer) clearTimeout(toastTimer); watch(currentTab, (newVal) => { if (newVal === 'bsod' && bsodList.value.length === 0) loadMinidumps(); });
toastTimer = setTimeout(() => {
toast.show = false;
}, 3000);
}
onUnmounted(() => { 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); }
for (const fn of unlistenFns) fn(); onUnmounted(() => { for (const fn of unlistenFns) fn(); if (toastTimer) clearTimeout(toastTimer); });
if (toastTimer) clearTimeout(toastTimer);
});
</script> </script>
<style>
body {
margin: 0; padding: 0; background-color: #f4f6f9; /* 更柔和的背景 */
overflow-y: scroll;
}
</style>
<style scoped> <style scoped>
/* 容器调整 */ /* ... 布局样式保持不变 ... */
.container { .app-layout { display: flex; height: 100vh; width: 100vw; overflow: hidden; background-color: #f4f6f9; }
max-width: 1200px; margin: 0 auto; padding: 20px 30px; box-sizing: border-box; .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; }
font-family: 'Segoe UI', system-ui, sans-serif; color: #2c3e50; .brand { padding: 0 20px 30px; display: flex; align-items: center; gap: 12px; }
} .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-header { display: flex; justify-content: space-between; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #f0f2f5; }
.card-header h3 { margin: 0; font-size: 1.1rem; font-weight: 600; }
/* 卡片样式 (微调更紧凑) */ /* --- [修复] 关键 CSS 修改 --- */
.card {
background: white; border-radius: 10px; padding: 20px; /* 1. item-header 增加 gap */
box-shadow: 0 2px 10px rgba(0,0,0,0.03); border: 1px solid #eaecf0; .item-header {
border-top: 3px solid #2ecc71; display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
gap: 12px; /* [新增] 强制间距,防止内容对撞 */
} }
.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; } /* 2. 进度条标签 增加 gap */
h2 { font-size: 1.1rem; margin: 0; font-weight: 600; } .progress-label {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
color: #636e72;
gap: 10px; /* [新增] 强制间距 */
}
/* 3. 长文本处理:设备名、日志来源等 */
.text-ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0; /* Flexbox item 必须有这个才能正确收缩 */
}
/* 让 item-header 中的 strong 也能截断 */
.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;
}
/* 4. 日志行布局优化 */
.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;
}
/* --- 剩余样式 (保持不变) --- */
.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 { 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; }
.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; }
.event-id { font-weight: bold; color: #2c3e50; background: #e2e8f0; padding: 1px 5px; border-radius: 3px; margin-right: 8px; font-size: 0.75rem; } .card.danger { border-top-color: #ff4757; }
.event-time { margin-left: auto; font-size: 0.75rem; color: #95a5a6; } .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; }
.content-box { padding: 5px 0; } .primary-btn:hover:not(:disabled) { background: #27ae60; } .primary-btn:disabled { background: #bdc3c7; cursor: not-allowed; }
.main-text { font-weight: 500; color: #2c3e50; margin-bottom: 8px; } .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-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; }
</style> </style>