414 lines
26 KiB
Vue
414 lines
26 KiB
Vue
<template>
|
||
<div class="app-layout">
|
||
<!-- 左侧侧边栏 -->
|
||
<aside class="sidebar">
|
||
<div class="brand">
|
||
<span class="icon">🩺</span>
|
||
<h1>体检中心</h1>
|
||
</div>
|
||
<nav class="nav-menu">
|
||
<button
|
||
class="nav-item"
|
||
:class="{ active: currentTab === 'overview' }"
|
||
@click="currentTab = 'overview'"
|
||
>
|
||
<span class="nav-icon">📊</span> 静态概览
|
||
</button>
|
||
<button
|
||
class="nav-item"
|
||
:class="{ active: currentTab === 'bsod' }"
|
||
@click="currentTab = 'bsod'"
|
||
>
|
||
<span class="nav-icon">☠️</span> 蓝屏分析
|
||
</button>
|
||
</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">
|
||
<!-- 1. 硬件概览 (强制独占一行) -->
|
||
<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>
|
||
|
||
<!-- 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">
|
||
<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>
|
||
|
||
<!-- 3. 电池健康 -->
|
||
<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>
|
||
|
||
<!-- 4. 硬盘健康 -->
|
||
<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>
|
||
|
||
<!-- 5. 日志卡片 -->
|
||
<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>
|
||
|
||
<!-- TAB 2: 蓝屏分析 -->
|
||
<div v-if="currentTab === 'bsod'" class="tab-view bsod-view">
|
||
<div class="view-header">
|
||
<h2>蓝屏死机分析 (Minidump)</h2>
|
||
<button class="secondary-btn" @click="loadMinidumps" :disabled="bsodLoading">🔄 刷新列表</button>
|
||
</div>
|
||
|
||
<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>
|
||
|
||
</main>
|
||
|
||
<!-- Toast 通知 -->
|
||
<transition name="toast-slide">
|
||
<div v-if="toast.show" class="toast-notification" :class="toast.type">
|
||
<div class="toast-content">
|
||
<span class="check-icon">{{ toast.type === 'error' ? '❌' : '✅' }}</span>
|
||
<div class="toast-text"><h4>{{ toast.title }}</h4><p>{{ toast.message }}</p></div>
|
||
</div>
|
||
<div class="toast-progress"></div>
|
||
</div>
|
||
</transition>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onUnmounted, reactive, onMounted, watch } from 'vue';
|
||
import { invoke } from '@tauri-apps/api/core';
|
||
import { listen } from '@tauri-apps/api/event';
|
||
|
||
// --- 状态管理 ---
|
||
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 loading = ref(false);
|
||
const errorMsg = ref('');
|
||
const scanFinished = ref(false);
|
||
const fileInput = ref(null);
|
||
let unlistenFns = [];
|
||
|
||
// BSOD 相关
|
||
const bsodList = ref([]);
|
||
const selectedBsod = ref(null);
|
||
const bsodResult = ref(null);
|
||
const bsodLoading = ref(false);
|
||
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 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 getProgressStyle(used, total) {
|
||
const percent = calculatePercent(used, total);
|
||
let color = percent >= 90 ? '#ff4757' : (percent >= 75 ? '#ffa502' : '#2ed573');
|
||
return { width: `${percent}%`, background: color };
|
||
}
|
||
function getBatteryColor(p) { return p < 50 ? '#ff4757' : (p < 80 ? '#ffa502' : '#2ed573'); }
|
||
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'); } }
|
||
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 = ''; }
|
||
|
||
async function startScan() {
|
||
report.value = emptyReport(); errorMsg.value = ''; scanFinished.value = false; loading.value = true;
|
||
if (unlistenFns.length > 0) { for (const fn of unlistenFns) fn(); unlistenFns = []; }
|
||
try {
|
||
unlistenFns.push(
|
||
await listen('report-hardware', (e) => report.value.hardware = e.payload),
|
||
await listen('report-storage', (e) => report.value.storage = e.payload),
|
||
await listen('report-drivers', (e) => report.value.drivers = e.payload),
|
||
await listen('report-minidumps', (e) => report.value.minidumps = e.payload),
|
||
await listen('report-battery', (e) => report.value.battery = e.payload),
|
||
await listen('report-events', (e) => report.value.events = e.payload),
|
||
await listen('diagnosis-finished', () => { loading.value = false; scanFinished.value = true; triggerToast('扫描完成', '体检已结束', 'success'); })
|
||
);
|
||
await invoke('run_diagnosis');
|
||
} catch (e) { loading.value = false; errorMsg.value = e; }
|
||
}
|
||
|
||
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; } }
|
||
|
||
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); }
|
||
onUnmounted(() => { for (const fn of unlistenFns) fn(); if (toastTimer) clearTimeout(toastTimer); });
|
||
</script>
|
||
|
||
<!-- [修复 2] 全局滚动条样式优化 -->
|
||
<style>
|
||
body {
|
||
margin: 0;
|
||
padding: 0;
|
||
background-color: #f4f6f9;
|
||
/* 改为 hidden,因为使用了内部 flex 滚动区域 (.main-content) */
|
||
overflow: hidden;
|
||
}
|
||
</style>
|
||
|
||
<style scoped>
|
||
.app-layout { display: flex; height: 100vh; width: 100vw; overflow: hidden; background-color: #f4f6f9; }
|
||
.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; }
|
||
.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; }
|
||
.view-header h2 { margin: 0; font-size: 1.8rem; color: #2c3e50; }
|
||
.actions-row { display: flex; align-items: center; gap: 15px; }
|
||
.sub-actions { display: flex; gap: 10px; }
|
||
|
||
.dashboard { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 20px; }
|
||
.card { background: white; border-radius: 10px; padding: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.03); border: 1px solid #eaecf0; border-top: 3px solid #2ecc71; }
|
||
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #f0f2f5; }
|
||
.card-header h3 { margin: 0; font-size: 1.1rem; font-weight: 600; }
|
||
|
||
.item-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; gap: 12px; }
|
||
.progress-label { display: flex; justify-content: space-between; font-size: 0.85rem; color: #636e72; gap: 10px; }
|
||
.text-ellipsis { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
||
.item-header strong { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; flex: 1; }
|
||
.status-text, .code-tag, .badge { flex-shrink: 0; white-space: nowrap; }
|
||
.event-id { font-weight: bold; color: #2c3e50; background: #e2e8f0; padding: 1px 5px; border-radius: 3px; font-size: 0.75rem; flex-shrink: 0; }
|
||
.event-source { font-size: 0.75rem; color: #7f8c8d; flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin: 0 5px; text-align: left; }
|
||
.event-time { font-size: 0.75rem; color: #95a5a6; flex-shrink: 0; white-space: nowrap; }
|
||
.badge { font-size: 0.75rem; padding: 3px 10px; border-radius: 12px; color: white; font-weight: 600; letter-spacing: 0.5px; white-space: nowrap; flex-shrink: 0; }
|
||
|
||
.grid-container-summary { display: grid; grid-template-columns: 1fr 1.5fr; gap: 25px; }
|
||
@media (max-width: 900px) { .grid-container-summary { grid-template-columns: 1fr; } }
|
||
.basic-info, .usage-bars { display: flex; flex-direction: column; gap: 12px; }
|
||
.usage-bars { padding-top: 5px; }
|
||
.usage-item { display: flex; flex-direction: column; gap: 6px; }
|
||
.info-item { display: flex; flex-direction: column; }
|
||
.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; }
|
||
.progress-bar-large { width: 100%; height: 16px; background: #f1f3f5; border-radius: 8px; overflow: hidden; }
|
||
.fill { height: 100%; transition: width 0.6s ease; }
|
||
.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.error-item { background: #fff5f5; border-color: #fed7d7; }
|
||
.list-item.warning-item { background: #fffaf0; border-color: #fce588; }
|
||
.description { margin: 0; color: #636e72; font-size: 0.9rem; line-height: 1.4; }
|
||
.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; word-break: break-all; }
|
||
.text-green { color: #27ae60; } .text-red { color: #c0392b; }
|
||
.badge-red { background-color: #ff4757; } .badge-green { background-color: #2ed573; } .badge-blue { background-color: #3498db; } .badge-gray { background-color: #95a5a6; }
|
||
.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; }
|
||
.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-bar { width: 100%; height: 8px; background: #f1f3f5; border-radius: 4px; overflow: hidden; }
|
||
.content-box { padding: 5px 0; } .main-text { font-weight: 500; color: #2c3e50; margin-bottom: 8px; }
|
||
.card.danger { border-top-color: #ff4757; }
|
||
|
||
/* [修复 1] 硬件概览独占一行 (Explicit Override) */
|
||
.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:hover:not(:disabled) { background: #27ae60; } .primary-btn:disabled { background: #bdc3c7; cursor: not-allowed; }
|
||
.secondary-btn { background: white; border: 1px solid #dcdfe6; color: #606266; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 0.9rem; }
|
||
.secondary-btn:hover:not(:disabled) { border-color: #2ecc71; color: #2ecc71; }
|
||
.icon-btn { background: transparent; border: none; cursor: pointer; padding: 8px; color: #7f8c8d; font-weight: 600; }
|
||
.icon-btn:hover:not(:disabled) { color: #2ecc71; background: #eafaf1; border-radius: 6px; }
|
||
.toast-notification { position: fixed; top: 20px; right: 20px; background: white; border-left: 5px solid #2ecc71; padding: 15px 20px; border-radius: 8px; box-shadow: 0 5px 20px rgba(0,0,0,0.15); z-index: 100; min-width: 250px; }
|
||
.toast-notification.error { border-left-color: #ff4757; }
|
||
.toast-content { display: flex; gap: 12px; } .toast-text h4 { margin: 0 0 4px; font-size: 1rem; } .toast-text p { margin: 0; color: #7f8c8d; font-size: 0.85rem; }
|
||
.toast-slide-enter-active, .toast-slide-leave-active { transition: all 0.3s; } .toast-slide-enter-from, .toast-slide-leave-to { transform: translateX(100%); opacity: 0; }
|
||
@keyframes spin { 100% { transform: rotate(360deg); } }
|
||
.fade-in { animation: fadeIn 0.4s ease-out; } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||
.slide-up { animation: slideUp 0.4s ease-out forwards; } @keyframes slideUp { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
|
||
.bsod-layout { display: grid; grid-template-columns: 300px 1fr; gap: 20px; height: calc(100vh - 120px); }
|
||
.bsod-list-panel { background: white; border-radius: 10px; border: 1px solid #eaecf0; overflow-y: auto; }
|
||
.bsod-detail-panel { background: white; border-radius: 10px; border: 1px solid #eaecf0; padding: 25px; overflow-y: auto; position: relative; }
|
||
.file-item { padding: 15px; border-bottom: 1px solid #f0f2f5; cursor: pointer; display: flex; gap: 12px; transition: background 0.2s; }
|
||
.file-item:hover { background: #f8f9fa; } .file-item.active { background: #eafaf1; border-left: 4px solid #2ecc71; }
|
||
.file-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> |