add custom bsod
This commit is contained in:
@@ -7,11 +7,11 @@
|
|||||||
use serde::Serialize;
|
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, ops::Deref};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use tauri::Emitter;
|
use tauri::Emitter;
|
||||||
use chrono::{Duration, FixedOffset, Local, NaiveDate, TimeZone, DateTime};
|
use chrono::{Duration, FixedOffset, Local, NaiveDate, TimeZone, DateTime};
|
||||||
// [修改] 只引入 minidump 基础库,移除 processor 依赖以避免 API 冲突
|
// 只引入 minidump 基础库
|
||||||
use minidump::{Minidump, MinidumpException, MinidumpSystemInfo};
|
use minidump::{Minidump, MinidumpException, MinidumpSystemInfo};
|
||||||
|
|
||||||
// --- 1. 数据结构 (保持不变) ---
|
// --- 1. 数据结构 (保持不变) ---
|
||||||
@@ -129,9 +129,7 @@ fn format_wmi_time(wmi_str: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 新增功能:翻译蓝屏代码为人话 (手动映射常见代码) ---
|
|
||||||
fn translate_bugcheck_u32(code: u32) -> (String, String) {
|
fn translate_bugcheck_u32(code: u32) -> (String, String) {
|
||||||
// 这些是 Windows 最常见的 BSOD 代码
|
|
||||||
match code {
|
match code {
|
||||||
0x000000D1 => (
|
0x000000D1 => (
|
||||||
"DRIVER_IRQL_NOT_LESS_OR_EQUAL (0xD1)".to_string(),
|
"DRIVER_IRQL_NOT_LESS_OR_EQUAL (0xD1)".to_string(),
|
||||||
@@ -190,6 +188,31 @@ fn translate_bugcheck_u32(code: u32) -> (String, String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [新增] 抽离公共分析逻辑
|
||||||
|
fn analyze_dump_data<T: Deref<Target = [u8]>>(dump: Minidump<T>) -> Result<BsodAnalysisReport, String> {
|
||||||
|
let exception_stream = dump.get_stream::<MinidumpException>()
|
||||||
|
.map_err(|_| "无法找到异常信息流 (No Exception Stream),可能是非标准 Dump 文件。".to_string())?;
|
||||||
|
|
||||||
|
let exception_code = exception_stream.raw.exception_record.exception_code;
|
||||||
|
let exception_address = exception_stream.raw.exception_record.exception_address;
|
||||||
|
|
||||||
|
let sys_info_str = match dump.get_stream::<MinidumpSystemInfo>() {
|
||||||
|
Ok(info) => format!("Windows Build {}", info.raw.build_number),
|
||||||
|
Err(_) => "Unknown OS".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: "根据错误代码自动匹配的分析结果。".to_string(),
|
||||||
|
recommendation: recommend,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// --- 命令:列出 Minidump 文件 ---
|
// --- 命令:列出 Minidump 文件 ---
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn list_minidumps() -> Result<Vec<BsodFileItem>, String> {
|
async fn list_minidumps() -> Result<Vec<BsodFileItem>, String> {
|
||||||
@@ -200,17 +223,15 @@ async fn list_minidumps() -> Result<Vec<BsodFileItem>, String> {
|
|||||||
if let Ok(entries) = fs::read_dir(path) {
|
if let Ok(entries) = fs::read_dir(path) {
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
if let Ok(entry) = entry {
|
if let Ok(entry) = entry {
|
||||||
// [修复] 使用 .as_ref() 防止 metadata 所有权被移动
|
|
||||||
let metadata = entry.metadata().ok();
|
let metadata = entry.metadata().ok();
|
||||||
let created = metadata.as_ref()
|
let created = metadata.as_ref()
|
||||||
.and_then(|m| m.modified().ok()) // 通常用修改时间
|
.and_then(|m| m.modified().ok())
|
||||||
.map(|t| {
|
.map(|t| {
|
||||||
let dt: DateTime<Local> = t.into();
|
let dt: DateTime<Local> = t.into();
|
||||||
dt.format("%Y-%m-%d %H:%M:%S").to_string()
|
dt.format("%Y-%m-%d %H:%M:%S").to_string()
|
||||||
})
|
})
|
||||||
.unwrap_or("Unknown".to_string());
|
.unwrap_or("Unknown".to_string());
|
||||||
|
|
||||||
// [修复] 使用 .as_ref() 再次访问 metadata
|
|
||||||
let size = metadata.as_ref().map(|m| m.len() / 1024).unwrap_or(0);
|
let size = metadata.as_ref().map(|m| m.len() / 1024).unwrap_or(0);
|
||||||
|
|
||||||
files.push(BsodFileItem {
|
files.push(BsodFileItem {
|
||||||
@@ -223,65 +244,42 @@ async fn list_minidumps() -> Result<Vec<BsodFileItem>, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 按时间倒序排列
|
|
||||||
files.sort_by(|a, b| b.created_time.cmp(&a.created_time));
|
files.sort_by(|a, b| b.created_time.cmp(&a.created_time));
|
||||||
Ok(files)
|
Ok(files)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 命令:分析指定的 Minidump 文件 ---
|
// --- 命令:分析指定路径的 Minidump ---
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn analyze_minidump(filepath: String) -> Result<BsodAnalysisReport, String> {
|
async fn analyze_minidump(filepath: String) -> Result<BsodAnalysisReport, String> {
|
||||||
let path = Path::new(&filepath);
|
let path = Path::new(&filepath);
|
||||||
|
|
||||||
// 1. 读取文件 (使用基础 minidump 库)
|
|
||||||
let dump = Minidump::read_path(path).map_err(|e| format!("无法读取文件: {}", e))?;
|
let dump = Minidump::read_path(path).map_err(|e| format!("无法读取文件: {}", e))?;
|
||||||
|
analyze_dump_data(dump)
|
||||||
|
}
|
||||||
|
|
||||||
// 2. 直接获取异常流 (Exception Stream)
|
// [新增] 命令:分析二进制内容的 Minidump (用于前端导入)
|
||||||
let exception_stream = dump.get_stream::<MinidumpException>()
|
#[tauri::command]
|
||||||
.map_err(|_| "无法找到异常信息流 (No Exception Stream)".to_string())?;
|
async fn analyze_minidump_bytes(file_content: Vec<u8>) -> Result<BsodAnalysisReport, String> {
|
||||||
|
let dump = Minidump::read(file_content).map_err(|e| format!("无法解析文件内容: {}", e))?;
|
||||||
// [修复] 使用 .raw 访问内部原始结构
|
analyze_dump_data(dump)
|
||||||
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 (保持不变) ---
|
// --- 现有命令:run_diagnosis (保持不变) ---
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn run_diagnosis(window: tauri::Window) -> Result<(), String> {
|
async fn run_diagnosis(window: tauri::Window) -> Result<(), String> {
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
// 1. 硬件概览
|
// ... (run_diagnosis 的内容保持不变,这里省略以节省篇幅,请确保保留原有逻辑) ...
|
||||||
|
// 为了确保代码完整性,这里包含 run_diagnosis 的第一部分作为占位,实际应用中请保持原样
|
||||||
{
|
{
|
||||||
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(); }
|
||||||
@@ -299,7 +297,6 @@ async fn run_diagnosis(window: tauri::Window) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
@@ -311,29 +308,19 @@ 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);
|
||||||
}
|
}
|
||||||
|
// ... (其他部分保持不变) ...
|
||||||
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 {
|
||||||
@@ -357,8 +344,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 {
|
||||||
@@ -374,32 +359,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 {
|
||||||
@@ -411,15 +389,9 @@ 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,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -427,8 +399,6 @@ async fn run_diagnosis(window: tauri::Window) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
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 {
|
||||||
@@ -437,12 +407,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" {
|
||||||
@@ -454,7 +422,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),
|
||||||
@@ -470,17 +437,14 @@ async fn run_diagnosis(window: tauri::Window) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
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, list_minidumps, analyze_minidump])
|
.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");
|
||||||
}
|
}
|
||||||
102
src/App.vue
102
src/App.vue
@@ -158,8 +158,15 @@
|
|||||||
<div v-if="currentTab === 'bsod'" class="tab-view bsod-view">
|
<div v-if="currentTab === 'bsod'" class="tab-view bsod-view">
|
||||||
<div class="view-header">
|
<div class="view-header">
|
||||||
<h2>蓝屏死机分析 (Minidump)</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>
|
<button class="secondary-btn" @click="loadMinidumps" :disabled="bsodLoading">🔄 刷新列表</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 隐藏的 BSOD 文件输入框 -->
|
||||||
|
<input type="file" ref="bsodFileInput" @change="handleBsodFileImport" accept=".dmp" style="display: none" />
|
||||||
|
|
||||||
<div class="bsod-layout">
|
<div class="bsod-layout">
|
||||||
<div class="bsod-list-panel">
|
<div class="bsod-list-panel">
|
||||||
@@ -211,7 +218,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="empty-detail">
|
<div v-else class="empty-detail">
|
||||||
👈 请从左侧选择一个蓝屏文件开始分析
|
👈 请从左侧选择一个蓝屏文件开始分析,<br>或点击上方“导入文件”分析外部文件
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -255,6 +262,7 @@ const selectedBsod = ref(null);
|
|||||||
const bsodResult = ref(null);
|
const bsodResult = ref(null);
|
||||||
const bsodLoading = ref(false);
|
const bsodLoading = ref(false);
|
||||||
const bsodAnalyzing = ref(false);
|
const bsodAnalyzing = ref(false);
|
||||||
|
const bsodFileInput = ref(null); // 新增 BSOD 文件输入引用
|
||||||
|
|
||||||
const toast = reactive({ show: false, title: '', message: '', type: 'success' });
|
const toast = reactive({ show: false, title: '', message: '', type: 'success' });
|
||||||
let toastTimer = null;
|
let toastTimer = null;
|
||||||
@@ -272,52 +280,23 @@ 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() {
|
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);
|
||||||
|
|
||||||
// 1. 尝试使用现代浏览器 API (支持选择保存位置)
|
|
||||||
if ('showSaveFilePicker' in window) {
|
if ('showSaveFilePicker' in window) {
|
||||||
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'] },
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
const writable = await handle.createWritable();
|
|
||||||
await writable.write(content);
|
|
||||||
await writable.close();
|
|
||||||
triggerToast('导出成功', '文件已保存到指定位置', 'success');
|
|
||||||
return;
|
|
||||||
} catch (err) {
|
|
||||||
if (err.name === 'AbortError') return; // 用户取消
|
|
||||||
console.warn("File System Access API warning:", err);
|
|
||||||
// 继续执行降级方案
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 降级方案:传统的 Blob 下载 (通常保存到下载文件夹)
|
|
||||||
try {
|
try {
|
||||||
const blob = new Blob([content], { type: "application/json" });
|
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 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');
|
triggerToast('导出成功', '已保存到默认下载目录', 'success');
|
||||||
} catch (err) {
|
} catch (err) { triggerToast('导出失败', err.message, 'error'); }
|
||||||
triggerToast('导出失败', err.message, 'error');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function triggerImport() { fileInput.value.click(); }
|
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; scanFinished.value = false; triggerToast('导入成功', '已加载历史报告'); } else { triggerToast('导入失败', '文件格式错误', 'error'); } } catch (err) { triggerToast('解析失败', err.message, 'error'); } }; reader.readAsText(file); event.target.value = ''; }
|
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 = ''; }
|
||||||
|
|
||||||
@@ -338,24 +317,51 @@ async function startScan() {
|
|||||||
} catch (e) { loading.value = false; errorMsg.value = e; }
|
} 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 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; 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; } }
|
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; } }
|
||||||
|
|
||||||
|
// [新增] BSOD 导入功能
|
||||||
|
function triggerBsodImport() { bsodFileInput.value.click(); }
|
||||||
|
function handleBsodFileImport(event) {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
// 更新 UI 状态,模拟选中了一个“外部文件”
|
||||||
|
selectedBsod.value = { path: 'external', filename: file.name, created_time: 'Imported', size_kb: Math.round(file.size / 1024) };
|
||||||
|
bsodResult.value = null;
|
||||||
|
bsodAnalyzing.value = true;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async (e) => {
|
||||||
|
try {
|
||||||
|
// 将 ArrayBuffer 转换为 Uint8Array (Rust Vec<u8>)
|
||||||
|
const arrayBuffer = e.target.result;
|
||||||
|
const bytes = new Uint8Array(arrayBuffer);
|
||||||
|
const byteArray = Array.from(bytes); // 转换为普通数组以便序列化传输
|
||||||
|
|
||||||
|
const result = await invoke('analyze_minidump_bytes', { fileContent: byteArray });
|
||||||
|
bsodResult.value = result;
|
||||||
|
triggerToast('分析成功', '已完成外部文件解析', 'success');
|
||||||
|
} catch (err) {
|
||||||
|
triggerToast('分析失败', err, 'error');
|
||||||
|
} finally {
|
||||||
|
bsodAnalyzing.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
event.target.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
watch(currentTab, (newVal) => { if (newVal === 'bsod' && bsodList.value.length === 0) loadMinidumps(); });
|
watch(currentTab, (newVal) => { if (newVal === 'bsod' && bsodList.value.length === 0) loadMinidumps(); });
|
||||||
|
|
||||||
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); }
|
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); });
|
onUnmounted(() => { for (const fn of unlistenFns) fn(); if (toastTimer) clearTimeout(toastTimer); });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- [修复 2] 全局滚动条样式优化 -->
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body { margin: 0; padding: 0; background-color: #f4f6f9; overflow: hidden; }
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
background-color: #f4f6f9;
|
|
||||||
/* 改为 hidden,因为使用了内部 flex 滚动区域 (.main-content) */
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -418,11 +424,7 @@ body {
|
|||||||
.content-box { padding: 5px 0; } .main-text { font-weight: 500; color: #2c3e50; margin-bottom: 8px; }
|
.content-box { padding: 5px 0; } .main-text { font-weight: 500; color: #2c3e50; margin-bottom: 8px; }
|
||||||
.card.danger { border-top-color: #ff4757; }
|
.card.danger { border-top-color: #ff4757; }
|
||||||
|
|
||||||
/* [修复 1] 硬件概览独占一行 (Explicit Override) */
|
.card.summary { border-top-color: #3498db; grid-column: 1 / -1; }
|
||||||
.card.summary {
|
|
||||||
border-top-color: #3498db;
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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; }
|
.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; }
|
||||||
.primary-btn:hover:not(:disabled) { background: #27ae60; } .primary-btn:disabled { background: #bdc3c7; cursor: not-allowed; }
|
.primary-btn:hover:not(:disabled) { background: #27ae60; } .primary-btn:disabled { background: #bdc3c7; cursor: not-allowed; }
|
||||||
|
|||||||
Reference in New Issue
Block a user