Files
system-doctor/src/App.vue
Julian Freeman 6e7ce21359 make the bg wider
2025-11-25 23:12:06 -04:00

684 lines
18 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="container">
<header class="header">
<h1>🏥 电脑健康体检中心 Pro</h1>
<p class="subtitle">静态排查仪表盘 - Powered by Rust & Tauri</p>
</header>
<div class="actions">
<!-- 主操作按钮 -->
<button @click="startScan" :disabled="loading" class="scan-btn" :class="{ 'scanning': loading }">
<span v-if="loading">🔄 正在深入排查系统底层...</span>
<span v-else>🔍 开始全面体检</span>
</button>
<!-- 辅助操作按钮区 -->
<div class="secondary-actions">
<button class="secondary-btn" @click="triggerImport" :disabled="loading">
📂 导入报告
</button>
<button class="secondary-btn" @click="exportReport" :disabled="!report || loading">
💾 导出报告
</button>
</div>
<!-- 隐藏的文件输入框 -->
<input
type="file"
ref="fileInput"
@change="handleFileImport"
accept=".json"
style="display: none"
/>
<div v-if="errorMsg" class="error-box">
{{ errorMsg }}
</div>
</div>
<!-- 结果显示区域 -->
<div v-if="report" class="dashboard fade-in">
<!-- 1. 硬件概览 (带进度条可视化) -->
<div class="card summary">
<div class="card-header">
<h2>🖥 硬件概览与资源占用</h2>
</div>
<div class="grid-container-summary">
<!-- 左侧基础信息 -->
<div class="basic-info">
<div class="info-item">
<span class="label">CPU 型号</span>
<span class="value text-ellipsis" :title="report.hardware.cpu_name">{{ report.hardware.cpu_name }}</span>
</div>
<!-- 整机信息 (品牌机看这个) -->
<div class="info-item">
<span class="label">整机型号 (System)</span>
<span class="value text-ellipsis" :title="report.hardware.sys_vendor + ' ' + report.hardware.sys_product">
{{ report.hardware.sys_vendor }} {{ report.hardware.sys_product }}
</span>
</div>
<!-- 主板信息 (DIY看这个) -->
<div class="info-item">
<span class="label">主板型号 (BaseBoard)</span>
<span class="value text-ellipsis" :title="report.hardware.mobo_vendor + ' ' + report.hardware.mobo_product">
{{ report.hardware.mobo_vendor }} {{ report.hardware.mobo_product }}
</span>
</div>
<div class="info-item">
<span class="label">BIOS 版本</span>
<span class="value">{{ report.hardware.bios_version }}</span>
</div>
<div class="info-item">
<span class="label">操作系统</span>
<span class="value">{{ report.hardware.os_version }}</span>
</div>
</div>
<!-- 右侧可视化条 -->
<div class="usage-bars">
<!-- C盘空间可视化 -->
<div class="usage-item">
<div class="progress-label">
<span>C盘空间 (已用 {{ report.hardware.c_drive_used_gb }}GB / 总计 {{ report.hardware.c_drive_total_gb }}GB)</span>
<strong>{{ calculatePercent(report.hardware.c_drive_used_gb, report.hardware.c_drive_total_gb) }}%</strong>
</div>
<div class="progress-bar-large">
<div class="fill"
:style="getProgressStyle(report.hardware.c_drive_used_gb, report.hardware.c_drive_total_gb)">
</div>
</div>
</div>
<!-- 内存占用可视化 -->
<div class="usage-item">
<div class="progress-label">
<span>内存占用 (已用 {{ report.hardware.memory_used_gb }}GB / 总计 {{ report.hardware.memory_total_gb }}GB)</span>
<strong>{{ calculatePercent(report.hardware.memory_used_gb, report.hardware.memory_total_gb) }}%</strong>
</div>
<div class="progress-bar-large">
<div class="fill"
:style="getProgressStyle(report.hardware.memory_used_gb, report.hardware.memory_total_gb)">
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 2. 驱动与设备管理器状态 -->
<div class="card" :class="{ danger: report.drivers.length > 0, success: report.drivers.length === 0 }">
<div class="card-header">
<h2>🔌 驱动与设备状态</h2>
<span class="badge" :class="report.drivers.length > 0 ? 'badge-red' : 'badge-green'">
{{ report.drivers.length > 0 ? '发现异常' : '正常' }}
</span>
</div>
<div v-if="report.drivers.length === 0" class="good-news">
设备管理器中未发现带有黄色感叹号或错误的设备
</div>
<div v-else class="list-container">
<div v-for="(drv, idx) in report.drivers" :key="idx" class="list-item error-item">
<div class="item-header">
<strong>{{ drv.device_name }}</strong>
<span class="code-tag">Code {{ drv.error_code }}</span>
</div>
<p class="description">{{ drv.description }}</p>
</div>
</div>
</div>
<!-- 3. 电池健康度 (仅笔记本显示) -->
<div v-if="report.battery" class="card" :class="{ danger: report.battery.health_percentage < 60 }">
<div class="card-header">
<h2>🔋 电池健康度 (寿命)</h2>
<span class="badge" :class="report.battery.is_ac_connected ? 'badge-blue' : 'badge-gray'">
{{ report.battery.is_ac_connected ? '⚡ 已连接电源' : '🔋 使用电池中' }}
</span>
</div>
<div class="content-box">
<div class="progress-wrapper">
<div class="progress-label">
<span>当前健康度 (相对于设计容量)</span>
<strong>{{ report.battery.health_percentage }}%</strong>
</div>
<div class="progress-bar">
<div class="fill"
:style="{ width: report.battery.health_percentage + '%', background: getBatteryColor(report.battery.health_percentage) }">
</div>
</div>
</div>
<p class="explanation">{{ report.battery.explanation }}</p>
</div>
</div>
<!-- 4. 硬盘健康度 -->
<div class="card" :class="{ danger: hasStorageDanger }">
<div class="card-header">
<h2>💾 硬盘健康度 (S.M.A.R.T)</h2>
</div>
<div class="list-container">
<div v-for="(disk, index) in report.storage" :key="index" class="list-item">
<div class="item-header">
<strong>{{ disk.model }}</strong>
<span class="status-text" :class="disk.is_danger ? 'text-red' : 'text-green'">
{{ disk.health_status }}
</span>
</div>
<p class="description">{{ disk.human_explanation }}</p>
</div>
</div>
</div>
<!-- 5. 蓝屏分析 -->
<div class="card" :class="{ danger: report.minidumps.found, success: !report.minidumps.found }">
<div class="card-header">
<h2> 蓝屏死机记录 (BSOD)</h2>
</div>
<div class="content-box">
<p class="main-text">{{ report.minidumps.explanation }}</p>
<p v-if="report.minidumps.found" class="tip">
💡 建议使用 BlueScreenView WinDbg 工具打开 C:\Windows\Minidump 文件夹下的 .dmp 文件进行深入分析
</p>
</div>
</div>
<!-- 6. 关键系统日志 -->
<div class="card" :class="{ danger: report.events.length > 0 }">
<div class="card-header">
<h2> 关键供电与硬件日志 (Event Log)</h2>
</div>
<div v-if="report.events.length === 0" class="good-news">
近期日志中未发现 Kernel-Power(41) WHEA(18/19) 等致命错误
</div>
<div v-else class="list-container">
<div v-for="(evt, idx) in report.events" :key="idx" class="list-item warning-item">
<div class="item-header">
<span class="event-id">ID: {{ evt.event_id }}</span>
<span class="event-source">{{ evt.source }}</span>
<span class="event-time">{{ formatTime(evt.time_generated) }}</span>
</div>
<p class="description highlight">{{ evt.analysis_hint }}</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import { invoke } from '@tauri-apps/api/core';
const report = ref(null);
const loading = ref(false);
const errorMsg = ref('');
const fileInput = ref(null); // 文件输入框的引用
const hasStorageDanger = computed(() => {
return report.value?.storage.some(d => d.is_danger);
});
function calculatePercent(used, total) {
if (total === 0) return 0;
return Math.round((used / total) * 100);
}
function getProgressStyle(used, total) {
const percent = calculatePercent(used, total);
let color = '#2ed573';
if (percent >= 90) {
color = '#ff4757';
} else if (percent >= 75) {
color = '#ffa502';
}
return {
width: `${percent}%`,
background: color
};
}
function getBatteryColor(percentage) {
if (percentage < 50) return '#ff4757';
if (percentage < 80) return '#ffa502';
return '#2ed573';
}
function formatTime(wmiTime) {
if (!wmiTime) return "Unknown Time";
return wmiTime.replace('T', ' ').substring(0, 19);
}
// --- 导出报告功能 ---
function exportReport() {
if (!report.value) return;
try {
const dataStr = JSON.stringify(report.value, null, 2);
// 使用 Blob 创建文件对象
const blob = new Blob([dataStr], { type: "application/json" });
const url = URL.createObjectURL(blob);
// 创建临时链接触发下载
const link = document.createElement('a');
link.href = url;
// 文件名包含时间戳,避免重名
const timestamp = new Date().toISOString().slice(0, 19).replace(/[:T]/g, "-");
link.download = `SystemDoctor_Report_${timestamp}.json`;
document.body.appendChild(link);
link.click();
// 清理
document.body.removeChild(link);
URL.revokeObjectURL(url);
} catch (err) {
errorMsg.value = "导出失败: " + err.message;
}
}
// --- 导入报告功能 ---
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;
errorMsg.value = '';
} else {
errorMsg.value = "无效的报告文件:格式不正确或内容缺失。";
}
} catch (err) {
errorMsg.value = "导入失败: 无法解析文件 (" + err.message + ")";
}
};
reader.readAsText(file);
// 清空 value 以便允许重复选择同一个文件
event.target.value = '';
}
async function startScan() {
// 1. 如果界面已有报告,先清空
if (report.value) {
report.value = null;
}
loading.value = true;
errorMsg.value = '';
try {
const result = await invoke('run_diagnosis');
console.log("诊断结果:", result);
report.value = result;
} catch (e) {
console.error(e);
errorMsg.value = "扫描失败: " + e + " (请确保以管理员身份运行此程序)";
} finally {
loading.value = false;
}
}
</script>
<!-- 添加全局样式重置 body -->
<style>
body {
margin: 0;
padding: 0;
overflow-x: hidden; /* 防止水平滚动条 */
}
</style>
<style scoped>
/* 基础布局 */
.container {
max-width: 1200px; /* 改宽了: 从960px增加到1200px */
margin: 0 auto;
padding: 30px 20px;
box-sizing: border-box;
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
color: #2c3e50;
background-color: #f0f2f5;
min-height: 100vh;
}
.header {
text-align: center;
margin-bottom: 30px;
}
h1 {
font-size: 2rem;
margin-bottom: 8px;
color: #2c3e50;
letter-spacing: -0.5px;
}
.subtitle {
color: #7f8c8d;
font-size: 1rem;
}
/* 操作区域 */
.actions {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 35px;
gap: 15px; /* 增加按钮组之间的间距 */
}
.secondary-actions {
display: flex;
gap: 15px;
}
.scan-btn {
background: linear-gradient(135deg, #42b983 0%, #2ecc71 100%);
color: white;
border: none;
padding: 14px 40px;
font-size: 1.1rem;
border-radius: 50px;
cursor: pointer;
box-shadow: 0 4px 15px rgba(46, 204, 113, 0.25);
transition: all 0.2s ease;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
min-width: 200px;
}
.scan-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(46, 204, 113, 0.35);
filter: brightness(1.05);
}
.scan-btn:active:not(:disabled) {
transform: translateY(1px);
}
.scan-btn:disabled {
background: #bdc3c7;
cursor: not-allowed;
box-shadow: none;
opacity: 0.8;
}
/* 辅助按钮样式 */
.secondary-btn {
background: white;
border: 1px solid #dcdfe6;
color: #606266;
padding: 8px 20px;
font-size: 0.9rem;
border-radius: 20px;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 5px;
}
.secondary-btn:hover:not(:disabled) {
color: #409eff;
border-color: #c6e2ff;
background-color: #ecf5ff;
}
.secondary-btn:disabled {
color: #c0c4cc;
cursor: not-allowed;
background-color: #f5f7fa;
}
.error-box {
margin-top: 5px;
padding: 12px 20px;
background-color: #ffeaea;
color: #c0392b;
border: 1px solid #ffcccc;
border-radius: 8px;
font-weight: 500;
font-size: 0.95rem;
}
/* 仪表盘网格 */
.dashboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 25px;
}
.fade-in {
animation: fadeIn 0.4s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(15px); }
to { opacity: 1; transform: translateY(0); }
}
/* 卡片通用样式 */
.card {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 5px 15px rgba(0,0,0,0.04);
border-top: 4px solid #42b983;
transition: transform 0.2s, box-shadow 0.2s;
border-left: 1px solid #eee; border-right: 1px solid #eee; border-bottom: 1px solid #eee;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0,0,0,0.08);
}
.card.danger { border-top-color: #ff4757; background: #fffbfb; }
.card.success { border-top-color: #2ed573; }
.card.summary {
border-top-color: #3742fa;
grid-column: 1 / -1;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 18px;
padding-bottom: 12px;
border-bottom: 1px solid #f1f2f6;
}
h2 {
font-size: 1.2rem;
margin: 0;
font-weight: 600;
}
/* 硬件概览布局 */
.grid-container-summary {
display: grid;
grid-template-columns: 1fr 1.5fr;
gap: 30px;
align-items: start;
}
@media (max-width: 768px) {
.grid-container-summary {
grid-template-columns: 1fr;
gap: 20px;
}
}
.basic-info {
display: flex;
flex-direction: column;
gap: 15px;
}
.info-item {
display: flex;
flex-direction: column;
}
.label {
font-size: 0.8rem;
color: #95a5a6;
margin-bottom: 4px;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 600;
}
.value {
font-size: 1.05rem;
font-weight: 500;
color: #2c3e50;
}
.text-ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 大型进度条样式 */
.usage-bars {
display: flex;
flex-direction: column;
gap: 25px;
padding-top: 10px;
}
.usage-item {
display: flex;
flex-direction: column;
gap: 8px;
}
.progress-label {
display: flex;
justify-content: space-between;
font-size: 0.9rem;
color: #636e72;
}
.progress-label strong {
color: #2c3e50;
font-size: 1rem;
}
.progress-bar-large {
width: 100%;
height: 20px;
background: #e6e8ec;
border-radius: 10px;
overflow: hidden;
box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
}
.fill {
height: 100%;
transition: width 0.6s cubic-bezier(0.25, 0.8, 0.25, 1), background-color 0.6s ease;
}
/* 列表样式 */
.list-container {
display: flex;
flex-direction: column;
gap: 12px;
}
.list-item {
background: #fcfdfe;
padding: 12px 15px;
border-radius: 8px;
border: 1px solid #ececec;
}
.list-item.error-item { background: #fff5f5; border-color: #ffcccc; }
.list-item.warning-item { background: #fffcf0; border-color: #ffeaa7; }
.item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
flex-wrap: wrap;
gap: 5px;
}
.description {
margin: 0;
color: #636e72;
font-size: 0.9rem;
line-height: 1.5;
}
.description.highlight { color: #d35400; font-weight: 500; }
/* 状态文本与徽章 */
.status-text { font-weight: 600; font-size: 0.9rem; }
.text-green { color: #27ae60; }
.text-red { color: #c0392b; }
.badge {
font-size: 0.75rem;
padding: 3px 10px;
border-radius: 12px;
color: white;
font-weight: 600;
letter-spacing: 0.5px;
}
.badge-green { background-color: #2ed573; }
.badge-red { background-color: #ff4757; }
.badge-blue { background-color: #3742fa; }
.badge-gray { background-color: #95a5a6; }
.code-tag {
background: #ff4757; color: white; padding: 2px 6px; border-radius: 4px; font-size: 0.8rem; font-weight: bold;
}
.good-news {
color: #27ae60; font-weight: 600; background: #eafaf1; padding: 15px; border-radius: 8px; border: 1px solid #d4efdf; text-align: center;
}
.tip {
margin-top: 15px; font-size: 0.85rem; color: #d35400; background: #fff3e0; padding: 12px; border-radius: 6px; border: 1px solid #ffe0b2;
}
/* 电池进度条 */
.progress-wrapper { margin-bottom: 15px; }
.progress-bar {
width: 100%; height: 10px; background: #e6e8ec; border-radius: 5px; overflow: hidden;
}
/* 日志元数据 */
.event-id, .event-source, .event-time {
font-size: 0.75rem; color: #7f8c8d;
}
.event-id {
font-weight: bold; color: #2c3e50; background: #dfe6e9; padding: 1px 5px; border-radius: 4px; margin-right: 8px;
}
.event-time { margin-left: auto; }
/* 内容盒模型 */
.content-box { padding: 5px 0; }
.main-text { font-weight: 500; color: #2c3e50; margin-bottom: 10px;}
</style>