45c7e73db5
Also toggle close-gate and compound subfields so the settings page only shows relevant controls. Co-authored-by: Cursor <cursoragent@cursor.com>
716 lines
26 KiB
JavaScript
716 lines
26 KiB
JavaScript
/**
|
|
* 实例:导航显示,env 配置,改密,PM2 重启.
|
|
*/
|
|
(function (global) {
|
|
const DISPLAY = () => global.__INSTANCE_DISPLAY__ || {};
|
|
|
|
function setStatus(el, text, isErr) {
|
|
if (!el) return;
|
|
el.textContent = text || "";
|
|
el.classList.toggle("err", !!isErr);
|
|
}
|
|
|
|
async function fetchJson(url, opts) {
|
|
const res = await fetch(url, Object.assign({ credentials: "same-origin" }, opts || {}));
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) {
|
|
throw new Error(data.msg || res.statusText || "请求失败");
|
|
}
|
|
return data;
|
|
}
|
|
|
|
/** 默认关闭的导航开关:缺失时按 false,不能用 !== false */
|
|
const NAV_DEFAULT_OFF = {
|
|
show_nav_dashboard: true,
|
|
show_nav_account_ledger: true,
|
|
show_nav_system_guide: true,
|
|
show_settings_transfer: true,
|
|
};
|
|
|
|
function navPrefShow(display, key) {
|
|
if (!key) return true;
|
|
if (NAV_DEFAULT_OFF[key]) return display[key] === true;
|
|
return display[key] !== false;
|
|
}
|
|
|
|
function applyDisplayToNav(display) {
|
|
const map = {
|
|
dashboard: "show_nav_dashboard",
|
|
account_ledger: "show_nav_account_ledger",
|
|
key_monitor: "show_nav_key_monitor",
|
|
trade: "show_nav_trade",
|
|
strategy: "show_nav_strategy",
|
|
strategy_records: "show_nav_strategy_records",
|
|
records: "show_nav_records",
|
|
stats: "show_nav_stats",
|
|
options: "show_nav_options",
|
|
"options-review": "show_nav_options_review",
|
|
options_review: "show_nav_options_review",
|
|
"hedge-plan": "show_nav_hedge_plan",
|
|
hedge_plan: "show_nav_hedge_plan",
|
|
risk_policy: "show_nav_risk_policy",
|
|
system_guide: "show_nav_system_guide",
|
|
env_config: "show_nav_env_config",
|
|
};
|
|
document
|
|
.querySelectorAll(
|
|
".embed-top-nav [data-embed-tab], .top-nav a[href^='/'], #inst-mobile-tabbar [data-embed-tab], #inst-mobile-more [data-embed-tab]"
|
|
)
|
|
.forEach((a) => {
|
|
const tab = a.getAttribute("data-embed-tab") || (a.getAttribute("href") || "").replace(/^\//, "").split("?")[0];
|
|
if (tab === "more") return;
|
|
const key = map[tab];
|
|
if (!key) return;
|
|
const show = navPrefShow(display, key);
|
|
a.classList.toggle("nav-hidden", !show);
|
|
a.style.display = show ? "" : "none";
|
|
});
|
|
global.__INSTANCE_DISPLAY__ = display;
|
|
}
|
|
|
|
function pageNavAllowed(tab) {
|
|
const d = DISPLAY();
|
|
const map = {
|
|
dashboard: "show_nav_dashboard",
|
|
account_ledger: "show_nav_account_ledger",
|
|
key_monitor: "show_nav_key_monitor",
|
|
trade: "show_nav_trade",
|
|
strategy: "show_nav_strategy",
|
|
strategy_records: "show_nav_strategy_records",
|
|
records: "show_nav_records",
|
|
stats: "show_nav_stats",
|
|
options: "show_nav_options",
|
|
"options-review": "show_nav_options_review",
|
|
options_review: "show_nav_options_review",
|
|
"hedge-plan": "show_nav_hedge_plan",
|
|
hedge_plan: "show_nav_hedge_plan",
|
|
risk_policy: "show_nav_risk_policy",
|
|
system_guide: "show_nav_system_guide",
|
|
env_config: "show_nav_env_config",
|
|
};
|
|
const key = map[tab];
|
|
if (!key) return true;
|
|
return navPrefShow(d, key);
|
|
}
|
|
|
|
function displayPrefsRoot() {
|
|
const settingsPane = document.querySelector('.embed-tab-pane[data-embed-pane="settings"]');
|
|
if (settingsPane) {
|
|
const inSettings = settingsPane.querySelector("#display-prefs-form");
|
|
if (inSettings) return inSettings;
|
|
}
|
|
const pane = document.querySelector(".embed-tab-pane.is-active-pane");
|
|
if (pane) {
|
|
const inPane = pane.querySelector("#display-prefs-form");
|
|
if (inPane) return inPane;
|
|
}
|
|
return document.getElementById("display-prefs-form");
|
|
}
|
|
|
|
function displayPrefsStatusEl() {
|
|
const card = document.getElementById("display-prefs-card");
|
|
if (card) {
|
|
const el = card.querySelector("#display-prefs-status");
|
|
if (el) return el;
|
|
}
|
|
return document.getElementById("display-prefs-status");
|
|
}
|
|
|
|
function envConfigRoot() {
|
|
const activePane = document.querySelector(".embed-tab-pane.is-active-pane");
|
|
if (activePane) {
|
|
return activePane.querySelector(".env-config-page");
|
|
}
|
|
return document.querySelector(".env-config-page");
|
|
}
|
|
|
|
function bindEnvTabs() {
|
|
/* Tab 切换由 CSS radio+label 实现 */
|
|
}
|
|
|
|
async function loadDisplayPrefsForm(force) {
|
|
const root = displayPrefsRoot();
|
|
if (!root) return;
|
|
if (!force && root.getAttribute("data-prefs-ssr") === "1" && root.querySelector("[data-pref-key]")) {
|
|
return;
|
|
}
|
|
return loadDisplayPrefsFormIn(root);
|
|
}
|
|
|
|
async function loadDisplayPrefsFormIn(root) {
|
|
try {
|
|
const data = await fetchJson("/api/settings/display");
|
|
const display = data.display || {};
|
|
const meta = data.meta || [];
|
|
root.innerHTML = "";
|
|
meta.forEach((group) => {
|
|
const section = document.createElement("div");
|
|
section.className = "display-prefs-group";
|
|
const title = document.createElement("h3");
|
|
title.className = "settings-subcard-title";
|
|
title.textContent = group.group;
|
|
section.appendChild(title);
|
|
const grid = document.createElement("div");
|
|
grid.className = "display-prefs-checks";
|
|
(group.entries || []).forEach((item) => {
|
|
const label = document.createElement("label");
|
|
label.className = "chk-label";
|
|
const cb = document.createElement("input");
|
|
cb.type = "checkbox";
|
|
cb.dataset.prefKey = item.key;
|
|
cb.checked = NAV_DEFAULT_OFF[item.key]
|
|
? display[item.key] === true
|
|
: display[item.key] !== false;
|
|
label.appendChild(cb);
|
|
label.appendChild(document.createTextNode(" " + item.label));
|
|
grid.appendChild(label);
|
|
});
|
|
section.appendChild(grid);
|
|
root.appendChild(section);
|
|
});
|
|
root.setAttribute("data-prefs-ssr", "1");
|
|
} catch (e) {
|
|
root.innerHTML = '<span class="err">' + (e.message || "加载失败") + "</span>";
|
|
}
|
|
}
|
|
|
|
async function saveDisplayPrefs() {
|
|
const status = displayPrefsStatusEl();
|
|
const root = displayPrefsRoot();
|
|
if (!root) {
|
|
setStatus(status, "未找到导航设置表单", true);
|
|
return;
|
|
}
|
|
const display = {};
|
|
root.querySelectorAll("input[data-pref-key]").forEach((cb) => {
|
|
display[cb.dataset.prefKey] = !!cb.checked;
|
|
});
|
|
try {
|
|
const data = await fetchJson("/api/settings/display", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ display }),
|
|
});
|
|
applyDisplayToNav(data.display || display);
|
|
setStatus(status, "已保存,导航已更新");
|
|
} catch (e) {
|
|
setStatus(status, e.message || "保存失败", true);
|
|
}
|
|
}
|
|
|
|
let envSchemaGroups = [];
|
|
|
|
function renderEnvFieldRow(field) {
|
|
const row = document.createElement("div");
|
|
row.className = "env-field-row" + (field.restart_required ? " env-field-row--restart" : "");
|
|
row.dataset.envKey = field.key;
|
|
const label = document.createElement("label");
|
|
label.className = "env-field-label";
|
|
label.htmlFor = "env-f-" + field.key;
|
|
label.textContent = field.label || field.key;
|
|
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;
|
|
row.appendChild(note);
|
|
}
|
|
let input;
|
|
if (field.type === "bool") {
|
|
input = document.createElement("select");
|
|
input.id = "env-f-" + field.key;
|
|
[["true", "开启"], ["false", "关闭"]].forEach(([v, text]) => {
|
|
const o = document.createElement("option");
|
|
o.value = v;
|
|
o.textContent = text;
|
|
input.appendChild(o);
|
|
});
|
|
const cur = (field.current || field.default || "false").toLowerCase();
|
|
input.value = cur === "true" || cur === "1" ? "true" : "false";
|
|
} else if (field.type === "select" && Array.isArray(field.options) && field.options.length) {
|
|
input = document.createElement("select");
|
|
input.id = "env-f-" + field.key;
|
|
const cur = String(field.current || field.default || "");
|
|
const seen = new Set();
|
|
field.options.forEach((opt) => {
|
|
const v = String(opt.value != null ? opt.value : "");
|
|
if (seen.has(v)) return;
|
|
seen.add(v);
|
|
const o = document.createElement("option");
|
|
o.value = v;
|
|
o.textContent = opt.label || v;
|
|
input.appendChild(o);
|
|
});
|
|
if (cur && !seen.has(cur)) {
|
|
const o = document.createElement("option");
|
|
o.value = cur;
|
|
o.textContent = cur;
|
|
input.insertBefore(o, input.firstChild);
|
|
}
|
|
input.value = cur || (field.options[0] && field.options[0].value) || "";
|
|
} else {
|
|
input = document.createElement("input");
|
|
input.id = "env-f-" + field.key;
|
|
input.type = "password";
|
|
// 防止浏览器把登录密码自动填进 API Key/Secret(保存对冲开关时曾误写入密钥)
|
|
input.autocomplete = "new-password";
|
|
input.setAttribute("data-lpignore", "true");
|
|
input.setAttribute("data-1p-ignore", "true");
|
|
input.setAttribute("data-form-type", "other");
|
|
input.readOnly = true;
|
|
input.addEventListener("focus", function () {
|
|
input.readOnly = false;
|
|
});
|
|
if (field.sensitive) {
|
|
input.dataset.envSensitive = "1";
|
|
input.dataset.envDirty = "0";
|
|
input.addEventListener("input", function () {
|
|
input.dataset.envDirty = "1";
|
|
});
|
|
if (field.has_value) {
|
|
const cur = document.createElement("div");
|
|
cur.className = "env-sensitive-current muted";
|
|
const labelSpan = document.createElement("span");
|
|
labelSpan.textContent = "已配置 ";
|
|
const masked = document.createElement("span");
|
|
masked.className = "env-masked-value";
|
|
masked.textContent = field.masked || "";
|
|
cur.appendChild(labelSpan);
|
|
cur.appendChild(masked);
|
|
row.appendChild(cur);
|
|
}
|
|
input.placeholder = field.has_value ? "修改时填写新值,留空不修改" : "请输入";
|
|
} else {
|
|
input.type = "text";
|
|
input.autocomplete = "off";
|
|
input.value = field.current || field.default || "";
|
|
}
|
|
}
|
|
input.dataset.envKey = field.key;
|
|
input.className = "env-field-input";
|
|
row.appendChild(input);
|
|
if (field.hidden) {
|
|
row.hidden = true;
|
|
row.style.display = "none";
|
|
}
|
|
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");
|
|
groups.forEach((_group, idx) => {
|
|
const radio = document.createElement("input");
|
|
radio.type = "radio";
|
|
radio.name = "env-section";
|
|
radio.id = "env-sec-" + idx;
|
|
radio.className = "env-tab-radio";
|
|
if (idx === 0) radio.checked = true;
|
|
body.appendChild(radio);
|
|
});
|
|
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";
|
|
let modeSectionIdx = 0;
|
|
groups.forEach((group, idx) => {
|
|
if ((group.title || "").indexOf("期权/对冲模式") >= 0) modeSectionIdx = idx;
|
|
const label = document.createElement("label");
|
|
label.className = "env-tab-btn";
|
|
label.htmlFor = "env-sec-" + idx;
|
|
label.setAttribute("role", "tab");
|
|
label.textContent = group.title || "其他";
|
|
tabBar.appendChild(label);
|
|
const panel = document.createElement("section");
|
|
panel.className = "env-panel env-panel--" + idx;
|
|
panel.setAttribute("role", "tabpanel");
|
|
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);
|
|
body.dataset.envModeSectionIdx = String(modeSectionIdx);
|
|
bindTradeModeAutoRefresh(body);
|
|
bindOptionsSectionVisibility(body);
|
|
return body;
|
|
}
|
|
|
|
function envFieldRowByKey(body, key) {
|
|
if (!body || !key) return null;
|
|
const byRow = body.querySelector('.env-field-row[data-env-key="' + key + '"]');
|
|
if (byRow) return byRow;
|
|
const input = body.querySelector('.env-field-input[data-env-key="' + key + '"]');
|
|
return input ? input.closest(".env-field-row") : null;
|
|
}
|
|
|
|
function envFieldValue(body, key, fallback) {
|
|
const input = body && body.querySelector('.env-field-input[data-env-key="' + key + '"]');
|
|
if (!input) return fallback;
|
|
return String(input.value || fallback || "").trim();
|
|
}
|
|
|
|
function envTruthy(v) {
|
|
return ["1", "true", "yes", "on"].indexOf(String(v || "").toLowerCase()) >= 0;
|
|
}
|
|
|
|
function setEnvRowHidden(row, hide) {
|
|
if (!row) return;
|
|
row.hidden = !!hide;
|
|
row.style.display = hide ? "none" : "";
|
|
}
|
|
|
|
var COIN_ONLY_ENV_KEYS = [
|
|
"OKX_OPTIONS_COIN_COMPOUND",
|
|
"OKX_OPTIONS_COIN_BUDGET_USDT",
|
|
"OKX_OPTIONS_COIN_MAX_USDT_ENABLED",
|
|
"OKX_OPTIONS_COIN_MAX_USDT",
|
|
"OKX_OPTIONS_COIN_SPOT_BUY_BUFFER",
|
|
"OKX_OPTIONS_CLOSE_RECYCLE_MULT_COIN",
|
|
];
|
|
var USDC_ONLY_ENV_KEYS = [
|
|
"OKX_OPTIONS_TRADE_BUDGET_USDC",
|
|
"OKX_OPTIONS_COMPOUND_FULL_ENABLED",
|
|
"OKX_OPTIONS_COMPOUND_FULL_CAP_ENABLED",
|
|
"OKX_OPTIONS_COMPOUND_FULL_CAP_USDC",
|
|
"OKX_OPTIONS_CLOSE_RECYCLE_MULT",
|
|
];
|
|
|
|
function syncOptionsSectionVisibility(body) {
|
|
if (!body) return;
|
|
var margin = (envFieldValue(body, "OKX_OPTIONS_MARGIN_MODE", "coin") || "coin").toLowerCase();
|
|
var isCoin = margin !== "usdc";
|
|
var gateMode = (envFieldValue(body, "OKX_OPTIONS_CLOSE_GATE_MODE", "premium") || "premium").toLowerCase();
|
|
var compoundOn = envTruthy(envFieldValue(body, "OKX_OPTIONS_COMPOUND_FULL_ENABLED", "true"));
|
|
var coinCompoundOn = envTruthy(envFieldValue(body, "OKX_OPTIONS_COIN_COMPOUND", "true"));
|
|
var coinMaxOn = envTruthy(envFieldValue(body, "OKX_OPTIONS_COIN_MAX_USDT_ENABLED", "false"));
|
|
var compoundCapOn = envTruthy(envFieldValue(body, "OKX_OPTIONS_COMPOUND_FULL_CAP_ENABLED", "false"));
|
|
|
|
COIN_ONLY_ENV_KEYS.forEach(function (key) {
|
|
setEnvRowHidden(envFieldRowByKey(body, key), !isCoin);
|
|
});
|
|
USDC_ONLY_ENV_KEYS.forEach(function (key) {
|
|
setEnvRowHidden(envFieldRowByKey(body, key), isCoin);
|
|
});
|
|
|
|
// 门控子项:按模式显隐(仍受本位过滤)
|
|
if (isCoin) {
|
|
setEnvRowHidden(envFieldRowByKey(body, "OKX_OPTIONS_CLOSE_RECYCLE_MULT_COIN"), gateMode !== "premium");
|
|
} else {
|
|
setEnvRowHidden(envFieldRowByKey(body, "OKX_OPTIONS_CLOSE_RECYCLE_MULT"), gateMode !== "premium");
|
|
}
|
|
setEnvRowHidden(envFieldRowByKey(body, "OKX_OPTIONS_CLOSE_NET_PNL_MIN_USDT"), gateMode !== "net_pnl");
|
|
|
|
if (!isCoin) {
|
|
setEnvRowHidden(envFieldRowByKey(body, "OKX_OPTIONS_TRADE_BUDGET_USDC"), compoundOn);
|
|
setEnvRowHidden(envFieldRowByKey(body, "OKX_OPTIONS_COMPOUND_FULL_CAP_ENABLED"), !compoundOn);
|
|
setEnvRowHidden(
|
|
envFieldRowByKey(body, "OKX_OPTIONS_COMPOUND_FULL_CAP_USDC"),
|
|
!(compoundOn && compoundCapOn)
|
|
);
|
|
}
|
|
if (isCoin) {
|
|
setEnvRowHidden(envFieldRowByKey(body, "OKX_OPTIONS_COIN_BUDGET_USDT"), coinCompoundOn);
|
|
setEnvRowHidden(envFieldRowByKey(body, "OKX_OPTIONS_COIN_MAX_USDT"), !coinMaxOn);
|
|
}
|
|
}
|
|
|
|
function bindOptionsSectionVisibility(body) {
|
|
if (!body) return;
|
|
syncOptionsSectionVisibility(body);
|
|
var keys = [
|
|
"OKX_OPTIONS_MARGIN_MODE",
|
|
"OKX_OPTIONS_CLOSE_GATE_MODE",
|
|
"OKX_OPTIONS_COMPOUND_FULL_ENABLED",
|
|
"OKX_OPTIONS_COMPOUND_FULL_CAP_ENABLED",
|
|
"OKX_OPTIONS_COIN_COMPOUND",
|
|
"OKX_OPTIONS_COIN_MAX_USDT_ENABLED",
|
|
];
|
|
keys.forEach(function (key) {
|
|
var el = body.querySelector('.env-field-input[data-env-key="' + key + '"]');
|
|
if (!el || el.dataset.optionsVisBound === "1") return;
|
|
el.dataset.optionsVisBound = "1";
|
|
el.addEventListener("change", function () {
|
|
syncOptionsSectionVisibility(body);
|
|
});
|
|
});
|
|
}
|
|
|
|
function syncCompoundBudgetVisibility(body) {
|
|
syncOptionsSectionVisibility(body);
|
|
}
|
|
|
|
function bindCompoundBudgetVisibility(body) {
|
|
bindOptionsSectionVisibility(body);
|
|
}
|
|
|
|
function bindTradeModeAutoRefresh(body) {
|
|
bindEnvSelectAutoRefresh(
|
|
body,
|
|
"OKX_TRADE_MODE",
|
|
"切换交易模式并刷新配置…",
|
|
"交易模式已切换为当前选项,配置区已刷新",
|
|
"modeRefreshBound",
|
|
true
|
|
);
|
|
bindEnvSelectAutoRefresh(
|
|
body,
|
|
"SIM_DEFAULT_MODE",
|
|
"切换撮合模式并刷新配置…",
|
|
"撮合模式已切换,配置区已刷新(sim 隐藏 API / live 显示实盘项)",
|
|
"simModeRefreshBound",
|
|
false
|
|
);
|
|
}
|
|
|
|
function bindEnvSelectAutoRefresh(body, key, pendingMsg, okMsg, flag, jumpModeSection) {
|
|
const modeSel = body && body.querySelector('.env-field-input[data-env-key="' + key + '"]');
|
|
if (!modeSel || modeSel.dataset[flag] === "1") return;
|
|
modeSel.dataset[flag] = "1";
|
|
modeSel.addEventListener("change", async () => {
|
|
const status = document.getElementById("env-config-status");
|
|
const nextMode = modeSel.value;
|
|
setStatus(status, pendingMsg);
|
|
try {
|
|
const payload = { values: {} };
|
|
payload.values[key] = nextMode;
|
|
await fetchJson("/api/settings/env", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
await loadEnvConfig(true);
|
|
if (jumpModeSection) {
|
|
const page = envConfigRoot() || document.querySelector(".env-config-page");
|
|
const newBody = page && page.querySelector("#env-config-body");
|
|
const idx = newBody && newBody.dataset.envModeSectionIdx;
|
|
if (idx != null) {
|
|
const radio = document.getElementById("env-sec-" + idx);
|
|
if (radio) radio.checked = true;
|
|
}
|
|
}
|
|
setStatus(status, okMsg);
|
|
if (typeof global.refreshAccountSnapshot === "function") {
|
|
global.refreshAccountSnapshot({ force: true, silent: true });
|
|
}
|
|
// 顶栏交易所名:okx_sim / okx_live
|
|
const label = nextMode === "sim" ? "okx_sim" : nextMode === "live" ? "okx_live" : "";
|
|
if (label) {
|
|
document.querySelectorAll('[data-funds-field="exchange-mode-label"]').forEach((el) => {
|
|
el.textContent = label;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
setStatus(status, e.message || "切换失败", true);
|
|
}
|
|
});
|
|
}
|
|
|
|
async function loadEnvConfig(force) {
|
|
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]")) {
|
|
return;
|
|
}
|
|
return loadEnvConfigIn(root);
|
|
}
|
|
|
|
async function loadEnvConfigIn(root) {
|
|
const page = root || envConfigRoot() || document.querySelector(".env-config-page");
|
|
if (!page) return;
|
|
const loading = document.createElement("div");
|
|
loading.className = "env-config-loading-wrap card";
|
|
loading.id = "env-config-body";
|
|
loading.innerHTML = '<div class="env-config-loading muted">加载配置中…</div>';
|
|
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 || [];
|
|
loading.replaceWith(renderEnvConfigBody(envSchemaGroups));
|
|
} catch (e) {
|
|
loading.innerHTML = '<span class="err">' + (e.message || "加载失败") + "</span>";
|
|
}
|
|
}
|
|
|
|
function collectEnvValues() {
|
|
const root = envConfigRoot();
|
|
const values = {};
|
|
const scope = root || document;
|
|
scope.querySelectorAll(".env-field-input[data-env-key]").forEach((el) => {
|
|
if (el.dataset.envSensitive === "1" && el.dataset.envDirty !== "1") {
|
|
// 未改动过的敏感项不提交,避免浏览器自动填充覆盖已有密钥
|
|
return;
|
|
}
|
|
values[el.dataset.envKey] = el.value;
|
|
});
|
|
return values;
|
|
}
|
|
|
|
async function saveEnvConfig(restartAfter) {
|
|
const status = document.getElementById("env-config-status");
|
|
setStatus(status, "保存中…");
|
|
try {
|
|
const data = await fetchJson("/api/settings/env", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ values: collectEnvValues() }),
|
|
});
|
|
const needRestart = restartAfter || data.restart_required;
|
|
if (needRestart) {
|
|
setStatus(status, "已保存,正在重启实例…");
|
|
await restartInstance();
|
|
setStatus(status, "保存并重启完成");
|
|
await loadEnvConfig(true);
|
|
} else {
|
|
setStatus(status, "已保存(即时生效项已应用)");
|
|
await loadEnvConfig(true);
|
|
}
|
|
} catch (e) {
|
|
setStatus(status, e.message || "保存失败", true);
|
|
}
|
|
}
|
|
|
|
async function restartInstance() {
|
|
try {
|
|
await fetchJson("/api/admin/restart", { method: "POST" });
|
|
} catch (_) {
|
|
// 重启会中断当前 HTTP 连接;只要后续 health 恢复即视为成功.
|
|
}
|
|
const deadline = Date.now() + 90000;
|
|
while (Date.now() < deadline) {
|
|
await new Promise((r) => setTimeout(r, 2000));
|
|
try {
|
|
const h = await fetch("/api/admin/health", { credentials: "same-origin" });
|
|
if (h.ok) return;
|
|
} catch (_) {}
|
|
}
|
|
throw new Error("重启后服务未在预期时间内恢复");
|
|
}
|
|
|
|
async function savePassword() {
|
|
const status = document.getElementById("pwd-save-status");
|
|
const body = {
|
|
old_password: (document.getElementById("pwd-old") || {}).value || "",
|
|
new_username: (document.getElementById("pwd-new-username") || {}).value || "",
|
|
new_password: (document.getElementById("pwd-new") || {}).value || "",
|
|
confirm_password: (document.getElementById("pwd-confirm") || {}).value || "",
|
|
};
|
|
try {
|
|
const data = await fetchJson("/api/settings/password", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
});
|
|
if (data.restart_required) {
|
|
setStatus(status, "密码已保存,正在重启…");
|
|
await restartInstance();
|
|
setStatus(status, "密码已更新,请用新密码登录");
|
|
} else {
|
|
setStatus(status, "密码已更新");
|
|
}
|
|
} catch (e) {
|
|
setStatus(status, e.message || "保存失败", true);
|
|
}
|
|
}
|
|
|
|
function installDelegatedHandlers() {
|
|
if (document.documentElement.dataset.prefsDelegateBound === "1") return;
|
|
document.documentElement.dataset.prefsDelegateBound = "1";
|
|
document.addEventListener("click", (ev) => {
|
|
const target = ev.target;
|
|
if (!(target instanceof Element)) return;
|
|
if (target.closest("#display-prefs-save")) {
|
|
ev.preventDefault();
|
|
void saveDisplayPrefs();
|
|
return;
|
|
}
|
|
if (target.closest("#env-config-save")) {
|
|
ev.preventDefault();
|
|
void saveEnvConfig(false);
|
|
return;
|
|
}
|
|
if (target.closest("#env-config-save-restart")) {
|
|
ev.preventDefault();
|
|
void saveEnvConfig(true);
|
|
return;
|
|
}
|
|
if (target.closest("#env-config-reload")) {
|
|
ev.preventDefault();
|
|
void loadEnvConfig(true);
|
|
return;
|
|
}
|
|
if (target.closest("#pwd-save-btn")) {
|
|
ev.preventDefault();
|
|
void savePassword();
|
|
}
|
|
});
|
|
}
|
|
|
|
function bindClickOnce(id, handler) {
|
|
const el = document.getElementById(id);
|
|
if (!el || el.dataset.bound === "1") return;
|
|
el.dataset.bound = "1";
|
|
el.addEventListener("click", handler);
|
|
}
|
|
|
|
function bindEvents() {
|
|
bindClickOnce("display-prefs-save", saveDisplayPrefs);
|
|
bindClickOnce("env-config-save", () => saveEnvConfig(false));
|
|
bindClickOnce("env-config-save-restart", () => saveEnvConfig(true));
|
|
bindClickOnce("env-config-reload", () => loadEnvConfig(true));
|
|
bindClickOnce("pwd-save-btn", savePassword);
|
|
}
|
|
|
|
function initPage() {
|
|
installDelegatedHandlers();
|
|
bindEvents();
|
|
loadDisplayPrefsForm(false);
|
|
loadEnvConfig(false);
|
|
const root = envConfigRoot();
|
|
const body = root && root.querySelector("#env-config-body");
|
|
if (body) {
|
|
bindTradeModeAutoRefresh(body);
|
|
bindCompoundBudgetVisibility(body);
|
|
}
|
|
if (global.__INSTANCE_DISPLAY__) applyDisplayToNav(global.__INSTANCE_DISPLAY__);
|
|
}
|
|
|
|
global.InstanceSettingsPrefs = {
|
|
pageNavAllowed,
|
|
applyDisplayToNav,
|
|
loadDisplayPrefsForm,
|
|
loadEnvConfig,
|
|
bindEnvTabs,
|
|
bindEvents,
|
|
restartInstance,
|
|
};
|
|
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", initPage);
|
|
} else {
|
|
initPage();
|
|
}
|
|
})(typeof window !== "undefined" ? window : globalThis);
|