custom export location
This commit is contained in:
96
src/App.vue
96
src/App.vue
@@ -39,14 +39,14 @@
|
||||
⚠️ {{ errorMsg }}
|
||||
</div>
|
||||
|
||||
<!-- 右上角 Toast 通知 (扫描完成提示) -->
|
||||
<!-- 右上角 Toast 通知 (通用化) -->
|
||||
<transition name="toast-slide">
|
||||
<div v-if="showToast" class="toast-notification">
|
||||
<div v-if="toast.show" class="toast-notification" :class="toast.type">
|
||||
<div class="toast-content">
|
||||
<span class="check-icon">✅</span>
|
||||
<span class="check-icon">{{ toast.type === 'error' ? '❌' : '✅' }}</span>
|
||||
<div class="toast-text">
|
||||
<h4>扫描已完成</h4>
|
||||
<p>所有硬件与日志检查完毕</p>
|
||||
<h4>{{ toast.title }}</h4>
|
||||
<p>{{ toast.message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toast-progress"></div>
|
||||
@@ -203,7 +203,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onUnmounted } from 'vue';
|
||||
import { ref, computed, onUnmounted, reactive } from 'vue';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
|
||||
@@ -214,11 +214,18 @@ const emptyReport = () => ({
|
||||
const report = ref(emptyReport());
|
||||
const loading = ref(false);
|
||||
const errorMsg = ref('');
|
||||
const showToast = ref(false);
|
||||
const fileInput = ref(null);
|
||||
let unlistenFns = [];
|
||||
let toastTimer = null;
|
||||
|
||||
// 通用 Toast 状态
|
||||
const toast = reactive({
|
||||
show: false,
|
||||
title: '',
|
||||
message: '',
|
||||
type: 'success' // 'success' | 'error'
|
||||
});
|
||||
|
||||
const hasStorageDanger = computed(() => report.value?.storage?.some(d => d.is_danger) || false);
|
||||
const isReportValid = computed(() => report.value && (report.value.hardware || report.value.storage));
|
||||
|
||||
@@ -233,16 +240,49 @@ function getBatteryColor(p) { return p < 50 ? '#ff4757' : (p < 80 ? '#ffa502' :
|
||||
function formatTime(t) { return !t ? "Unknown Time" : t.replace('T', ' ').substring(0, 19); }
|
||||
|
||||
// --- 导出/导入 ---
|
||||
function exportReport() {
|
||||
async function exportReport() {
|
||||
if (!isReportValid.value) return;
|
||||
const fileName = `SystemDoctor_Report_${new Date().toISOString().slice(0, 19).replace(/[:T]/g, "-")}.json`;
|
||||
const content = JSON.stringify(report.value, null, 2);
|
||||
|
||||
// 尝试使用现代文件系统 API (显示保存对话框)
|
||||
if (window.showSaveFilePicker) {
|
||||
try {
|
||||
const handle = await window.showSaveFilePicker({
|
||||
suggestedName: fileName,
|
||||
types: [{
|
||||
description: 'JSON Report',
|
||||
accept: { 'application/json': ['.json'] },
|
||||
}],
|
||||
});
|
||||
const writable = await handle.createWritable();
|
||||
await writable.write(content);
|
||||
await writable.close();
|
||||
triggerToast('导出成功', '文件已保存到指定位置', 'success');
|
||||
return;
|
||||
} catch (err) {
|
||||
// 用户取消不报错,其他错误降级处理
|
||||
if (err.name !== 'AbortError') {
|
||||
console.warn('Native save dialog failed, falling back to download link', err);
|
||||
} else {
|
||||
return; // 用户取消
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 降级方案:创建链接直接下载 (默认下载文件夹)
|
||||
try {
|
||||
const blob = new Blob([JSON.stringify(report.value, null, 2)], { type: "application/json" });
|
||||
const blob = new Blob([content], { 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`;
|
||||
link.download = fileName;
|
||||
document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url);
|
||||
} catch (err) { errorMsg.value = "导出失败: " + err.message; }
|
||||
triggerToast('导出成功', '文件已保存到默认下载目录', 'success');
|
||||
} catch (err) {
|
||||
errorMsg.value = "导出失败: " + err.message;
|
||||
triggerToast('导出失败', err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function triggerImport() { fileInput.value.click(); }
|
||||
@@ -254,9 +294,17 @@ function handleFileImport(event) {
|
||||
try {
|
||||
const json = JSON.parse(e.target.result);
|
||||
if (json && (json.hardware || json.storage)) {
|
||||
report.value = json; errorMsg.value = '';
|
||||
} else { errorMsg.value = "无效的报告文件。"; }
|
||||
} catch (err) { errorMsg.value = "解析失败: " + err.message; }
|
||||
report.value = json;
|
||||
errorMsg.value = '';
|
||||
triggerToast('导入成功', '已加载历史报告数据', 'success');
|
||||
} else {
|
||||
errorMsg.value = "无效的报告文件。";
|
||||
triggerToast('导入失败', '文件格式不正确', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
errorMsg.value = "解析失败: " + err.message;
|
||||
triggerToast('导入失败', err.message, 'error');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
event.target.value = '';
|
||||
@@ -266,7 +314,6 @@ function handleFileImport(event) {
|
||||
async function startScan() {
|
||||
report.value = emptyReport();
|
||||
errorMsg.value = '';
|
||||
showToast.value = false; // 重置 Toast
|
||||
loading.value = true;
|
||||
|
||||
if (unlistenFns.length > 0) { for (const fn of unlistenFns) fn(); unlistenFns = []; }
|
||||
@@ -280,7 +327,7 @@ async function startScan() {
|
||||
const l6 = await listen('report-events', (e) => report.value.events = e.payload);
|
||||
const l7 = await listen('diagnosis-finished', () => {
|
||||
loading.value = false;
|
||||
triggerToast(); // 触发 Toast
|
||||
triggerToast('扫描已完成', '所有硬件与日志检查完毕', 'success');
|
||||
});
|
||||
unlistenFns.push(l1, l2, l3, l4, l5, l6, l7);
|
||||
await invoke('run_diagnosis');
|
||||
@@ -288,15 +335,20 @@ async function startScan() {
|
||||
console.error(e);
|
||||
errorMsg.value = "启动扫描失败: " + e;
|
||||
loading.value = false;
|
||||
triggerToast('扫描失败', '无法启动后台诊断程序', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// --- Toast 逻辑 ---
|
||||
function triggerToast() {
|
||||
showToast.value = true;
|
||||
function triggerToast(title, message, type = 'success') {
|
||||
toast.title = title;
|
||||
toast.message = message;
|
||||
toast.type = type;
|
||||
toast.show = true;
|
||||
|
||||
if (toastTimer) clearTimeout(toastTimer);
|
||||
toastTimer = setTimeout(() => {
|
||||
showToast.value = false;
|
||||
toast.show = false;
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
@@ -361,7 +413,7 @@ h1 { font-size: 1.5rem; margin: 0; color: #2c3e50; font-weight: 700; letter-spac
|
||||
.icon-btn:hover:not(:disabled) { background: #f0f2f5; color: #2c3e50; }
|
||||
.icon-btn:disabled { color: #ccc; cursor: not-allowed; }
|
||||
|
||||
/* --- Toast 通知 --- */
|
||||
/* --- Toast 通知 (增强版) --- */
|
||||
.toast-notification {
|
||||
position: fixed; top: 25px; right: 25px; z-index: 9999;
|
||||
background: white; border-left: 5px solid #2ecc71;
|
||||
@@ -369,6 +421,8 @@ h1 { font-size: 1.5rem; margin: 0; color: #2c3e50; font-weight: 700; letter-spac
|
||||
box-shadow: 0 5px 20px rgba(0,0,0,0.15);
|
||||
min-width: 280px; overflow: hidden;
|
||||
}
|
||||
.toast-notification.error { border-left-color: #ff4757; }
|
||||
|
||||
.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; }
|
||||
@@ -379,6 +433,8 @@ h1 { font-size: 1.5rem; margin: 0; color: #2c3e50; font-weight: 700; letter-spac
|
||||
position: absolute; bottom: 0; left: 0; height: 3px; background: #2ecc71;
|
||||
width: 100%; animation: progress 3s linear forwards;
|
||||
}
|
||||
.toast-notification.error .toast-progress { background: #ff4757; }
|
||||
|
||||
@keyframes progress { from { width: 100%; } to { width: 0%; } }
|
||||
|
||||
/* Toast 进出动画 */
|
||||
|
||||
Reference in New Issue
Block a user