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:
@@ -2642,8 +2642,168 @@ button.btn-sm {
|
||||
.settings-display-panel,
|
||||
.settings-macro-panel,
|
||||
.settings-supervisor-panel {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.settings-section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--border-soft);
|
||||
}
|
||||
|
||||
.settings-section.is-collapsed .settings-section-head {
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
.settings-section-head .settings-display-title {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.settings-section-head-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.settings-section-fold {
|
||||
flex-shrink: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--border-soft);
|
||||
border-radius: 6px;
|
||||
background: color-mix(in srgb, var(--panel) 90%, var(--accent) 10%);
|
||||
color: var(--accent);
|
||||
cursor: pointer;
|
||||
font-size: 0;
|
||||
line-height: 1;
|
||||
transition: transform 0.15s ease, border-color 0.15s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.settings-section-fold::before {
|
||||
content: "▾";
|
||||
font-size: 0.85rem;
|
||||
line-height: 28px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.settings-section-fold:hover {
|
||||
border-color: color-mix(in srgb, var(--accent) 50%, var(--border-soft));
|
||||
}
|
||||
|
||||
.settings-section.is-collapsed .settings-section-fold::before {
|
||||
content: "▸";
|
||||
}
|
||||
|
||||
.settings-section-save {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.82rem;
|
||||
padding: 6px 14px;
|
||||
}
|
||||
|
||||
.settings-section-body {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.settings-section.is-collapsed .settings-section-body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.settings-page-toolbar {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.settings-card-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px dashed var(--border-soft);
|
||||
}
|
||||
|
||||
.settings-card-fold {
|
||||
flex-shrink: 0;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--border-soft);
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
font-size: 0;
|
||||
line-height: 1;
|
||||
transition: color 0.15s ease, border-color 0.15s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.settings-card-fold::before {
|
||||
content: "▾";
|
||||
font-size: 0.8rem;
|
||||
line-height: 26px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.settings-card-fold:hover {
|
||||
color: var(--accent);
|
||||
border-color: color-mix(in srgb, var(--accent) 40%, var(--border-soft));
|
||||
}
|
||||
|
||||
.settings-card.is-collapsed .settings-card-fold::before {
|
||||
content: "▸";
|
||||
}
|
||||
|
||||
.settings-card-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 0.92rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.settings-card-save {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.78rem;
|
||||
padding: 5px 12px;
|
||||
}
|
||||
|
||||
.settings-card-body {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.settings-card.is-collapsed .settings-card-body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.settings-section-head {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.settings-section-head-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.settings-card-topbar {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-display-title {
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -843,8 +843,13 @@
|
||||
</div>
|
||||
</details>
|
||||
<p id="settings-meta-line" class="settings-meta-line"></p>
|
||||
<div class="settings-display-panel card">
|
||||
<h3 class="settings-display-title">显示与导航</h3>
|
||||
<section class="settings-section card settings-display-panel" data-settings-section="display">
|
||||
<div class="settings-section-head">
|
||||
<button type="button" class="settings-section-fold" aria-expanded="true" aria-label="折叠"></button>
|
||||
<h3 class="settings-display-title">显示与导航</h3>
|
||||
<button type="button" class="primary settings-section-save" data-settings-section="display">保存</button>
|
||||
</div>
|
||||
<div class="settings-section-body">
|
||||
<label class="chk-label settings-display-chk">
|
||||
<input type="checkbox" id="pref-show-account-pnl" checked />
|
||||
监控区显示资金账户、交易账户与浮动盈亏
|
||||
@@ -874,9 +879,15 @@
|
||||
顶栏显示「计算器」
|
||||
</label>
|
||||
<p class="settings-display-hint">保存至 hub_settings.json,换浏览器同样生效。关闭导航后对应页面将不可从顶栏进入,直接访问 URL 会跳回监控区。</p>
|
||||
</div>
|
||||
<div class="settings-macro-panel card">
|
||||
<h3 class="settings-display-title">宏观关键数据(风控前置)</h3>
|
||||
</div>
|
||||
</section>
|
||||
<section class="settings-section card settings-macro-panel" data-settings-section="macro">
|
||||
<div class="settings-section-head">
|
||||
<button type="button" class="settings-section-fold" aria-expanded="true" aria-label="折叠"></button>
|
||||
<h3 class="settings-display-title">宏观关键数据(风控前置)</h3>
|
||||
<button type="button" class="ghost settings-section-save" data-settings-section="macro">保存表单</button>
|
||||
</div>
|
||||
<div class="settings-section-body">
|
||||
<p class="settings-display-hint">
|
||||
手动录入 FOMC / CPI / 就业数据发布时间(北京时间)。监控区在发布前后各 1 小时提示风险:有仓注意仓位,无仓建议等待。仅提醒,不拦截下单。
|
||||
</p>
|
||||
@@ -903,9 +914,15 @@
|
||||
</div>
|
||||
</form>
|
||||
<div id="macro-event-list" class="macro-event-list"></div>
|
||||
</div>
|
||||
<div class="settings-supervisor-panel card">
|
||||
<h3 class="settings-display-title">交易监管 · 企业微信</h3>
|
||||
</div>
|
||||
</section>
|
||||
<section class="settings-section card settings-supervisor-panel" data-settings-section="supervisor">
|
||||
<div class="settings-section-head">
|
||||
<button type="button" class="settings-section-fold" aria-expanded="true" aria-label="折叠"></button>
|
||||
<h3 class="settings-display-title">交易监管 · 企业微信</h3>
|
||||
<button type="button" class="primary settings-section-save" data-settings-section="supervisor">保存</button>
|
||||
</div>
|
||||
<div class="settings-section-body">
|
||||
<p class="settings-display-hint">
|
||||
与四所实例策略通知独立;手动/中控开平仓与新开仓会推送至此 Webhook。链接可在下方单独修改。
|
||||
</p>
|
||||
@@ -947,13 +964,25 @@
|
||||
<input id="supervisor-reopen-min" type="number" min="1" step="1" value="30" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="settings-section card" data-settings-section="exchanges">
|
||||
<div class="settings-section-head">
|
||||
<button type="button" class="settings-section-fold" aria-expanded="true" aria-label="折叠"></button>
|
||||
<h3 class="settings-display-title">交易所账户</h3>
|
||||
<div class="settings-section-head-actions">
|
||||
<button type="button" id="btn-settings-add" class="ghost">添加交易所</button>
|
||||
<button type="button" class="primary settings-section-save" data-settings-section="exchanges">全部保存</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-section-body">
|
||||
<div id="settings-list" class="settings-grid-wrap"></div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="toolbar settings-page-toolbar">
|
||||
<button type="button" id="btn-settings-save" class="primary">保存全部</button>
|
||||
<button type="button" id="btn-settings-reload" class="ghost">重新加载</button>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<button type="button" id="btn-settings-save" class="primary">保存设置</button>
|
||||
<button type="button" id="btn-settings-add">添加交易所</button>
|
||||
<button type="button" id="btn-settings-reload">重新加载</button>
|
||||
</div>
|
||||
<div id="settings-list" class="settings-grid-wrap"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user