improve title ui
This commit is contained in:
380
src/App.vue
380
src/App.vue
@@ -1,34 +1,58 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<header class="header">
|
<!-- 顶部标题栏 -->
|
||||||
<h1>🏥 电脑健康体检中心 Pro</h1>
|
<header class="header-bar">
|
||||||
<p class="subtitle">静态排查仪表盘 - Powered by Rust & Tauri</p>
|
<div class="brand">
|
||||||
|
<span class="icon">🩺</span>
|
||||||
|
<h1>静态排查仪表盘</h1>
|
||||||
|
</div>
|
||||||
|
<span class="version-tag">Powered by Rust & Tauri</span>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="actions">
|
<!-- 工具栏:左侧主操作,右侧辅助操作 -->
|
||||||
<!-- 主操作按钮 -->
|
<div class="toolbar">
|
||||||
<button @click="startScan" :disabled="loading" class="scan-btn" :class="{ 'scanning': loading }">
|
<div class="left-actions">
|
||||||
<span v-if="loading">🔄 正在深入排查系统底层...</span>
|
<button @click="startScan" :disabled="loading" class="primary-btn" :class="{ 'scanning': loading }">
|
||||||
<span v-else>🔍 开始全面体检</span>
|
<span v-if="loading" class="icon-spin">🔄</span>
|
||||||
</button>
|
<span v-if="loading">正在排查...</span>
|
||||||
|
<template v-else>
|
||||||
<!-- 辅助操作按钮区 -->
|
<span class="btn-icon">🔍</span> 开始全面体检
|
||||||
<div class="secondary-actions">
|
</template>
|
||||||
<button class="secondary-btn" @click="triggerImport" :disabled="loading">
|
|
||||||
📂 导入报告
|
|
||||||
</button>
|
|
||||||
<button class="secondary-btn" @click="exportReport" :disabled="!isReportValid || loading">
|
|
||||||
💾 导出报告
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="right-actions">
|
||||||
|
<button class="icon-btn" @click="triggerImport" :disabled="loading" title="导入报告">
|
||||||
|
📂 导入
|
||||||
|
</button>
|
||||||
|
<button class="icon-btn" @click="exportReport" :disabled="!isReportValid || loading" title="导出报告">
|
||||||
|
💾 导出
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 隐藏的文件输入框 -->
|
||||||
<input type="file" ref="fileInput" @change="handleFileImport" accept=".json" style="display: none" />
|
<input type="file" ref="fileInput" @change="handleFileImport" accept=".json" style="display: none" />
|
||||||
|
|
||||||
<div v-if="errorMsg" class="error-box">⚠️ {{ errorMsg }}</div>
|
<!-- 错误提示 (保留在原本位置或顶部) -->
|
||||||
<!-- 完成提示 -->
|
<div v-if="errorMsg" class="inline-error">
|
||||||
<div v-if="scanFinished" class="success-box">✅ 扫描已完成!所有项目检查完毕。</div>
|
⚠️ {{ errorMsg }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 右上角 Toast 通知 (扫描完成提示) -->
|
||||||
|
<transition name="toast-slide">
|
||||||
|
<div v-if="showToast" class="toast-notification">
|
||||||
|
<div class="toast-content">
|
||||||
|
<span class="check-icon">✅</span>
|
||||||
|
<div class="toast-text">
|
||||||
|
<h4>扫描已完成</h4>
|
||||||
|
<p>所有硬件与日志检查完毕</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="toast-progress"></div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
<!-- 结果显示区域 -->
|
<!-- 结果显示区域 -->
|
||||||
<div v-if="report.hardware" class="dashboard fade-in">
|
<div v-if="report.hardware" class="dashboard fade-in">
|
||||||
|
|
||||||
@@ -44,11 +68,11 @@
|
|||||||
<span class="value text-ellipsis" :title="report.hardware.cpu_name">{{ report.hardware.cpu_name }}</span>
|
<span class="value text-ellipsis" :title="report.hardware.cpu_name">{{ report.hardware.cpu_name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="label">整机型号 (System)</span>
|
<span class="label">整机型号</span>
|
||||||
<span class="value text-ellipsis">{{ report.hardware.sys_vendor }} {{ report.hardware.sys_product }}</span>
|
<span class="value text-ellipsis">{{ report.hardware.sys_vendor }} {{ report.hardware.sys_product }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="label">主板型号 (BaseBoard)</span>
|
<span class="label">主板型号</span>
|
||||||
<span class="value text-ellipsis">{{ report.hardware.mobo_vendor }} {{ report.hardware.mobo_product }}</span>
|
<span class="value text-ellipsis">{{ report.hardware.mobo_vendor }} {{ report.hardware.mobo_product }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
@@ -183,72 +207,45 @@ import { ref, computed, onUnmounted } from 'vue';
|
|||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { listen } from '@tauri-apps/api/event';
|
import { listen } from '@tauri-apps/api/event';
|
||||||
|
|
||||||
// 初始化 report 为包含空字段的对象,避免 v-if 报错
|
|
||||||
const emptyReport = () => ({
|
const emptyReport = () => ({
|
||||||
hardware: null,
|
hardware: null, storage: null, events: null, minidumps: null, drivers: null, battery: null
|
||||||
storage: null,
|
|
||||||
events: null,
|
|
||||||
minidumps: null,
|
|
||||||
drivers: null,
|
|
||||||
battery: null
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const report = ref(emptyReport());
|
const report = ref(emptyReport());
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const errorMsg = ref('');
|
const errorMsg = ref('');
|
||||||
const scanFinished = ref(false);
|
const showToast = ref(false);
|
||||||
const fileInput = ref(null);
|
const fileInput = ref(null);
|
||||||
let unlistenFns = []; // 存储事件取消函数
|
let unlistenFns = [];
|
||||||
|
let toastTimer = null;
|
||||||
|
|
||||||
const hasStorageDanger = computed(() => {
|
const hasStorageDanger = computed(() => report.value?.storage?.some(d => d.is_danger) || false);
|
||||||
return report.value?.storage?.some(d => d.is_danger) || false;
|
const isReportValid = computed(() => report.value && (report.value.hardware || report.value.storage));
|
||||||
});
|
|
||||||
|
|
||||||
const isReportValid = computed(() => {
|
// --- 辅助函数 ---
|
||||||
// 只要有任意一项数据,就认为可以导出
|
function calculatePercent(used, total) { return (!total || total === 0) ? 0 : Math.round((used / total) * 100); }
|
||||||
return report.value && (report.value.hardware || report.value.storage);
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- 辅助函数保持不变 ---
|
|
||||||
function calculatePercent(used, total) {
|
|
||||||
if (!total || total === 0) return 0;
|
|
||||||
return Math.round((used / total) * 100);
|
|
||||||
}
|
|
||||||
function getProgressStyle(used, total) {
|
function getProgressStyle(used, total) {
|
||||||
const percent = calculatePercent(used, total);
|
const percent = calculatePercent(used, total);
|
||||||
let color = '#2ed573';
|
let color = percent >= 90 ? '#ff4757' : (percent >= 75 ? '#ffa502' : '#2ed573');
|
||||||
if (percent >= 90) color = '#ff4757';
|
|
||||||
else if (percent >= 75) color = '#ffa502';
|
|
||||||
return { width: `${percent}%`, background: color };
|
return { width: `${percent}%`, background: color };
|
||||||
}
|
}
|
||||||
function getBatteryColor(percentage) {
|
function getBatteryColor(p) { return p < 50 ? '#ff4757' : (p < 80 ? '#ffa502' : '#2ed573'); }
|
||||||
if (percentage < 50) return '#ff4757';
|
function formatTime(t) { return !t ? "Unknown Time" : t.replace('T', ' ').substring(0, 19); }
|
||||||
if (percentage < 80) return '#ffa502';
|
|
||||||
return '#2ed573';
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 导出/导入 ---
|
// --- 导出/导入 ---
|
||||||
function exportReport() {
|
function exportReport() {
|
||||||
if (!isReportValid.value) return;
|
if (!isReportValid.value) return;
|
||||||
try {
|
try {
|
||||||
const dataStr = JSON.stringify(report.value, null, 2);
|
const blob = new Blob([JSON.stringify(report.value, null, 2)], { type: "application/json" });
|
||||||
const blob = new Blob([dataStr], { type: "application/json" });
|
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = url;
|
link.href = url;
|
||||||
const timestamp = new Date().toISOString().slice(0, 19).replace(/[:T]/g, "-");
|
link.download = `SystemDoctor_Report_${new Date().toISOString().slice(0, 19).replace(/[:T]/g, "-")}.json`;
|
||||||
link.download = `SystemDoctor_Report_${timestamp}.json`;
|
document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url);
|
||||||
document.body.appendChild(link);
|
} catch (err) { errorMsg.value = "导出失败: " + err.message; }
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
} catch (err) {
|
|
||||||
errorMsg.value = "导出失败: " + err.message;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function triggerImport() { fileInput.value.click(); }
|
function triggerImport() { fileInput.value.click(); }
|
||||||
|
|
||||||
function handleFileImport(event) {
|
function handleFileImport(event) {
|
||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
@@ -257,52 +254,36 @@ function handleFileImport(event) {
|
|||||||
try {
|
try {
|
||||||
const json = JSON.parse(e.target.result);
|
const json = JSON.parse(e.target.result);
|
||||||
if (json && (json.hardware || json.storage)) {
|
if (json && (json.hardware || json.storage)) {
|
||||||
report.value = json; // 导入时一次性覆盖
|
report.value = json; errorMsg.value = '';
|
||||||
scanFinished.value = false; // 导入的不显示“扫描完成”
|
} else { errorMsg.value = "无效的报告文件。"; }
|
||||||
errorMsg.value = '';
|
} catch (err) { errorMsg.value = "解析失败: " + err.message; }
|
||||||
} else {
|
|
||||||
errorMsg.value = "无效的报告文件。";
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
errorMsg.value = "解析失败: " + err.message;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
event.target.value = '';
|
event.target.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 扫描逻辑 (流式) ---
|
// --- 扫描逻辑 ---
|
||||||
async function startScan() {
|
async function startScan() {
|
||||||
// 1. 重置状态
|
|
||||||
report.value = emptyReport();
|
report.value = emptyReport();
|
||||||
errorMsg.value = '';
|
errorMsg.value = '';
|
||||||
scanFinished.value = false;
|
showToast.value = false; // 重置 Toast
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
// 2. 清理旧的监听器
|
if (unlistenFns.length > 0) { for (const fn of unlistenFns) fn(); unlistenFns = []; }
|
||||||
if (unlistenFns.length > 0) {
|
|
||||||
for (const fn of unlistenFns) fn();
|
|
||||||
unlistenFns = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 注册新的事件监听器
|
|
||||||
try {
|
try {
|
||||||
const l1 = await listen('report-hardware', (e) => report.value.hardware = e.payload);
|
const l1 = await listen('report-hardware', (e) => report.value.hardware = e.payload);
|
||||||
const l2 = await listen('report-storage', (e) => report.value.storage = e.payload);
|
const l2 = await listen('report-storage', (e) => report.value.storage = e.payload);
|
||||||
const l3 = await listen('report-drivers', (e) => report.value.drivers = e.payload);
|
const l3 = await listen('report-drivers', (e) => report.value.drivers = e.payload);
|
||||||
const l4 = await listen('report-minidumps', (e) => report.value.minidumps = e.payload);
|
const l4 = await listen('report-minidumps', (e) => report.value.minidumps = e.payload);
|
||||||
const l5 = await listen('report-battery', (e) => report.value.battery = e.payload); // 可能是null
|
const l5 = await listen('report-battery', (e) => report.value.battery = e.payload);
|
||||||
const l6 = await listen('report-events', (e) => report.value.events = e.payload);
|
const l6 = await listen('report-events', (e) => report.value.events = e.payload);
|
||||||
const l7 = await listen('diagnosis-finished', () => {
|
const l7 = await listen('diagnosis-finished', () => {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
scanFinished.value = true;
|
triggerToast(); // 触发 Toast
|
||||||
});
|
});
|
||||||
|
|
||||||
unlistenFns.push(l1, l2, l3, l4, l5, l6, l7);
|
unlistenFns.push(l1, l2, l3, l4, l5, l6, l7);
|
||||||
|
|
||||||
// 4. 触发后端任务 (fire and forget)
|
|
||||||
await invoke('run_diagnosis');
|
await invoke('run_diagnosis');
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
errorMsg.value = "启动扫描失败: " + e;
|
errorMsg.value = "启动扫描失败: " + e;
|
||||||
@@ -310,149 +291,164 @@ async function startScan() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 组件销毁时清理监听
|
// --- Toast 逻辑 ---
|
||||||
|
function triggerToast() {
|
||||||
|
showToast.value = true;
|
||||||
|
if (toastTimer) clearTimeout(toastTimer);
|
||||||
|
toastTimer = setTimeout(() => {
|
||||||
|
showToast.value = false;
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
for (const fn of unlistenFns) fn();
|
for (const fn of unlistenFns) fn();
|
||||||
|
if (toastTimer) clearTimeout(toastTimer);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- 全局样式:解决滚动条问题 -->
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0; padding: 0; background-color: #f4f6f9; /* 更柔和的背景 */
|
||||||
padding: 0;
|
overflow-y: scroll;
|
||||||
background-color: #f0f2f5; /* 背景色移到这里,铺满全屏 */
|
|
||||||
overflow-y: scroll; /* 始终显示滚动条轨道(防止跳动)或者设为 auto */
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 基础布局 */
|
/* 容器调整 */
|
||||||
.container {
|
.container {
|
||||||
max-width: 1200px;
|
max-width: 1200px; margin: 0 auto; padding: 20px 30px; box-sizing: border-box;
|
||||||
margin: 0 auto;
|
font-family: 'Segoe UI', system-ui, sans-serif; color: #2c3e50;
|
||||||
padding: 30px 20px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
||||||
color: #2c3e50;
|
|
||||||
/* 移除 min-height: 100vh 和背景色,让它自然伸缩,背景由 body 接管 */
|
|
||||||
/* 这样内容少时高度不够 100vh 就不会触发滚动条 */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header { text-align: center; margin-bottom: 30px; }
|
/* --- 顶部标题栏 --- */
|
||||||
h1 {
|
.header-bar {
|
||||||
font-size: 2rem;
|
display: flex; justify-content: space-between; align-items: center;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 25px; padding-bottom: 15px; border-bottom: 1px solid #e1e4e8;
|
||||||
margin-top: 0; /* 关键:防止margin合并导致的滚动条 */
|
|
||||||
color: #2c3e50;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
}
|
||||||
.subtitle { color: #7f8c8d; font-size: 1rem; margin-top: 0; /* 同样防止margin合并 */ }
|
.brand { display: flex; align-items: center; gap: 10px; }
|
||||||
|
.brand .icon { font-size: 1.8rem; }
|
||||||
|
h1 { font-size: 1.5rem; margin: 0; color: #2c3e50; font-weight: 700; letter-spacing: -0.5px; }
|
||||||
|
.version-tag { font-size: 0.85rem; color: #95a5a6; font-weight: 500; }
|
||||||
|
|
||||||
.actions { display: flex; flex-direction: column; align-items: center; margin-bottom: 35px; gap: 15px; }
|
/* --- 工具栏布局 --- */
|
||||||
.secondary-actions { display: flex; gap: 15px; }
|
.toolbar {
|
||||||
|
display: flex; justify-content: space-between; align-items: center;
|
||||||
.scan-btn {
|
margin-bottom: 30px; background: white; padding: 10px 15px;
|
||||||
background: linear-gradient(135deg, #42b983 0%, #2ecc71 100%);
|
border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||||
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 {
|
.left-actions { display: flex; align-items: center; }
|
||||||
background: white; border: 1px solid #dcdfe6; color: #606266; padding: 8px 20px; font-size: 0.9rem; border-radius: 20px;
|
.right-actions { display: flex; gap: 10px; }
|
||||||
cursor: pointer; transition: all 0.2s; display: flex; align-items: center; gap: 5px;
|
|
||||||
|
/* 主按钮 (绿色强调) */
|
||||||
|
.primary-btn {
|
||||||
|
background: #2ecc71; color: white; border: none;
|
||||||
|
padding: 10px 24px; font-size: 1rem; border-radius: 8px;
|
||||||
|
cursor: pointer; font-weight: 600; display: flex; align-items: center; gap: 8px;
|
||||||
|
transition: background 0.2s, transform 0.1s;
|
||||||
}
|
}
|
||||||
.secondary-btn:hover:not(:disabled) { color: #409eff; border-color: #c6e2ff; background-color: #ecf5ff; }
|
.primary-btn:hover:not(:disabled) { background: #27ae60; transform: translateY(-1px); }
|
||||||
.secondary-btn:disabled { color: #c0c4cc; cursor: not-allowed; background-color: #f5f7fa; }
|
.primary-btn:active:not(:disabled) { transform: translateY(1px); }
|
||||||
|
.primary-btn:disabled { background: #bdc3c7; cursor: not-allowed; }
|
||||||
|
.icon-spin { animation: spin 1s linear infinite; }
|
||||||
|
|
||||||
.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; }
|
/* 辅助按钮 (白色简约) */
|
||||||
|
.icon-btn {
|
||||||
|
background: transparent; border: 1px solid transparent; color: #555;
|
||||||
|
padding: 8px 16px; font-size: 0.95rem; border-radius: 8px;
|
||||||
|
cursor: pointer; transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.icon-btn:hover:not(:disabled) { background: #f0f2f5; color: #2c3e50; }
|
||||||
|
.icon-btn:disabled { color: #ccc; cursor: not-allowed; }
|
||||||
|
|
||||||
.dashboard { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 25px; }
|
/* --- Toast 通知 --- */
|
||||||
.fade-in { animation: fadeIn 0.4s ease-out; }
|
.toast-notification {
|
||||||
@keyframes fadeIn { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
|
position: fixed; top: 25px; right: 25px; z-index: 9999;
|
||||||
|
background: white; border-left: 5px solid #2ecc71;
|
||||||
|
padding: 15px 20px; border-radius: 8px;
|
||||||
|
box-shadow: 0 5px 20px rgba(0,0,0,0.15);
|
||||||
|
min-width: 280px; overflow: hidden;
|
||||||
|
}
|
||||||
|
.toast-content { display: flex; align-items: flex-start; gap: 12px; }
|
||||||
|
.check-icon { font-size: 1.2rem; }
|
||||||
|
.toast-text h4 { margin: 0 0 4px 0; font-size: 1rem; color: #2c3e50; }
|
||||||
|
.toast-text p { margin: 0; font-size: 0.85rem; color: #7f8c8d; }
|
||||||
|
|
||||||
|
/* 简单的倒计时进度条动画 */
|
||||||
|
.toast-progress {
|
||||||
|
position: absolute; bottom: 0; left: 0; height: 3px; background: #2ecc71;
|
||||||
|
width: 100%; animation: progress 3s linear forwards;
|
||||||
|
}
|
||||||
|
@keyframes progress { from { width: 100%; } to { width: 0%; } }
|
||||||
|
|
||||||
|
/* Toast 进出动画 */
|
||||||
|
.toast-slide-enter-active, .toast-slide-leave-active { transition: all 0.3s ease; }
|
||||||
|
.toast-slide-enter-from, .toast-slide-leave-to { transform: translateX(50px); opacity: 0; }
|
||||||
|
|
||||||
|
.inline-error {
|
||||||
|
background: #ffeaea; color: #c0392b; padding: 10px; border-radius: 6px; margin-bottom: 20px; font-size: 0.9rem; font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- 动画与通用 --- */
|
||||||
|
@keyframes spin { 100% { transform: rotate(360deg); } }
|
||||||
|
.dashboard { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 20px; }
|
||||||
|
.slide-up { animation: slideUp 0.4s ease-out forwards; }
|
||||||
|
@keyframes slideUp { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
|
|
||||||
|
/* 卡片样式 (微调更紧凑) */
|
||||||
.card {
|
.card {
|
||||||
background: white; border-radius: 12px; padding: 20px; box-shadow: 0 5px 15px rgba(0,0,0,0.04);
|
background: white; border-radius: 10px; padding: 20px;
|
||||||
border-top: 4px solid #42b983; transition: transform 0.2s, box-shadow 0.2s;
|
box-shadow: 0 2px 10px rgba(0,0,0,0.03); border: 1px solid #eaecf0;
|
||||||
border-left: 1px solid #eee; border-right: 1px solid #eee; border-bottom: 1px solid #eee;
|
border-top: 3px solid #2ecc71;
|
||||||
}
|
}
|
||||||
.card:hover { transform: translateY(-2px); box-shadow: 0 8px 25px rgba(0,0,0,0.08); }
|
.card:hover { box-shadow: 0 8px 20px rgba(0,0,0,0.06); transform: translateY(-2px); transition: all 0.2s; }
|
||||||
.card.danger { border-top-color: #ff4757; background: #fffbfb; }
|
.card.danger { border-top-color: #ff4757; }
|
||||||
.card.success { border-top-color: #2ed573; }
|
.card.summary { border-top-color: #3498db; grid-column: 1 / -1; }
|
||||||
.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; }
|
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #f0f2f5; }
|
||||||
h2 { font-size: 1.2rem; margin: 0; font-weight: 600; }
|
h2 { font-size: 1.1rem; margin: 0; font-weight: 600; }
|
||||||
|
|
||||||
.grid-container-summary { display: grid; grid-template-columns: 1fr 1.5fr; gap: 30px; align-items: start; }
|
.grid-container-summary { display: grid; grid-template-columns: 1fr 1.5fr; gap: 25px; }
|
||||||
@media (max-width: 768px) { .grid-container-summary { grid-template-columns: 1fr; gap: 20px; } }
|
@media (max-width: 768px) { .grid-container-summary { grid-template-columns: 1fr; } }
|
||||||
|
|
||||||
.basic-info, .usage-bars { display: flex; flex-direction: column; gap: 15px; }
|
.basic-info, .usage-bars { display: flex; flex-direction: column; gap: 12px; }
|
||||||
.usage-bars { padding-top: 10px; }
|
.usage-bars { padding-top: 5px; }
|
||||||
.usage-item { display: flex; flex-direction: column; gap: 8px; }
|
.usage-item { display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
|
||||||
.info-item { display: flex; flex-direction: column; }
|
.info-item { display: flex; flex-direction: column; }
|
||||||
.label { font-size: 0.8rem; color: #95a5a6; margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; }
|
.label { font-size: 0.75rem; color: #95a5a6; margin-bottom: 2px; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; }
|
||||||
.value { font-size: 1.05rem; font-weight: 500; color: #2c3e50; }
|
.value { font-size: 1rem; font-weight: 500; color: #2c3e50; }
|
||||||
.text-ellipsis { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.text-ellipsis { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
|
||||||
.progress-label { display: flex; justify-content: space-between; font-size: 0.9rem; color: #636e72; }
|
.progress-label { display: flex; justify-content: space-between; font-size: 0.85rem; color: #636e72; }
|
||||||
.progress-label strong { color: #2c3e50; font-size: 1rem; }
|
.progress-bar-large { width: 100%; height: 16px; background: #f1f3f5; border-radius: 8px; overflow: hidden; }
|
||||||
.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 ease; }
|
||||||
.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-container { display: flex; flex-direction: column; gap: 10px; }
|
||||||
.list-item { background: #fcfdfe; padding: 12px 15px; border-radius: 8px; border: 1px solid #ececec; }
|
.list-item { background: #f8f9fa; padding: 10px 14px; border-radius: 6px; border: 1px solid #edf2f7; }
|
||||||
.list-item.error-item { background: #fff5f5; border-color: #ffcccc; }
|
.list-item.error-item { background: #fff5f5; border-color: #fed7d7; }
|
||||||
.list-item.warning-item { background: #fffcf0; border-color: #ffeaa7; }
|
.list-item.warning-item { background: #fffaf0; border-color: #fce588; }
|
||||||
|
|
||||||
.item-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; flex-wrap: wrap; gap: 5px; }
|
.item-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; }
|
||||||
.description { margin: 0; color: #636e72; font-size: 0.9rem; line-height: 1.5; }
|
.description { margin: 0; color: #636e72; font-size: 0.9rem; line-height: 1.4; }
|
||||||
.description.highlight { color: #d35400; font-weight: 500; }
|
.description.highlight { color: #d35400; font-weight: 500; }
|
||||||
.description.raw-message { margin-top: 5px; font-size: 0.85rem; color: #7f8c8d; font-family: Consolas, monospace; background: #f0f3f4; padding: 5px 8px; border-radius: 4px; word-break: break-all; }
|
.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; }
|
||||||
|
|
||||||
.status-text { font-weight: 600; font-size: 0.9rem; }
|
.status-text { font-weight: 600; font-size: 0.9rem; }
|
||||||
.text-green { color: #27ae60; }
|
.text-green { color: #27ae60; } .text-red { color: #c0392b; }
|
||||||
.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 { font-size: 0.7rem; padding: 2px 8px; border-radius: 10px; color: white; font-weight: 600; }
|
||||||
.badge-green { background-color: #2ed573; }
|
.badge-green { background-color: #2ed573; } .badge-red { background-color: #ff4757; } .badge-blue { background-color: #3498db; } .badge-gray { background-color: #95a5a6; }
|
||||||
.badge-red { background-color: #ff4757; }
|
.code-tag { background: #ff4757; color: white; padding: 1px 5px; border-radius: 3px; font-size: 0.75rem; font-weight: bold; }
|
||||||
.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; }
|
.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: 15px; font-size: 0.85rem; color: #d35400; background: #fff3e0; padding: 12px; border-radius: 6px; border: 1px solid #ffe0b2; }
|
.tip { margin-top: 12px; font-size: 0.85rem; color: #d35400; background: #fff3e0; padding: 10px; border-radius: 6px; border: 1px solid #ffe0b2; }
|
||||||
|
|
||||||
.progress-wrapper { margin-bottom: 15px; }
|
.progress-wrapper { margin-bottom: 12px; }
|
||||||
.progress-bar { width: 100%; height: 10px; background: #e6e8ec; border-radius: 5px; overflow: hidden; }
|
.progress-bar { width: 100%; height: 8px; background: #f1f3f5; border-radius: 4px; overflow: hidden; }
|
||||||
|
|
||||||
.event-id, .event-source, .event-time { font-size: 0.75rem; color: #7f8c8d; }
|
.event-id { font-weight: bold; color: #2c3e50; background: #e2e8f0; padding: 1px 5px; border-radius: 3px; margin-right: 8px; font-size: 0.75rem; }
|
||||||
.event-id { font-weight: bold; color: #2c3e50; background: #dfe6e9; padding: 1px 5px; border-radius: 4px; margin-right: 8px; }
|
.event-time { margin-left: auto; font-size: 0.75rem; color: #95a5a6; }
|
||||||
.event-time { margin-left: auto; }
|
|
||||||
.content-box { padding: 5px 0; }
|
.content-box { padding: 5px 0; }
|
||||||
.main-text { font-weight: 500; color: #2c3e50; margin-bottom: 10px; }
|
.main-text { font-weight: 500; color: #2c3e50; margin-bottom: 8px; }
|
||||||
|
|
||||||
/* 动画部分 */
|
|
||||||
@keyframes slideUp {
|
|
||||||
from { opacity: 0; transform: translateY(20px); }
|
|
||||||
to { opacity: 1; transform: translateY(0); }
|
|
||||||
}
|
|
||||||
.slide-up { animation: slideUp 0.5s ease-out forwards; }
|
|
||||||
|
|
||||||
.success-box {
|
|
||||||
margin-top: 10px;
|
|
||||||
padding: 12px 20px;
|
|
||||||
background-color: #eafaf1;
|
|
||||||
color: #27ae60;
|
|
||||||
border: 1px solid #d4efdf;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-weight: 600;
|
|
||||||
text-align: center;
|
|
||||||
animation: slideUp 0.3s ease-out;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
Reference in New Issue
Block a user