From 576f6fbcfc1b1a709382eb96587fa44881a832e7 Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 8 Jul 2026 21:10:54 +0800 Subject: [PATCH] Redesign env config UI: tabs, 2-col form, remove per-field badges Co-authored-by: Cursor --- lib/common/static/instance_settings_prefs.js | 152 ++++++++++++----- lib/common/static/instance_theme.css | 167 ++++++++++++++----- lib/env/env_ui_manifest.py | 6 +- lib/instance/templates/embed_shell.html | 4 +- lib/instance/templates/env_config_panel.html | 115 +++++++------ lib/instance/templates/index.html | 4 +- 6 files changed, 312 insertions(+), 136 deletions(-) diff --git a/lib/common/static/instance_settings_prefs.js b/lib/common/static/instance_settings_prefs.js index 358afa1..44bc8c7 100644 --- a/lib/common/static/instance_settings_prefs.js +++ b/lib/common/static/instance_settings_prefs.js @@ -65,13 +65,37 @@ return document.getElementById("display-prefs-form"); } - function envConfigGrid() { + function envConfigRoot() { const pane = document.querySelector(".embed-tab-pane.is-active-pane"); if (pane) { - const inPane = pane.querySelector("#env-config-grid"); + const inPane = pane.querySelector(".env-config-page"); if (inPane) return inPane; } - return document.getElementById("env-config-grid"); + return document.querySelector(".env-config-page"); + } + + function bindEnvTabs(scope) { + const root = scope || envConfigRoot(); + if (!root || root.getAttribute("data-env-tabs-bound") === "1") return; + const tabs = root.querySelectorAll(".env-tab-btn"); + const panels = root.querySelectorAll(".env-panel"); + if (!tabs.length) return; + root.setAttribute("data-env-tabs-bound", "1"); + tabs.forEach((btn) => { + btn.addEventListener("click", () => { + const idx = btn.getAttribute("data-env-tab"); + tabs.forEach((b) => { + const on = b.getAttribute("data-env-tab") === idx; + b.classList.toggle("is-active", on); + b.setAttribute("aria-selected", on ? "true" : "false"); + }); + panels.forEach((p) => { + const on = p.getAttribute("data-env-panel") === idx; + p.classList.toggle("is-active", on); + p.hidden = !on; + }); + }); + }); } async function loadDisplayPrefsForm(force) { @@ -140,26 +164,32 @@ let envSchemaGroups = []; - function renderEnvField(field) { - const card = document.createElement("div"); - card.className = "env-field-card"; + function renderEnvFieldRow(field) { + const row = document.createElement("div"); + row.className = "env-field-row" + (field.restart_required ? " env-field-row--restart" : ""); const label = document.createElement("label"); label.className = "env-field-label"; + label.htmlFor = "env-f-" + field.key; label.textContent = field.label || field.key; - card.appendChild(label); + if (field.restart_required) { + const mark = document.createElement("span"); + mark.className = "env-restart-mark"; + mark.title = "需重启"; + mark.textContent = "*"; + label.appendChild(mark); + } + row.appendChild(label); if (field.note) { const note = document.createElement("div"); note.className = "env-field-note muted"; note.textContent = field.note; - card.appendChild(note); + row.appendChild(note); } let input; if (field.type === "bool") { input = document.createElement("select"); - [ - ["true", "开启"], - ["false", "关闭"], - ].forEach(([v, text]) => { + input.id = "env-f-" + field.key; + [["true", "开启"], ["false", "关闭"]].forEach(([v, text]) => { const o = document.createElement("option"); o.value = v; o.textContent = text; @@ -169,6 +199,7 @@ input.value = cur === "true" || cur === "1" ? "true" : "false"; } else { input = document.createElement("input"); + input.id = "env-f-" + field.key; input.type = field.sensitive ? "password" : "text"; if (field.sensitive && field.has_value) { input.placeholder = field.masked || "留空不修改"; @@ -178,52 +209,88 @@ } input.dataset.envKey = field.key; input.className = "env-field-input"; - card.appendChild(input); - const badge = document.createElement("span"); - badge.className = "env-field-badge " + (field.restart_required ? "env-badge-restart" : "env-badge-hot"); - badge.textContent = field.restart_required ? "需重启" : "保存即生效"; - card.appendChild(badge); - return card; + row.appendChild(input); + return row; + } + + function renderEnvConfigBody(groups) { + const body = document.createElement("div"); + body.className = "env-config-body card"; + body.id = "env-config-body"; + body.setAttribute("data-env-ssr", "1"); + const tabBar = document.createElement("div"); + tabBar.className = "env-config-tabs"; + tabBar.setAttribute("role", "tablist"); + const panelsWrap = document.createElement("div"); + panelsWrap.className = "env-config-panels"; + panelsWrap.id = "env-config-grid"; + groups.forEach((group, idx) => { + const tab = document.createElement("button"); + tab.type = "button"; + tab.className = "env-tab-btn" + (idx === 0 ? " is-active" : ""); + tab.setAttribute("role", "tab"); + tab.dataset.envTab = String(idx); + tab.setAttribute("aria-selected", idx === 0 ? "true" : "false"); + tab.textContent = group.title || "其他"; + tabBar.appendChild(tab); + const panel = document.createElement("section"); + panel.className = "env-panel" + (idx === 0 ? " is-active" : ""); + panel.setAttribute("role", "tabpanel"); + panel.dataset.envPanel = String(idx); + if (idx !== 0) panel.hidden = true; + if (group.has_restart) { + const hint = document.createElement("p"); + hint.className = "env-panel-hint"; + hint.textContent = "本组含需重启项,修改后请点「保存并重启」。"; + panel.appendChild(hint); + } + const grid = document.createElement("div"); + grid.className = "env-form-grid"; + (group.fields || []).forEach((field) => grid.appendChild(renderEnvFieldRow(field))); + panel.appendChild(grid); + panelsWrap.appendChild(panel); + }); + body.appendChild(tabBar); + body.appendChild(panelsWrap); + return body; } async function loadEnvConfig(force) { - const grid = envConfigGrid(); - if (!grid) return; - if (!force && grid.getAttribute("data-env-ssr") === "1" && grid.querySelector("[data-env-key]")) { + const root = envConfigRoot(); + const body = root && root.querySelector("#env-config-body"); + if (!force && body && body.getAttribute("data-env-ssr") === "1" && body.querySelector("[data-env-key]")) { + bindEnvTabs(root); return; } - return loadEnvConfigIn(grid); + return loadEnvConfigIn(root); } - async function loadEnvConfigIn(grid) { - grid.innerHTML = '
加载配置中…
'; + async function loadEnvConfigIn(root) { + const page = root || envConfigRoot() || document.querySelector(".env-config-page"); + if (!page) return; + page.removeAttribute("data-env-tabs-bound"); + const loading = document.createElement("div"); + loading.className = "env-config-loading-wrap card"; + loading.id = "env-config-body"; + loading.innerHTML = '
加载配置中…
'; + const oldBody = page.querySelector("#env-config-body"); + const oldGrid = page.querySelector("#env-config-grid.env-config-loading-wrap"); + if (oldBody) oldBody.replaceWith(loading); + else if (oldGrid) oldGrid.replaceWith(loading); try { const data = await fetchJson("/api/settings/env"); envSchemaGroups = data.groups || []; - grid.innerHTML = ""; - envSchemaGroups.forEach((group) => { - const card = document.createElement("div"); - card.className = "card env-group-card"; - const h = document.createElement("h3"); - h.className = "env-group-title"; - h.textContent = group.title || "其他"; - card.appendChild(h); - const inner = document.createElement("div"); - inner.className = "env-group-fields"; - (group.fields || []).forEach((field) => inner.appendChild(renderEnvField(field))); - card.appendChild(inner); - grid.appendChild(card); - }); - grid.setAttribute("data-env-ssr", "1"); + loading.replaceWith(renderEnvConfigBody(envSchemaGroups)); + bindEnvTabs(page); } catch (e) { - grid.innerHTML = '' + (e.message || "加载失败") + ""; + loading.innerHTML = '' + (e.message || "加载失败") + ""; } } function collectEnvValues() { - const grid = envConfigGrid(); + const root = envConfigRoot(); const values = {}; - const scope = grid || document; + const scope = root || document; scope.querySelectorAll(".env-field-input[data-env-key]").forEach((el) => { values[el.dataset.envKey] = el.value; }); @@ -310,6 +377,7 @@ bindEvents(); loadDisplayPrefsForm(false); loadEnvConfig(false); + bindEnvTabs(); if (global.__INSTANCE_DISPLAY__) applyDisplayToNav(global.__INSTANCE_DISPLAY__); } diff --git a/lib/common/static/instance_theme.css b/lib/common/static/instance_theme.css index 510ba1d..10c6a84 100644 --- a/lib/common/static/instance_theme.css +++ b/lib/common/static/instance_theme.css @@ -2185,85 +2185,172 @@ html[data-theme="light"] .journal-detail-img-thumb { display: none !important; } -/* ── env 配置页(三列卡片) ── */ +/* ── env 配置页(Tab + 双列表单) ── */ .env-config-page { margin-top: 12px; } .env-config-head { - padding: 12px 14px; + padding: 14px 16px; margin-bottom: 12px; } +.env-config-head-row { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + justify-content: space-between; + gap: 12px 16px; +} + +.env-config-head h2 { + margin: 0 0 4px; + font-size: 1rem; +} + +.env-config-head-hint { + margin: 0; + font-size: 0.78rem; + max-width: 42rem; +} + .env-config-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; - margin-top: 10px; + flex-shrink: 0; } -.env-config-grid { +.env-config-body { + padding: 0; + overflow: hidden; +} + +.env-config-tabs { + display: flex; + flex-wrap: nowrap; + gap: 0; + overflow-x: auto; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + padding: 0 8px; + scrollbar-width: thin; +} + +.env-tab-btn { + flex: 0 0 auto; + border: none; + background: transparent; + color: var(--muted, #8892b0); + font-size: 0.8rem; + padding: 10px 14px; + cursor: pointer; + border-bottom: 2px solid transparent; + margin-bottom: -1px; + white-space: nowrap; + transition: color 0.15s, border-color 0.15s; +} + +.env-tab-btn:hover { + color: #c5cae0; +} + +.env-tab-btn.is-active { + color: #e8ecff; + border-bottom-color: var(--accent, #7c6cf0); + font-weight: 600; +} + +.env-config-panels { + padding: 14px 16px 16px; +} + +.env-panel-hint { + margin: 0 0 12px; + padding: 8px 10px; + font-size: 0.75rem; + border-radius: 6px; + background: rgba(251, 191, 36, 0.08); + color: #e8c468; + border: 1px solid rgba(251, 191, 36, 0.15); +} + +.env-form-grid { display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 12px; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px 20px; align-items: start; } -.env-group-card { - padding: 10px 12px; +.env-field-row { + display: flex; + flex-direction: column; + gap: 5px; min-width: 0; } -.env-group-title { - margin: 0 0 8px; - font-size: 0.88rem; - font-weight: 600; -} - -.env-group-fields { - display: flex; - flex-direction: column; - gap: 8px; -} - -.env-field-card { - display: flex; - flex-direction: column; - gap: 4px; +.env-field-row--restart .env-field-label { + color: #d4c4a0; } .env-field-label { - font-size: 0.72rem; + font-size: 0.8rem; font-weight: 600; - color: #a8b0cc; + color: #c5cae0; + line-height: 1.3; +} + +.env-restart-mark { + color: #fbbf24; + font-weight: 700; + margin-left: 2px; } .env-field-note { - font-size: 0.68rem; + font-size: 0.72rem; line-height: 1.35; + margin-top: -2px; } .env-field-input { width: 100%; - font-size: 0.78rem; + font-size: 0.82rem; + padding: 7px 10px; + border-radius: 6px; + box-sizing: border-box; } -.env-field-badge { - font-size: 0.65rem; - align-self: flex-start; - padding: 1px 6px; - border-radius: 4px; +.env-config-loading-wrap { + padding: 24px; + text-align: center; } -.env-badge-hot { - background: rgba(56, 178, 120, 0.15); - color: #6ee7a8; +/* 兼容旧结构 */ +.env-config-grid { + display: block; } +.env-group-card, +.env-field-card, +.env-field-badge, +.env-badge-hot, .env-badge-restart { - background: rgba(251, 191, 36, 0.12); - color: #fbbf24; + display: none; +} + +@media (max-width: 900px) { + .env-form-grid { + grid-template-columns: minmax(0, 1fr); + } +} + +@media (max-width: 720px) { + .env-config-head-row { + flex-direction: column; + } + .env-config-toolbar { + width: 100%; + } } .display-prefs-form { @@ -2314,13 +2401,13 @@ html[data-theme="light"] .journal-detail-img-thumb { } @media (max-width: 1100px) { - .env-config-grid { + .env-form-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } @media (max-width: 720px) { - .env-config-grid { + .env-form-grid { grid-template-columns: minmax(0, 1fr); } .settings-password-form { diff --git a/lib/env/env_ui_manifest.py b/lib/env/env_ui_manifest.py index a30e6eb..fcd4819 100644 --- a/lib/env/env_ui_manifest.py +++ b/lib/env/env_ui_manifest.py @@ -207,7 +207,11 @@ def build_env_ui_payload( _build_field(key, label, note, schema, values) for key, label, note in sec["fields"] ] - groups.append({"title": sec["title"], "fields": fields}) + groups.append({ + "title": sec["title"], + "fields": fields, + "has_restart": any(f.get("restart_required") for f in fields), + }) return groups diff --git a/lib/instance/templates/embed_shell.html b/lib/instance/templates/embed_shell.html index 2122892..cab9322 100644 --- a/lib/instance/templates/embed_shell.html +++ b/lib/instance/templates/embed_shell.html @@ -7,7 +7,7 @@ - + {{ exchange_display }} · 加密货币 | 交易监控复盘系统 @@ -103,7 +103,7 @@ const ORDER_ENTRY_MODEL_CODE_TO_CATEGORY = {{ entry_model_code_to_category | toj - + diff --git a/lib/instance/templates/env_config_panel.html b/lib/instance/templates/env_config_panel.html index 96db571..a78721a 100644 --- a/lib/instance/templates/env_config_panel.html +++ b/lib/instance/templates/env_config_panel.html @@ -1,55 +1,72 @@ -{# env配置:白名单分组,仅中文标签 #} +{# env配置:Tab 分类 + 双列表单,顶部统一保存 #}
-

env 配置

-

仅展示运营相关配置。标注「保存即生效」的项会立即应用;标注「需重启」的项保存后请点「保存并重启」。完整说明见项目文档 docs/env配置说明.md

-
- - - - -
-
-
- {% if env_config_groups %} - {% for group in env_config_groups %} -
-

{{ group.title or '其他' }}

-
- {% for field in group.fields %} -
- - {% if field.note %} -
{{ field.note }}
- {% endif %} - {% if field.type == 'bool' %} - - {% else %} - - {% endif %} - - {% if field.restart_required %}需重启{% else %}保存即生效{% endif %} - -
- {% endfor %} +
+
+

env 配置

+

按分类修改,改完点保存。含「需重启」的项请用「保存并重启」。

+
+
+ + +
- {% endfor %} - {% else %} -
加载配置中…
- {% endif %} +
+ + {% if env_config_groups %} +
+
+ {% for group in env_config_groups %} + + {% endfor %} +
+
+ {% for group in env_config_groups %} +
+ {% if group.has_restart %} +

本组含需重启项,修改后请点「保存并重启」。

+ {% endif %} +
+ {% for field in group.fields %} +
+ + {% if field.note %} +
{{ field.note }}
+ {% endif %} + {% if field.type == 'bool' %} + + {% else %} + + {% endif %} +
+ {% endfor %} +
+
+ {% endfor %} +
+
+ {% else %} +
+
加载配置中…
+
+ {% endif %}
diff --git a/lib/instance/templates/index.html b/lib/instance/templates/index.html index caf8d56..eea2992 100644 --- a/lib/instance/templates/index.html +++ b/lib/instance/templates/index.html @@ -17,7 +17,7 @@ {{ exchange_display }} · 加密货币 | 交易监控复盘系统 - + window.__INSTANCE_DISPLAY__ = {{ display | tojson }}; - + \ No newline at end of file