custom export location

This commit is contained in:
Julian Freeman
2025-11-25 23:52:30 -04:00
parent fe330511b9
commit 4fc501f941

View File

@@ -39,14 +39,14 @@
{{ errorMsg }} {{ errorMsg }}
</div> </div>
<!-- 右上角 Toast 通知 (扫描完成提示) --> <!-- 右上角 Toast 通知 (通用化) -->
<transition name="toast-slide"> <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"> <div class="toast-content">
<span class="check-icon"></span> <span class="check-icon">{{ toast.type === 'error' ? '❌' : '✅' }}</span>
<div class="toast-text"> <div class="toast-text">
<h4>扫描已完成</h4> <h4>{{ toast.title }}</h4>
<p>所有硬件与日志检查完毕</p> <p>{{ toast.message }}</p>
</div> </div>
</div> </div>
<div class="toast-progress"></div> <div class="toast-progress"></div>
@@ -203,7 +203,7 @@
</template> </template>
<script setup> <script setup>
import { ref, computed, onUnmounted } from 'vue'; import { ref, computed, onUnmounted, reactive } 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';
@@ -214,11 +214,18 @@ const emptyReport = () => ({
const report = ref(emptyReport()); const report = ref(emptyReport());
const loading = ref(false); const loading = ref(false);
const errorMsg = ref(''); const errorMsg = ref('');
const showToast = ref(false);
const fileInput = ref(null); const fileInput = ref(null);
let unlistenFns = []; let unlistenFns = [];
let toastTimer = null; 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 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(() => 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 formatTime(t) { return !t ? "Unknown Time" : t.replace('T', ' ').substring(0, 19); }
// --- 导出/导入 --- // --- 导出/导入 ---
function exportReport() { async function exportReport() {
if (!isReportValid.value) return; 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 { try {
const blob = new Blob([JSON.stringify(report.value, null, 2)], { type: "application/json" }); 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([content], { 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;
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); 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(); } function triggerImport() { fileInput.value.click(); }
@@ -254,9 +294,17 @@ 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; errorMsg.value = ''; report.value = json;
} else { errorMsg.value = "无效的报告文件。"; } errorMsg.value = '';
} catch (err) { errorMsg.value = "解析失败: " + err.message; } triggerToast('导入成功', '已加载历史报告数据', 'success');
} else {
errorMsg.value = "无效的报告文件。";
triggerToast('导入失败', '文件格式不正确', 'error');
}
} catch (err) {
errorMsg.value = "解析失败: " + err.message;
triggerToast('导入失败', err.message, 'error');
}
}; };
reader.readAsText(file); reader.readAsText(file);
event.target.value = ''; event.target.value = '';
@@ -266,7 +314,6 @@ function handleFileImport(event) {
async function startScan() { async function startScan() {
report.value = emptyReport(); report.value = emptyReport();
errorMsg.value = ''; errorMsg.value = '';
showToast.value = false; // 重置 Toast
loading.value = true; loading.value = true;
if (unlistenFns.length > 0) { for (const fn of unlistenFns) fn(); unlistenFns = []; } 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 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;
triggerToast(); // 触发 Toast triggerToast('扫描已完成', '所有硬件与日志检查完毕', 'success');
}); });
unlistenFns.push(l1, l2, l3, l4, l5, l6, l7); unlistenFns.push(l1, l2, l3, l4, l5, l6, l7);
await invoke('run_diagnosis'); await invoke('run_diagnosis');
@@ -288,15 +335,20 @@ async function startScan() {
console.error(e); console.error(e);
errorMsg.value = "启动扫描失败: " + e; errorMsg.value = "启动扫描失败: " + e;
loading.value = false; loading.value = false;
triggerToast('扫描失败', '无法启动后台诊断程序', 'error');
} }
} }
// --- Toast 逻辑 --- // --- Toast 逻辑 ---
function triggerToast() { function triggerToast(title, message, type = 'success') {
showToast.value = true; toast.title = title;
toast.message = message;
toast.type = type;
toast.show = true;
if (toastTimer) clearTimeout(toastTimer); if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(() => { toastTimer = setTimeout(() => {
showToast.value = false; toast.show = false;
}, 3000); }, 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:hover:not(:disabled) { background: #f0f2f5; color: #2c3e50; }
.icon-btn:disabled { color: #ccc; cursor: not-allowed; } .icon-btn:disabled { color: #ccc; cursor: not-allowed; }
/* --- Toast 通知 --- */ /* --- Toast 通知 (增强版) --- */
.toast-notification { .toast-notification {
position: fixed; top: 25px; right: 25px; z-index: 9999; position: fixed; top: 25px; right: 25px; z-index: 9999;
background: white; border-left: 5px solid #2ecc71; 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); box-shadow: 0 5px 20px rgba(0,0,0,0.15);
min-width: 280px; overflow: hidden; min-width: 280px; overflow: hidden;
} }
.toast-notification.error { border-left-color: #ff4757; }
.toast-content { display: flex; align-items: flex-start; gap: 12px; } .toast-content { display: flex; align-items: flex-start; gap: 12px; }
.check-icon { font-size: 1.2rem; } .check-icon { font-size: 1.2rem; }
.toast-text h4 { margin: 0 0 4px 0; font-size: 1rem; color: #2c3e50; } .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; position: absolute; bottom: 0; left: 0; height: 3px; background: #2ecc71;
width: 100%; animation: progress 3s linear forwards; width: 100%; animation: progress 3s linear forwards;
} }
.toast-notification.error .toast-progress { background: #ff4757; }
@keyframes progress { from { width: 100%; } to { width: 0%; } } @keyframes progress { from { width: 100%; } to { width: 0%; } }
/* Toast 进出动画 */ /* Toast 进出动画 */