Initial commit
This commit is contained in:
470
scripts/renderer.js
Normal file
470
scripts/renderer.js
Normal file
@@ -0,0 +1,470 @@
|
||||
// ============ 全局变量 ============
|
||||
let currentDbName = null;
|
||||
let pendingImport = null;
|
||||
let allBackups = [];
|
||||
|
||||
// ============ 初始化 ============
|
||||
async function init() {
|
||||
try {
|
||||
await window.electronAPI.initDatabase();
|
||||
loadDatabases();
|
||||
setupDropZone();
|
||||
setupEventListeners();
|
||||
} catch (err) {
|
||||
console.error("初始化失败:", err);
|
||||
showToast("初始化失败: " + err.message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 工具函数 ============
|
||||
const formatFileSize = (b) => window.electronAPI.formatFileSize(b);
|
||||
const formatDateDisplay = (d) => window.electronAPI.formatDateDisplay(d);
|
||||
|
||||
// ============ 数据库管理 ============
|
||||
function loadDatabases() {
|
||||
const databases = window.electronAPI.listDatabases();
|
||||
const listEl = document.getElementById("databaseList");
|
||||
|
||||
if (databases.length === 0) {
|
||||
listEl.innerHTML = "<div style=\"color: var(--text-muted); text-align: center; padding: 20px; font-size: 11px;\">暂无数据库</div>";
|
||||
return;
|
||||
}
|
||||
|
||||
listEl.innerHTML = databases.map(db => `
|
||||
<div class="database-item ${db.name === currentDbName ? "active" : ""}" data-name="${db.name}">
|
||||
<div class="db-details">
|
||||
<div class="db-name">${db.name}</div>
|
||||
<div class="db-info">${db.count} 备份 · ${formatFileSize(db.totalSize)}</div>
|
||||
</div>
|
||||
<div class="db-actions">
|
||||
<button class="db-action-btn" title="重命名" onclick="event.stopPropagation(); showRenameDb('${db.name}')"><i class="fa-solid fa-pen"></i></button>
|
||||
<button class="db-action-btn delete" title="删除" onclick="event.stopPropagation(); confirmDeleteDb('${db.name}')"><i class="fa-solid fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
`).join("");
|
||||
|
||||
listEl.querySelectorAll(".database-item").forEach(item => {
|
||||
item.addEventListener("click", () => selectDatabase(item.dataset.name));
|
||||
});
|
||||
}
|
||||
|
||||
function selectDatabase(dbName) {
|
||||
try {
|
||||
window.electronAPI.openDatabase(dbName);
|
||||
currentDbName = dbName;
|
||||
|
||||
document.getElementById("noDatabase").style.display = "none";
|
||||
document.getElementById("databaseContent").style.display = "flex";
|
||||
document.getElementById("searchInput").value = "";
|
||||
|
||||
document.querySelectorAll(".database-item").forEach(item => {
|
||||
item.classList.toggle("active", item.dataset.name === dbName);
|
||||
});
|
||||
|
||||
loadBackups();
|
||||
} catch (err) {
|
||||
showToast(err.message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
function loadBackups() {
|
||||
allBackups = window.electronAPI.getBackups(); // 已按 date_modified 降序排序
|
||||
renderBackups(allBackups);
|
||||
}
|
||||
|
||||
function renderBackups(backups) {
|
||||
const listEl = document.getElementById("backupList");
|
||||
document.getElementById("backupCount").textContent = `${backups.length} 个备份`;
|
||||
|
||||
if (backups.length === 0) {
|
||||
listEl.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const icons = { "zip": "fa-file-zipper", "rar": "fa-file-zipper", "7z": "fa-file-zipper", "tar": "fa-file-archive", "gz": "fa-file-archive" };
|
||||
|
||||
listEl.innerHTML = backups.map(b => `
|
||||
<div class="backup-item" data-id="${b.id}" data-search="${b.filename.toLowerCase()} ${(b.tag || "").toLowerCase()}">
|
||||
<div class="backup-icon"><i class="fa-solid ${icons[b.archive_type] || "fa-file"}"></i></div>
|
||||
<div class="backup-details">
|
||||
<div class="backup-name">${b.filename}</div>
|
||||
<div class="backup-meta">
|
||||
<span><i class="fa-regular fa-calendar"></i> ${formatDateDisplay(b.date_modified)}</span>
|
||||
<span><i class="fa-regular fa-file"></i> ${formatFileSize(b.file_size)}</span>
|
||||
<span class="backup-tag ${b.tag ? "" : "empty"}" onclick="showEditTag(${b.id}, '${escapeHtml(b.tag || "")}')">
|
||||
<i class="fa-solid fa-tag"></i> ${b.tag || "标签"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="backup-actions">
|
||||
<button class="backup-action-btn export" title="导出" onclick="exportBackup(${b.id})"><i class="fa-solid fa-download"></i></button>
|
||||
<button class="backup-action-btn delete" title="删除" onclick="confirmDeleteBackup(${b.id})"><i class="fa-solid fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
`).join("");
|
||||
}
|
||||
|
||||
function filterBackups() {
|
||||
const query = document.getElementById("searchInput").value.toLowerCase().trim();
|
||||
|
||||
if (!query) {
|
||||
renderBackups(allBackups);
|
||||
return;
|
||||
}
|
||||
|
||||
const filtered = allBackups.filter(b =>
|
||||
b.filename.toLowerCase().includes(query) ||
|
||||
(b.tag && b.tag.toLowerCase().includes(query)) ||
|
||||
b.date_modified.includes(query)
|
||||
);
|
||||
|
||||
renderBackups(filtered);
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return str ? str.replace(/'/g, "\\'").replace(/"/g, "\\\"") : "";
|
||||
}
|
||||
|
||||
// ============ 拖拽处理 ============
|
||||
function setupDropZone() {
|
||||
const dropZone = document.getElementById("dropZone");
|
||||
|
||||
["dragenter", "dragover", "dragleave", "drop"].forEach(e => {
|
||||
dropZone.addEventListener(e, ev => { ev.preventDefault(); ev.stopPropagation(); });
|
||||
document.body.addEventListener(e, ev => { ev.preventDefault(); ev.stopPropagation(); });
|
||||
});
|
||||
|
||||
["dragenter", "dragover"].forEach(e => dropZone.addEventListener(e, () => dropZone.classList.add("drag-over")));
|
||||
["dragleave", "drop"].forEach(e => dropZone.addEventListener(e, () => dropZone.classList.remove("drag-over")));
|
||||
|
||||
dropZone.addEventListener("drop", handleDrop);
|
||||
}
|
||||
|
||||
async function handleDrop(e) {
|
||||
if (!currentDbName) { showToast("请先选择数据库", "warning"); return; }
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length === 0) return;
|
||||
await processImport(files[0].path);
|
||||
}
|
||||
|
||||
async function processImport(filePath) {
|
||||
showLoading("处理中...");
|
||||
|
||||
try {
|
||||
const stats = window.electronAPI.getFileStats(filePath);
|
||||
if (!stats) throw new Error("无法读取文件");
|
||||
|
||||
const originalName = window.electronAPI.pathBasename(filePath);
|
||||
let dateModified, archiveType, sourcePath = filePath, tempFile = null;
|
||||
|
||||
if (stats.isDirectory) {
|
||||
showLoading("压缩文件夹...");
|
||||
const latestTime = window.electronAPI.getLatestModifiedTime(filePath);
|
||||
dateModified = window.electronAPI.formatDateModified(latestTime);
|
||||
archiveType = "zip";
|
||||
|
||||
const dataPath = await window.electronAPI.getDataPath();
|
||||
const tempDir = window.electronAPI.pathJoin(dataPath, "temp");
|
||||
window.electronAPI.ensureDir(tempDir);
|
||||
tempFile = window.electronAPI.pathJoin(tempDir, `temp_${Date.now()}.zip`);
|
||||
await window.electronAPI.compressFolder(filePath, tempFile);
|
||||
sourcePath = tempFile;
|
||||
} else {
|
||||
if (!window.electronAPI.isSupportedArchive(filePath)) {
|
||||
throw new Error("不支持的格式");
|
||||
}
|
||||
dateModified = window.electronAPI.formatDateModified(new Date(stats.mtime));
|
||||
archiveType = window.electronAPI.getArchiveType(filePath);
|
||||
}
|
||||
|
||||
const targetFilename = window.electronAPI.generateBackupFilename(currentDbName, dateModified, archiveType);
|
||||
|
||||
if (!window.electronAPI.isNameMatching(originalName, currentDbName)) {
|
||||
hideLoading();
|
||||
pendingImport = { sourcePath, originalName, dateModified, archiveType, targetFilename, tempFile };
|
||||
showNameMismatchModal(originalName, currentDbName, targetFilename);
|
||||
return;
|
||||
}
|
||||
|
||||
await performImport(sourcePath, originalName, dateModified, archiveType, targetFilename, tempFile);
|
||||
} catch (err) {
|
||||
hideLoading();
|
||||
showToast(err.message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function performImport(sourcePath, originalName, dateModified, archiveType, targetFilename, tempFile) {
|
||||
showLoading("导入中...");
|
||||
|
||||
try {
|
||||
const storagePath = window.electronAPI.getCurrentStoragePath();
|
||||
const targetPath = window.electronAPI.pathJoin(storagePath, targetFilename);
|
||||
|
||||
const existing = window.electronAPI.findByDateModified(dateModified);
|
||||
if (existing) {
|
||||
window.electronAPI.deleteFile(window.electronAPI.pathJoin(storagePath, existing.filename));
|
||||
}
|
||||
|
||||
await window.electronAPI.copyArchive(sourcePath, targetPath);
|
||||
const targetStats = window.electronAPI.getFileStats(targetPath);
|
||||
|
||||
window.electronAPI.upsertBackup({
|
||||
filename: targetFilename,
|
||||
original_name: originalName,
|
||||
date_modified: dateModified,
|
||||
archive_type: archiveType,
|
||||
file_size: targetStats.size
|
||||
});
|
||||
|
||||
if (tempFile) window.electronAPI.deleteFile(tempFile);
|
||||
|
||||
hideLoading();
|
||||
showToast("导入成功", "success");
|
||||
loadBackups();
|
||||
loadDatabases();
|
||||
} catch {
|
||||
hideLoading();
|
||||
showToast("导入失败", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 模态框 ============
|
||||
function showModal(id) { document.getElementById(id).classList.add("show"); }
|
||||
function closeModal(id) {
|
||||
document.getElementById(id).classList.remove("show");
|
||||
if (id === "nameMismatchModal" && pendingImport?.tempFile) {
|
||||
window.electronAPI.deleteFile(pendingImport.tempFile);
|
||||
pendingImport = null;
|
||||
}
|
||||
}
|
||||
|
||||
function showNewDbModal() {
|
||||
document.getElementById("dbModalTitle").textContent = "新建数据库";
|
||||
document.getElementById("dbName").value = "";
|
||||
document.getElementById("btnConfirmDb").onclick = confirmCreateDb;
|
||||
showModal("dbModal");
|
||||
document.getElementById("dbName").focus();
|
||||
}
|
||||
|
||||
function confirmCreateDb() {
|
||||
const name = document.getElementById("dbName").value.trim();
|
||||
if (!name) { showToast("请输入名称", "warning"); return; }
|
||||
if (!/^[\w\u4e00-\u9fa5]+$/.test(name)) { showToast("名称包含非法字符", "warning"); return; }
|
||||
|
||||
try {
|
||||
window.electronAPI.createDatabase(name);
|
||||
closeModal("dbModal");
|
||||
showToast("创建成功", "success");
|
||||
loadDatabases();
|
||||
selectDatabase(name);
|
||||
} catch (err) {
|
||||
showToast(err.message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
function showRenameDb(dbName) {
|
||||
document.getElementById("dbModalTitle").textContent = "重命名";
|
||||
document.getElementById("dbName").value = dbName;
|
||||
document.getElementById("btnConfirmDb").onclick = () => confirmRenameDb(dbName);
|
||||
showModal("dbModal");
|
||||
document.getElementById("dbName").focus();
|
||||
document.getElementById("dbName").select();
|
||||
}
|
||||
|
||||
function confirmRenameDb(oldName) {
|
||||
const newName = document.getElementById("dbName").value.trim();
|
||||
if (!newName) { showToast("请输入名称", "warning"); return; }
|
||||
if (newName === oldName) { closeModal("dbModal"); return; }
|
||||
if (!/^[\w\u4e00-\u9fa5]+$/.test(newName)) { showToast("名称包含非法字符", "warning"); return; }
|
||||
|
||||
try {
|
||||
showLoading("重命名中...");
|
||||
window.electronAPI.renameDatabase(oldName, newName);
|
||||
if (currentDbName === oldName) {
|
||||
currentDbName = newName;
|
||||
window.electronAPI.openDatabase(newName);
|
||||
}
|
||||
hideLoading();
|
||||
closeModal("dbModal");
|
||||
showToast("重命名成功", "success");
|
||||
loadDatabases();
|
||||
loadBackups();
|
||||
} catch (err) {
|
||||
hideLoading();
|
||||
showToast(err.message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDeleteDb(dbName) {
|
||||
showConfirm("删除数据库", `确定删除 "${dbName}"?所有备份将被删除!`, () => {
|
||||
try {
|
||||
window.electronAPI.deleteDatabase(dbName);
|
||||
if (currentDbName === dbName) {
|
||||
currentDbName = null;
|
||||
document.getElementById("noDatabase").style.display = "flex";
|
||||
document.getElementById("databaseContent").style.display = "none";
|
||||
}
|
||||
showToast("已删除", "success");
|
||||
loadDatabases();
|
||||
} catch (err) {
|
||||
showToast(err.message, "error");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showConfirm(title, message, onConfirm) {
|
||||
document.getElementById("confirmTitle").textContent = title;
|
||||
document.getElementById("confirmMessage").textContent = message;
|
||||
document.getElementById("btnConfirmAction").onclick = () => { closeModal("confirmModal"); onConfirm(); };
|
||||
showModal("confirmModal");
|
||||
}
|
||||
|
||||
function showNameMismatchModal(sourceName, dbName, newFileName) {
|
||||
document.getElementById("sourceFileName").textContent = sourceName;
|
||||
document.getElementById("targetDbName").textContent = dbName;
|
||||
document.getElementById("newFileName").textContent = newFileName;
|
||||
showModal("nameMismatchModal");
|
||||
}
|
||||
|
||||
async function confirmMismatchImport() {
|
||||
closeModal("nameMismatchModal");
|
||||
if (pendingImport) {
|
||||
const { sourcePath, originalName, dateModified, archiveType, targetFilename, tempFile } = pendingImport;
|
||||
pendingImport = null;
|
||||
await performImport(sourcePath, originalName, dateModified, archiveType, targetFilename, tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 标签编辑 ============
|
||||
let editingTagId = null;
|
||||
|
||||
function showEditTag(id, currentTag) {
|
||||
editingTagId = id;
|
||||
document.getElementById("tagInput").value = currentTag || "";
|
||||
showModal("tagModal");
|
||||
document.getElementById("tagInput").focus();
|
||||
}
|
||||
|
||||
function saveTag() {
|
||||
const tag = document.getElementById("tagInput").value.trim();
|
||||
try {
|
||||
window.electronAPI.updateTag(editingTagId, tag);
|
||||
closeModal("tagModal");
|
||||
showToast("已保存", "success");
|
||||
loadBackups();
|
||||
} catch (err) {
|
||||
showToast(err.message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
function clearTag() {
|
||||
try {
|
||||
window.electronAPI.updateTag(editingTagId, null);
|
||||
closeModal("tagModal");
|
||||
showToast("已清除", "success");
|
||||
loadBackups();
|
||||
} catch (err) {
|
||||
showToast(err.message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 导出和删除 ============
|
||||
async function exportBackup(id) {
|
||||
const backup = allBackups.find(b => b.id === id);
|
||||
if (!backup) { showToast("不存在", "error"); return; }
|
||||
|
||||
const exportDir = await window.electronAPI.selectExportDir();
|
||||
if (!exportDir) return;
|
||||
|
||||
const storagePath = window.electronAPI.getCurrentStoragePath();
|
||||
const sourcePath = window.electronAPI.pathJoin(storagePath, backup.filename);
|
||||
const targetPath = window.electronAPI.pathJoin(exportDir, backup.filename);
|
||||
|
||||
try {
|
||||
showLoading("导出中...");
|
||||
await window.electronAPI.copyArchive(sourcePath, targetPath);
|
||||
hideLoading();
|
||||
showToast("导出成功", "success");
|
||||
window.electronAPI.showInFolder(targetPath);
|
||||
} catch {
|
||||
hideLoading();
|
||||
showToast("导出失败", "error");
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDeleteBackup(id) {
|
||||
showConfirm("删除备份", "确定删除?不可恢复!", () => {
|
||||
try {
|
||||
window.electronAPI.deleteBackup(id);
|
||||
showToast("已删除", "success");
|
||||
loadBackups();
|
||||
loadDatabases();
|
||||
} catch (err) {
|
||||
showToast(err.message, "error");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ============ 工具函数 ============
|
||||
function showToast(message, type = "success") {
|
||||
const toast = document.getElementById("toast");
|
||||
const icons = { success: "fa-check", error: "fa-xmark", warning: "fa-exclamation" };
|
||||
|
||||
toast.className = "toast " + type;
|
||||
document.getElementById("toastIcon").className = "toast-icon fa-solid " + (icons[type] || "fa-check");
|
||||
document.getElementById("toastMessage").textContent = message;
|
||||
toast.classList.add("show");
|
||||
|
||||
setTimeout(() => toast.classList.remove("show"), 2500);
|
||||
}
|
||||
|
||||
function showLoading(text = "处理中...") {
|
||||
document.getElementById("loadingText").textContent = text;
|
||||
document.getElementById("loadingOverlay").classList.add("show");
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
document.getElementById("loadingOverlay").classList.remove("show");
|
||||
}
|
||||
|
||||
// ============ 事件监听 ============
|
||||
function setupEventListeners() {
|
||||
document.getElementById("btnConfirmImport").addEventListener("click", confirmMismatchImport);
|
||||
document.getElementById("btnConfirmTag").addEventListener("click", saveTag);
|
||||
document.getElementById("btnClearTag").addEventListener("click", clearTag);
|
||||
|
||||
document.querySelectorAll(".modal-overlay").forEach(overlay => {
|
||||
overlay.addEventListener("click", e => {
|
||||
const modal = e.target.closest(".modal");
|
||||
if (modal) closeModal(modal.id);
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", e => {
|
||||
if (e.key === "Escape") {
|
||||
const openModal = document.querySelector(".modal.show");
|
||||
if (openModal) closeModal(openModal.id);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("dbName").addEventListener("keydown", e => {
|
||||
if (e.key === "Enter") document.getElementById("btnConfirmDb").click();
|
||||
});
|
||||
|
||||
document.getElementById("tagInput").addEventListener("keydown", e => {
|
||||
if (e.key === "Enter") saveTag();
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
|
||||
// 暴露全局函数
|
||||
window.showNewDbModal = showNewDbModal;
|
||||
window.showRenameDb = showRenameDb;
|
||||
window.confirmDeleteDb = confirmDeleteDb;
|
||||
window.showEditTag = showEditTag;
|
||||
window.exportBackup = exportBackup;
|
||||
window.confirmDeleteBackup = confirmDeleteBackup;
|
||||
window.closeModal = closeModal;
|
||||
window.filterBackups = filterBackups;
|
||||
Reference in New Issue
Block a user