feat: add settings tabs, auto backup, and show latest 5 backups

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-02 09:15:21 +08:00
parent 52ebbfbeae
commit 458cc42dd5
12 changed files with 621 additions and 190 deletions
+111 -42
View File
@@ -41,10 +41,15 @@
.hidden { display: none; }
pre { background: var(--panel); border: 1px solid #243041; border-radius: 10px; padding: 1rem; overflow: auto; font-size: 0.78rem; color: #b7c5d4; }
.center-page { display: flex; justify-content: center; align-items: flex-start; min-height: 50vh; padding: 1.5rem 0 3rem; }
.settings-card { width: 100%; max-width: 420px; }
.settings-stack { width: 100%; max-width: 420px; display: flex; flex-direction: column; gap: 1.25rem; }
.settings-title { font-size: 1.15rem; font-weight: 650; margin-bottom: 0.75rem; }
.settings-card { width: 100%; max-width: 460px; }
.settings-title { font-size: 1.15rem; font-weight: 650; margin: 0.25rem 0 0.85rem; }
.settings-hint { color: var(--muted); font-size: 0.85rem; line-height: 1.5; margin: 0 0 1rem; }
.settings-tabs { display: flex; border-bottom: 1px solid #243041; margin: -0.25rem -0.15rem 1.1rem; }
.settings-tabs button { appearance: none; border: 0; background: transparent; color: var(--muted); padding: 0.65rem 0.95rem; cursor: pointer; font-size: 0.92rem; border-bottom: 2px solid transparent; margin-bottom: -1px; }
.settings-tabs button.active { color: var(--text); border-bottom-color: var(--accent); font-weight: 600; }
.auto-box { background: #0c1117; border: 1px solid #243041; border-radius: 8px; padding: 0.85rem 0.95rem; margin-bottom: 1rem; }
.check-row { display: flex; align-items: center; gap: 0.55rem; margin-bottom: 0.75rem; color: var(--text); font-size: 0.92rem; }
.check-row input { width: auto; accent-color: var(--accent); }
.field { display: block; margin-bottom: 1rem; }
.field-input { width: 100%; margin-top: 0.4rem; padding: 0.55rem 0.65rem; border-radius: 6px; border: 1px solid #243041; background: #0c1117; color: var(--text); }
.btn-primary { margin-top: 0.5rem; padding: 0.55rem 1.2rem; border-radius: 6px; border: 0; background: var(--accent); color: #fff; cursor: pointer; }
@@ -131,43 +136,60 @@
</section>
<section id="page-settings" class="hidden">
<div class="center-page">
<div class="settings-stack">
<form id="settingsForm" class="tile settings-card">
<div class="settings-title">账号设置</div>
<p id="settingsAuthHint" class="settings-hint" style="margin-bottom:1rem"></p>
<label class="field"><span class="label">用户名</span>
<input id="setUsername" class="field-input" autocomplete="username" />
</label>
<label class="field"><span class="label">当前密码</span>
<input id="setCurPass" type="password" class="field-input" autocomplete="current-password" />
</label>
<label class="field"><span class="label">新密码(留空则不修改)</span>
<input id="setNewPass" type="password" class="field-input" autocomplete="new-password" />
</label>
<label class="field"><span class="label">确认新密码</span>
<input id="setNewPass2" type="password" class="field-input" autocomplete="new-password" />
</label>
<p id="settingsErr" class="form-err"></p>
<p id="settingsOk" class="form-ok"></p>
<button type="submit" class="btn-primary" id="settingsSaveBtn">保存账号</button>
</form>
<div class="tile settings-card">
<div class="settings-title">数据备份与恢复</div>
<p class="settings-hint">备份目录:<span id="backupDir">/root/market_intel_backups</span></p>
<p id="backupDirErr" class="form-err hidden"></p>
<label class="field"><span class="label">当前密码(备份/恢复需验证)</span>
<input id="backupPass" type="password" class="field-input" autocomplete="current-password" />
</label>
<label class="field"><span class="label">选择备份</span>
<select id="backupSelect" class="field-input"><option value="">尚无备份</option></select>
</label>
<p id="backupErr" class="form-err"></p>
<p id="backupOk" class="form-ok"></p>
<div class="btn-row">
<button type="button" class="btn-primary" id="backupBtn">立即备份</button>
<button type="button" class="btn-danger" id="restoreBtn">恢复选中备份</button>
<div class="settings-tabs">
<button type="button" id="tabAccount" class="active" data-tab="account">账户密码</button>
<button type="button" id="tabBackup" data-tab="backup">数据备份与恢复</button>
</div>
<div id="panelAccount">
<form id="settingsForm">
<div class="settings-title">账户密码</div>
<p id="settingsAuthHint" class="settings-hint"></p>
<label class="field"><span class="label">用户名</span>
<input id="setUsername" class="field-input" autocomplete="username" />
</label>
<label class="field"><span class="label">当前密码</span>
<input id="setCurPass" type="password" class="field-input" autocomplete="current-password" />
</label>
<label class="field"><span class="label">新密码(留空则不修改)</span>
<input id="setNewPass" type="password" class="field-input" autocomplete="new-password" />
</label>
<label class="field"><span class="label">确认新密码</span>
<input id="setNewPass2" type="password" class="field-input" autocomplete="new-password" />
</label>
<p id="settingsErr" class="form-err"></p>
<p id="settingsOk" class="form-ok"></p>
<button type="submit" class="btn-primary" id="settingsSaveBtn">保存账号</button>
</form>
</div>
<div id="panelBackup" class="hidden">
<div class="settings-title">数据备份与恢复</div>
<p class="settings-hint">备份目录:<span id="backupDir">/root/market_intel_backups</span><br/>列表仅显示最近 5 条。</p>
<p id="backupDirErr" class="form-err hidden"></p>
<div class="auto-box">
<label class="check-row">
<input type="checkbox" id="autoEnabled" checked />
<span>启用自动备份</span>
</label>
<label class="field"><span class="label">备份间隔(小时)</span>
<input id="autoInterval" type="number" min="1" max="168" value="24" class="field-input" />
</label>
<p class="settings-hint" style="margin-bottom:0" id="autoMeta">最近备份:—</p>
</div>
<label class="field"><span class="label">当前密码(操作需验证)</span>
<input id="backupPass" type="password" class="field-input" autocomplete="current-password" />
</label>
<label class="field"><span class="label">选择备份(最近 5 条)</span>
<select id="backupSelect" class="field-input"><option value="">尚无备份</option></select>
</label>
<p id="backupErr" class="form-err"></p>
<p id="backupOk" class="form-ok"></p>
<div class="btn-row">
<button type="button" class="btn-primary" id="autoSaveBtn">保存自动备份</button>
<button type="button" class="btn-primary" id="backupBtn">立即备份</button>
<button type="button" class="btn-danger" id="restoreBtn">恢复选中备份</button>
</div>
</div>
</div>
</div>
</div>
</section>
@@ -279,6 +301,16 @@
sel.appendChild(opt);
});
}
function showSettingsTab(tab) {
const isAccount = tab === "account";
document.getElementById("panelAccount").classList.toggle("hidden", !isAccount);
document.getElementById("panelBackup").classList.toggle("hidden", isAccount);
document.getElementById("tabAccount").classList.toggle("active", isAccount);
document.getElementById("tabBackup").classList.toggle("active", !isAccount);
}
document.getElementById("tabAccount").addEventListener("click", () => showSettingsTab("account"));
document.getElementById("tabBackup").addEventListener("click", () => showSettingsTab("backup"));
async function loadBackupInfo() {
document.getElementById("backupErr").textContent = "";
document.getElementById("backupOk").textContent = "";
@@ -294,12 +326,25 @@
} else {
dirErr.classList.add("hidden");
}
document.getElementById("autoEnabled").checked = !!info.auto_enabled;
document.getElementById("autoInterval").value = info.interval_hours || 24;
let meta = "最近备份:" + (info.latest_backup_ms ? fmtBackupTime(info.latest_backup_ms) : "—");
if (info.auto_enabled) meta += " · 下次约:" + (info.next_due_ms ? fmtBackupTime(info.next_due_ms) : "即将");
document.getElementById("autoMeta").textContent = meta;
renderBackupSelect(info.backups || []);
const disabled = !state.authRequired || !info.writable;
["backupPass","backupSelect","backupBtn","restoreBtn"].forEach(id => {
document.getElementById(id).disabled = disabled;
});
const noAuth = !state.authRequired;
const noWrite = !info.writable;
document.getElementById("backupPass").disabled = noAuth;
document.getElementById("backupSelect").disabled = noAuth;
document.getElementById("autoEnabled").disabled = noAuth;
document.getElementById("autoInterval").disabled = noAuth || !info.auto_enabled;
document.getElementById("autoSaveBtn").disabled = noAuth;
document.getElementById("backupBtn").disabled = noAuth || noWrite;
document.getElementById("restoreBtn").disabled = noAuth || noWrite;
}
document.getElementById("autoEnabled").addEventListener("change", () => {
document.getElementById("autoInterval").disabled = !document.getElementById("autoEnabled").checked || !state.authRequired;
});
async function loadSettings() {
document.getElementById("settingsErr").textContent = "";
@@ -361,6 +406,30 @@
}
});
document.getElementById("autoSaveBtn").addEventListener("click", async () => {
const pwd = document.getElementById("backupPass").value;
document.getElementById("backupErr").textContent = "";
document.getElementById("backupOk").textContent = "";
if (!pwd) { document.getElementById("backupErr").textContent = "请输入当前密码"; return; }
try {
const r = await apiFetch("/api/settings/backup/auto", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
current_password: pwd,
enabled: document.getElementById("autoEnabled").checked,
interval_hours: Number(document.getElementById("autoInterval").value) || 24,
}),
});
const d = await r.json();
if (!r.ok) throw new Error(d.detail || "save failed");
document.getElementById("backupOk").textContent = d.message || "自动备份设置已保存";
await loadBackupInfo();
} catch (err) {
document.getElementById("backupErr").textContent = String(err.message || err);
}
});
document.getElementById("backupBtn").addEventListener("click", async () => {
const pwd = document.getElementById("backupPass").value;
document.getElementById("backupErr").textContent = "";