improve title ui

This commit is contained in:
Julian Freeman
2025-11-25 23:49:47 -04:00
parent 52c0bd5bb3
commit fe330511b9

View File

@@ -1,34 +1,58 @@
<template>
<div class="container">
<header class="header">
<h1>🏥 电脑健康体检中心 Pro</h1>
<p class="subtitle">静态排查仪表盘 - Powered by Rust & Tauri</p>
<!-- 顶部标题栏 -->
<header class="header-bar">
<div class="brand">
<span class="icon">🩺</span>
<h1>静态排查仪表盘</h1>
</div>
<span class="version-tag">Powered by Rust & Tauri</span>
</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="!isReportValid || loading">
💾 导出报告
<!-- 工具栏左侧主操作右侧辅助操作 -->
<div class="toolbar">
<div class="left-actions">
<button @click="startScan" :disabled="loading" class="primary-btn" :class="{ 'scanning': loading }">
<span v-if="loading" class="icon-spin">🔄</span>
<span v-if="loading">正在排查...</span>
<template v-else>
<span class="btn-icon">🔍</span> 开始全面体检
</template>
</button>
</div>
<input type="file" ref="fileInput" @change="handleFileImport" accept=".json" style="display: none" />
<div v-if="errorMsg" class="error-box"> {{ errorMsg }}</div>
<!-- 完成提示 -->
<div v-if="scanFinished" class="success-box"> 扫描已完成所有项目检查完毕</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" />
<!-- 错误提示 (保留在原本位置或顶部) -->
<div v-if="errorMsg" class="inline-error">
{{ errorMsg }}
</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">
@@ -44,11 +68,11 @@
<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="label">整机型号</span>
<span class="value text-ellipsis">{{ report.hardware.sys_vendor }} {{ report.hardware.sys_product }}</span>
</div>
<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>
</div>
<div class="info-item">
@@ -183,72 +207,45 @@ import { ref, computed, onUnmounted } from 'vue';
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
// 初始化 report 为包含空字段的对象,避免 v-if 报错
const emptyReport = () => ({
hardware: null,
storage: null,
events: null,
minidumps: null,
drivers: null,
battery: null
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 showToast = ref(false);
const fileInput = ref(null);
let unlistenFns = []; // 存储事件取消函数
let unlistenFns = [];
let toastTimer = null;
const hasStorageDanger = computed(() => {
return report.value?.storage?.some(d => d.is_danger) || false;
});
const hasStorageDanger = computed(() => report.value?.storage?.some(d => d.is_danger) || false);
const isReportValid = computed(() => report.value && (report.value.hardware || report.value.storage));
const isReportValid = computed(() => {
// 只要有任意一项数据,就认为可以导出
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 calculatePercent(used, total) { return (!total || total === 0) ? 0 : 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';
let color = percent >= 90 ? '#ff4757' : (percent >= 75 ? '#ffa502' : '#2ed573');
return { width: `${percent}%`, background: color };
}
function getBatteryColor(percentage) {
if (percentage < 50) return '#ff4757';
if (percentage < 80) return '#ffa502';
return '#2ed573';
}
function getBatteryColor(p) { return p < 50 ? '#ff4757' : (p < 80 ? '#ffa502' : '#2ed573'); }
function formatTime(t) { return !t ? "Unknown Time" : t.replace('T', ' ').substring(0, 19); }
// --- 导出/导入 ---
function exportReport() {
if (!isReportValid.value) return;
try {
const dataStr = JSON.stringify(report.value, null, 2);
const blob = new Blob([dataStr], { type: "application/json" });
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;
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;
}
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);
} catch (err) { errorMsg.value = "导出失败: " + err.message; }
}
function triggerImport() { fileInput.value.click(); }
function handleFileImport(event) {
const file = event.target.files[0];
if (!file) return;
@@ -257,52 +254,36 @@ function handleFileImport(event) {
try {
const json = JSON.parse(e.target.result);
if (json && (json.hardware || json.storage)) {
report.value = json; // 导入时一次性覆盖
scanFinished.value = false; // 导入的不显示“扫描完成”
errorMsg.value = '';
} else {
errorMsg.value = "无效的报告文件。";
}
} catch (err) {
errorMsg.value = "解析失败: " + err.message;
}
report.value = json; errorMsg.value = '';
} else { errorMsg.value = "无效的报告文件。"; }
} catch (err) { errorMsg.value = "解析失败: " + err.message; }
};
reader.readAsText(file);
event.target.value = '';
}
// --- 扫描逻辑 (流式) ---
// --- 扫描逻辑 ---
async function startScan() {
// 1. 重置状态
report.value = emptyReport();
errorMsg.value = '';
scanFinished.value = false;
showToast.value = false; // 重置 Toast
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 {
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 l3 = await listen('report-drivers', (e) => report.value.drivers = 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 l7 = await listen('diagnosis-finished', () => {
loading.value = false;
scanFinished.value = true;
triggerToast(); // 触发 Toast
});
unlistenFns.push(l1, l2, l3, l4, l5, l6, l7);
// 4. 触发后端任务 (fire and forget)
await invoke('run_diagnosis');
} catch (e) {
console.error(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(() => {
for (const fn of unlistenFns) fn();
if (toastTimer) clearTimeout(toastTimer);
});
</script>
<!-- 全局样式解决滚动条问题 -->
<style>
body {
margin: 0;
padding: 0;
background-color: #f0f2f5; /* 背景色移到这里,铺满全屏 */
overflow-y: scroll; /* 始终显示滚动条轨道(防止跳动)或者设为 auto */
margin: 0; padding: 0; background-color: #f4f6f9; /* 更柔和的背景 */
overflow-y: scroll;
}
</style>
<style scoped>
/* 基础布局 */
/* 容器调整 */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 30px 20px;
box-sizing: border-box;
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
color: #2c3e50;
/* 移除 min-height: 100vh 和背景色,让它自然伸缩,背景由 body 接管 */
/* 这样内容少时高度不够 100vh 就不会触发滚动条 */
max-width: 1200px; margin: 0 auto; padding: 20px 30px; box-sizing: border-box;
font-family: 'Segoe UI', system-ui, sans-serif; color: #2c3e50;
}
.header { text-align: center; margin-bottom: 30px; }
h1 {
font-size: 2rem;
margin-bottom: 8px;
margin-top: 0; /* 关键防止margin合并导致的滚动条 */
color: #2c3e50;
letter-spacing: -0.5px;
/* --- 顶部标题栏 --- */
.header-bar {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 25px; padding-bottom: 15px; border-bottom: 1px solid #e1e4e8;
}
.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; }
.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;
/* --- 工具栏布局 --- */
.toolbar {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 30px; background: white; padding: 10px 15px;
border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.04);
}
.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;
.left-actions { display: flex; align-items: center; }
.right-actions { display: flex; gap: 10px; }
/* 主按钮 (绿色强调) */
.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; }
.secondary-btn:disabled { color: #c0c4cc; cursor: not-allowed; background-color: #f5f7fa; }
.primary-btn:hover:not(:disabled) { background: #27ae60; transform: translateY(-1px); }
.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; }
.fade-in { animation: fadeIn 0.4s ease-out; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
/* --- Toast 通知 --- */
.toast-notification {
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 {
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;
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: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: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; }
.card.summary { border-top-color: #3498db; 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; }
.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.1rem; 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; } }
.grid-container-summary { display: grid; grid-template-columns: 1fr 1.5fr; gap: 25px; }
@media (max-width: 768px) { .grid-container-summary { grid-template-columns: 1fr; } }
.basic-info, .usage-bars { display: flex; flex-direction: column; gap: 15px; }
.usage-bars { padding-top: 10px; }
.usage-item { display: flex; flex-direction: column; gap: 8px; }
.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.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; }
.label { font-size: 0.75rem; color: #95a5a6; margin-bottom: 2px; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; }
.value { font-size: 1rem; font-weight: 500; color: #2c3e50; }
.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 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; }
.progress-label { display: flex; justify-content: space-between; font-size: 0.85rem; color: #636e72; }
.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: 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; }
.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; }
.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; }
.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.4; }
.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; }
.text-green { color: #27ae60; }
.text-red { color: #c0392b; }
.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; }
.badge { font-size: 0.7rem; padding: 2px 8px; border-radius: 10px; color: white; font-weight: 600; }
.badge-green { background-color: #2ed573; } .badge-red { background-color: #ff4757; } .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: 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; }
.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: 15px; }
.progress-bar { width: 100%; height: 10px; background: #e6e8ec; border-radius: 5px; overflow: hidden; }
.progress-wrapper { margin-bottom: 12px; }
.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: #dfe6e9; padding: 1px 5px; border-radius: 4px; margin-right: 8px; }
.event-time { margin-left: auto; }
.event-id { font-weight: bold; color: #2c3e50; background: #e2e8f0; padding: 1px 5px; border-radius: 3px; margin-right: 8px; font-size: 0.75rem; }
.event-time { margin-left: auto; font-size: 0.75rem; color: #95a5a6; }
.content-box { padding: 5px 0; }
.main-text { font-weight: 500; color: #2c3e50; margin-bottom: 10px; }
/* 动画部分 */
@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;
}
.main-text { font-weight: 500; color: #2c3e50; margin-bottom: 8px; }
</style>