Files
system-doctor/src/App.vue
Julian Freeman b553fb495c import ui
2025-11-26 09:32:54 -04:00

414 lines
26 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.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>