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:
@@ -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" });
|
||||
|
||||
Reference in New Issue
Block a user