系统设置增加备份恢复:凌晨3点自动备份,保留10天,含 env 与复盘图片。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* 系统设置 · 备份恢复
|
||||
*/
|
||||
(function (global) {
|
||||
function statusEl() {
|
||||
return document.getElementById("backup-status");
|
||||
}
|
||||
|
||||
function setStatus(msg, isErr) {
|
||||
const el = statusEl();
|
||||
if (!el) return;
|
||||
el.textContent = msg || "";
|
||||
el.style.color = isErr ? "#fb7185" : "";
|
||||
}
|
||||
|
||||
function fmtSize(n) {
|
||||
const v = Number(n) || 0;
|
||||
if (v < 1024) return v + " B";
|
||||
if (v < 1024 * 1024) return (v / 1024).toFixed(1) + " KB";
|
||||
return (v / (1024 * 1024)).toFixed(2) + " MB";
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
async function loadList() {
|
||||
const body = document.getElementById("backup-list-body");
|
||||
if (!body) return;
|
||||
body.innerHTML = '<tr><td colspan="4" class="muted">加载中…</td></tr>';
|
||||
try {
|
||||
const r = await fetch("/api/settings/backup/list", { credentials: "same-origin" });
|
||||
const j = await r.json();
|
||||
if (!j.ok) throw new Error(j.msg || "加载失败");
|
||||
const dir = document.getElementById("backup-dir-label");
|
||||
if (dir && j.backup_dir) dir.textContent = j.backup_dir;
|
||||
const items = j.items || [];
|
||||
if (!items.length) {
|
||||
body.innerHTML = '<tr><td colspan="4" class="muted">暂无备份</td></tr>';
|
||||
return;
|
||||
}
|
||||
body.innerHTML = items
|
||||
.map(function (it) {
|
||||
const name = escapeHtml(it.name || "");
|
||||
return (
|
||||
"<tr>" +
|
||||
"<td><code style=\"font-size:.74rem\">" +
|
||||
name +
|
||||
"</code></td>" +
|
||||
"<td>" +
|
||||
escapeHtml(it.mtime_bj || "—") +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
escapeHtml(fmtSize(it.size)) +
|
||||
"</td>" +
|
||||
"<td style=\"white-space:nowrap\">" +
|
||||
'<a class="btn-secondary" style="padding:3px 8px;font-size:.74rem;text-decoration:none" href="/api/settings/backup/download/' +
|
||||
encodeURIComponent(it.name) +
|
||||
'">下载</a> ' +
|
||||
'<button type="button" class="btn-del" style="padding:3px 8px;font-size:.74rem" data-backup-restore="' +
|
||||
encodeURIComponent(it.name) +
|
||||
'">恢复</button>' +
|
||||
"</td>" +
|
||||
"</tr>"
|
||||
);
|
||||
})
|
||||
.join("");
|
||||
} catch (e) {
|
||||
body.innerHTML =
|
||||
'<tr><td colspan="4" class="muted">' + escapeHtml(e.message || "加载失败") + "</td></tr>";
|
||||
}
|
||||
}
|
||||
|
||||
async function createBackup() {
|
||||
setStatus("备份中…");
|
||||
try {
|
||||
const r = await fetch("/api/settings/backup/create", {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: "{}",
|
||||
});
|
||||
const j = await r.json();
|
||||
if (!j.ok) throw new Error(j.msg || "备份失败");
|
||||
setStatus("已生成 " + (j.name || "备份"));
|
||||
await loadList();
|
||||
} catch (e) {
|
||||
setStatus(e.message || "备份失败", true);
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreBackup(name) {
|
||||
if (!name) return;
|
||||
if (!confirm("确定用该备份恢复?\n将覆盖 .env、数据库与复盘图片,建议恢复后重启。")) return;
|
||||
setStatus("恢复中…");
|
||||
try {
|
||||
const r = await fetch("/api/settings/backup/restore", {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: name }),
|
||||
});
|
||||
const j = await r.json();
|
||||
if (!j.ok) throw new Error(j.msg || "恢复失败");
|
||||
setStatus(j.msg || "恢复完成");
|
||||
} catch (e) {
|
||||
setStatus(e.message || "恢复失败", true);
|
||||
}
|
||||
}
|
||||
|
||||
function boot() {
|
||||
const root = document.querySelector("[data-backup-root]");
|
||||
if (!root || root.dataset.bound === "1") return;
|
||||
root.dataset.bound = "1";
|
||||
const createBtn = document.getElementById("backup-create-btn");
|
||||
const refreshBtn = document.getElementById("backup-refresh-btn");
|
||||
if (createBtn) createBtn.addEventListener("click", createBackup);
|
||||
if (refreshBtn) refreshBtn.addEventListener("click", loadList);
|
||||
root.addEventListener("click", function (ev) {
|
||||
const btn = ev.target.closest("[data-backup-restore]");
|
||||
if (!btn) return;
|
||||
const name = decodeURIComponent(btn.getAttribute("data-backup-restore") || "");
|
||||
void restoreBackup(name);
|
||||
});
|
||||
void loadList();
|
||||
}
|
||||
|
||||
global.InstanceBackup = { boot: boot, loadList: loadList };
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
@@ -154,6 +154,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
if (tab === "settings" && global.InstanceBackup && typeof global.InstanceBackup.boot === "function") {
|
||||
global.InstanceBackup.boot();
|
||||
}
|
||||
}
|
||||
if (!revisit) {
|
||||
if (typeof global.refreshAccountSnapshot === "function") {
|
||||
|
||||
Reference in New Issue
Block a user