Add per-card save and collapse on settings page

Each settings section and exchange card gets its own save button and fold toggle with state persisted in localStorage.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-23 19:37:16 +08:00
parent 0dedaa2b4d
commit ea5c6cddb4
3 changed files with 337 additions and 33 deletions
+134 -19
View File
@@ -3648,6 +3648,105 @@
renderSettingsList(data);
};
});
bindSettingsCardFolds(list);
list.querySelectorAll(".settings-card-save").forEach((btn) => {
btn.addEventListener("click", () => {
void saveSettingsSection("exchange", { label: btn.dataset.label || "账户" });
});
});
}
const SETTINGS_FOLD_KEY = "hub_settings_section_fold";
function settingsFoldStorageKey(section, cardKey) {
return cardKey ? `${SETTINGS_FOLD_KEY}_${section}_${cardKey}` : `${SETTINGS_FOLD_KEY}_${section}`;
}
function getSettingsFoldState(section, cardKey) {
try {
return localStorage.getItem(settingsFoldStorageKey(section, cardKey)) === "1";
} catch (_) {
return false;
}
}
function setSettingsFoldState(section, collapsed, cardKey) {
try {
localStorage.setItem(settingsFoldStorageKey(section, cardKey), collapsed ? "1" : "0");
} catch (_) {}
}
function applySettingsSectionFold(el) {
const section = el.dataset.settingsSection;
if (!section) return;
const collapsed = getSettingsFoldState(section);
el.classList.toggle("is-collapsed", collapsed);
const btn = el.querySelector(":scope > .settings-section-head > .settings-section-fold");
if (btn) btn.setAttribute("aria-expanded", collapsed ? "false" : "true");
}
function applySettingsCardFold(card) {
const key = card.dataset.key || card.dataset.idx || "";
const collapsed = getSettingsFoldState("exchange", String(key));
card.classList.toggle("is-collapsed", collapsed);
const btn = card.querySelector(".settings-card-fold");
if (btn) btn.setAttribute("aria-expanded", collapsed ? "false" : "true");
}
function bindSettingsCardFolds(root) {
(root || document).querySelectorAll(".settings-card").forEach((card) => {
if (card.dataset.foldBound === "1") return;
card.dataset.foldBound = "1";
applySettingsCardFold(card);
const foldBtn = card.querySelector(".settings-card-fold");
if (!foldBtn) return;
foldBtn.addEventListener("click", () => {
const key = card.dataset.key || card.dataset.idx || "";
const collapsed = !card.classList.contains("is-collapsed");
card.classList.toggle("is-collapsed", collapsed);
foldBtn.setAttribute("aria-expanded", collapsed ? "false" : "true");
setSettingsFoldState("exchange", collapsed, String(key));
});
});
}
function initSettingsSectionFolds() {
document.querySelectorAll(".settings-section[data-settings-section]").forEach((el) => {
applySettingsSectionFold(el);
if (el.dataset.foldBound === "1") return;
el.dataset.foldBound = "1";
const foldBtn = el.querySelector(":scope > .settings-section-head > .settings-section-fold");
if (foldBtn) {
foldBtn.addEventListener("click", () => {
const section = el.dataset.settingsSection;
const collapsed = !el.classList.contains("is-collapsed");
el.classList.toggle("is-collapsed", collapsed);
foldBtn.setAttribute("aria-expanded", collapsed ? "false" : "true");
setSettingsFoldState(section, collapsed);
});
}
});
document.querySelectorAll(".settings-section-save").forEach((btn) => {
if (btn.dataset.saveBound === "1") return;
btn.dataset.saveBound = "1";
btn.addEventListener("click", () => {
const section = btn.dataset.settingsSection || "";
if (section === "macro") {
const form = document.getElementById("macro-event-form");
if (form) form.requestSubmit();
return;
}
const label =
section === "display"
? "显示与导航"
: section === "supervisor"
? "交易监管"
: section === "exchanges"
? "交易所账户"
: "设置";
void saveSettingsSection(section, { label });
});
});
}
function macroDatetimeLocalToApi(v) {
@@ -3798,6 +3897,7 @@
syncDisplayPrefsUI(data);
syncSupervisorSettingsUI(data);
renderSettingsList(data);
initSettingsSectionFolds();
});
}
@@ -3806,7 +3906,15 @@
const envOff = ex.env_disabled
? '<span class="badge">环境变量强制关</span>'
: "";
return `<div class="settings-card" data-idx="${idx}" data-key="${esc(ex.key || ex.id || "")}">
const cardKey = esc(ex.key || ex.id || String(idx));
const cardTitle = esc(ex.name || ex.key || `账户 ${idx + 1}`);
return `<div class="settings-card" data-idx="${idx}" data-key="${cardKey}">
<div class="settings-card-topbar">
<button type="button" class="settings-card-fold" aria-expanded="true" aria-label="折叠"></button>
<span class="settings-card-title">${cardTitle}</span>
<button type="button" class="primary settings-card-save" data-label="${cardTitle}">保存</button>
</div>
<div class="settings-card-body">
<div class="settings-card-head">
<label class="chk-label"><input type="checkbox" class="ex-enabled" ${ex.enabled ? "checked" : ""} ${ex.env_disabled ? "disabled" : ""}/> 启用</label>
${envOff}
@@ -3825,6 +3933,7 @@
<div class="field"><label>id</label><input class="ex-id" value="${esc(ex.id || "")}" /></div>
<button type="button" class="danger btn-del-ex" data-idx="${idx}">删除账户</button>
</div>
</div>
</div>`;
}
@@ -3888,7 +3997,8 @@
};
}
async function saveSettings() {
async function saveSettingsSection(section, opts) {
const options = opts || {};
const body = collectSettingsFromUI();
try {
const r = await apiFetch("/api/settings", {
@@ -3897,28 +4007,33 @@
body: JSON.stringify(body),
});
const j = await r.json();
if (j.ok) {
showToast("设置已保存(已写入 hub_settings.json");
if (j.settings) {
settingsCache = j.settings;
syncDisplayPrefsUI(j.settings);
syncSupervisorSettingsUI(j.settings);
renderSettingsList(j.settings);
loadSettingsMetaLine();
} else {
await loadSettingsUI();
}
if (lastMonitorRows.length) renderMonitorGrid(lastMonitorRows);
if (!pageNavAllowed(currentPage())) {
history.replaceState({}, "", "/monitor");
setActiveNav();
}
} else showToast("保存失败", true);
if (!j.ok) {
showToast("保存失败", true);
return;
}
const label = options.label || "设置";
showToast(`${label}已保存`);
if (j.settings) {
settingsCache = j.settings;
syncDisplayPrefsUI(j.settings);
syncSupervisorSettingsUI(j.settings);
renderSettingsList(j.settings);
loadSettingsMetaLine();
}
if (lastMonitorRows.length) renderMonitorGrid(lastMonitorRows);
if (!pageNavAllowed(currentPage())) {
history.replaceState({}, "", "/monitor");
setActiveNav();
}
} catch (e) {
showToast(String(e), true);
}
}
async function saveSettings() {
await saveSettingsSection("all", { label: "全部设置" });
}
document.getElementById("btn-logout").onclick = async () => {
try {
await fetch("/api/auth/logout", { method: "POST" });