feat: add system settings page for admin username and password
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Vendored
+116
-9
@@ -39,6 +39,15 @@
|
||||
.chart-svg { width: 100%; height: auto; display: block; }
|
||||
.hidden { display: none; }
|
||||
pre { background: var(--panel); border: 1px solid #243041; border-radius: 10px; padding: 1rem; overflow: auto; font-size: 0.78rem; color: #b7c5d4; }
|
||||
.center-page { display: flex; justify-content: center; align-items: flex-start; min-height: 50vh; padding: 1.5rem 0 3rem; }
|
||||
.settings-card { width: 100%; max-width: 420px; }
|
||||
.settings-title { font-size: 1.15rem; font-weight: 650; margin-bottom: 0.75rem; }
|
||||
.field { display: block; margin-bottom: 1rem; }
|
||||
.field-input { width: 100%; margin-top: 0.4rem; padding: 0.55rem 0.65rem; border-radius: 6px; border: 1px solid #243041; background: #0c1117; color: var(--text); }
|
||||
.btn-primary { margin-top: 0.5rem; padding: 0.55rem 1.2rem; border-radius: 6px; border: 0; background: var(--accent); color: #fff; cursor: pointer; }
|
||||
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.form-err { color: var(--bad); font-size: 0.85rem; }
|
||||
.form-ok { color: var(--ok); font-size: 0.85rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -49,15 +58,23 @@
|
||||
<div class="nav">
|
||||
<a id="nav-dash" class="active" data-page="dash">总览</a>
|
||||
<a id="nav-ops" data-page="ops">作战地图</a>
|
||||
<a id="nav-settings" data-page="settings">系统设置</a>
|
||||
<a id="nav-logout" class="hidden">退出</a>
|
||||
</div>
|
||||
<main>
|
||||
<section id="page-login" class="hidden">
|
||||
<div class="tile" style="max-width:360px">
|
||||
<div class="label">管理员密码</div>
|
||||
<input id="loginPass" type="password" style="width:100%;margin-top:0.5rem;padding:0.55rem;border-radius:6px;border:1px solid #243041;background:#0c1117;color:#e8eef5" />
|
||||
<p id="loginErr" style="color:var(--bad);font-size:0.85rem"></p>
|
||||
<button id="loginBtn" type="button" style="margin-top:0.5rem;padding:0.5rem 1rem;border:0;border-radius:6px;background:var(--accent);color:#fff;cursor:pointer">登录</button>
|
||||
<div class="center-page">
|
||||
<div class="tile settings-card">
|
||||
<div class="settings-title">登录</div>
|
||||
<label class="field"><span class="label">用户名</span>
|
||||
<input id="loginUser" class="field-input" value="admin" autocomplete="username" />
|
||||
</label>
|
||||
<label class="field"><span class="label">密码</span>
|
||||
<input id="loginPass" type="password" class="field-input" autocomplete="current-password" />
|
||||
</label>
|
||||
<p id="loginErr" class="form-err"></p>
|
||||
<button id="loginBtn" type="button" class="btn-primary">登录</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="page-dash">
|
||||
@@ -105,10 +122,33 @@
|
||||
</div>
|
||||
<p id="moveHint" style="color:var(--muted);font-size:0.85rem;margin-top:0.75rem"></p>
|
||||
</section>
|
||||
<section id="page-settings" class="hidden">
|
||||
<div class="center-page">
|
||||
<form id="settingsForm" class="tile settings-card">
|
||||
<div class="settings-title">系统设置</div>
|
||||
<p id="settingsAuthHint" class="hint" style="margin-bottom:1rem"></p>
|
||||
<label class="field"><span class="label">用户名</span>
|
||||
<input id="setUsername" class="field-input" autocomplete="username" />
|
||||
</label>
|
||||
<label class="field"><span class="label">当前密码</span>
|
||||
<input id="setCurPass" type="password" class="field-input" autocomplete="current-password" />
|
||||
</label>
|
||||
<label class="field"><span class="label">新密码(留空则不修改)</span>
|
||||
<input id="setNewPass" type="password" class="field-input" autocomplete="new-password" />
|
||||
</label>
|
||||
<label class="field"><span class="label">确认新密码</span>
|
||||
<input id="setNewPass2" type="password" class="field-input" autocomplete="new-password" />
|
||||
</label>
|
||||
<p id="settingsErr" class="form-err"></p>
|
||||
<p id="settingsOk" class="form-ok"></p>
|
||||
<button type="submit" class="btn-primary" id="settingsSaveBtn">保存</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<script>
|
||||
const TOKEN_KEY = "mi_token";
|
||||
const state = { page: "dash", range: "day", side: "both", moveMode: "abs", lastMov: null, authRequired: false };
|
||||
const state = { page: "dash", range: "day", side: "both", moveMode: "abs", lastMov: null, authRequired: false, loadedUsername: "admin" };
|
||||
function getToken() { try { return localStorage.getItem(TOKEN_KEY); } catch { return null; } }
|
||||
function setToken(t) { try { if (t) localStorage.setItem(TOKEN_KEY, t); else localStorage.removeItem(TOKEN_KEY); } catch {} }
|
||||
function authHeaders() {
|
||||
@@ -135,6 +175,7 @@
|
||||
document.getElementById("page-login").classList.remove("hidden");
|
||||
document.getElementById("page-dash").classList.add("hidden");
|
||||
document.getElementById("page-ops").classList.add("hidden");
|
||||
document.getElementById("page-settings").classList.add("hidden");
|
||||
document.getElementById("nav-logout").classList.add("hidden");
|
||||
}
|
||||
function hideLogin() {
|
||||
@@ -151,13 +192,19 @@
|
||||
hideLogin();
|
||||
document.getElementById("page-dash").classList.toggle("hidden", p !== "dash");
|
||||
document.getElementById("page-ops").classList.toggle("hidden", p !== "ops");
|
||||
document.getElementById("page-settings").classList.toggle("hidden", p !== "settings");
|
||||
document.getElementById("nav-dash").classList.toggle("active", p === "dash");
|
||||
document.getElementById("nav-ops").classList.toggle("active", p === "ops");
|
||||
location.hash = p === "ops" ? "ops-map" : "";
|
||||
document.getElementById("nav-settings").classList.toggle("active", p === "settings");
|
||||
if (p === "ops") location.hash = "ops-map";
|
||||
else if (p === "settings") location.hash = "settings";
|
||||
else location.hash = "";
|
||||
if (p === "ops") loadOps();
|
||||
if (p === "dash") refreshDash();
|
||||
if (p === "settings") loadSettings();
|
||||
}
|
||||
if (location.pathname === "/ops-map" || location.hash === "#ops-map") state.page = "ops";
|
||||
else if (location.pathname === "/settings" || location.hash === "#settings") state.page = "settings";
|
||||
document.querySelectorAll(".nav a[data-page]").forEach(a => a.addEventListener("click", () => showPage(a.dataset.page)));
|
||||
document.getElementById("nav-logout").addEventListener("click", async () => {
|
||||
setToken(null);
|
||||
@@ -165,15 +212,16 @@
|
||||
showLogin();
|
||||
});
|
||||
document.getElementById("loginBtn").addEventListener("click", async () => {
|
||||
const username = document.getElementById("loginUser").value.trim();
|
||||
const password = document.getElementById("loginPass").value;
|
||||
document.getElementById("loginErr").textContent = "";
|
||||
try {
|
||||
const r = await fetch("/api/auth/login", {
|
||||
method: "POST", credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ password }),
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
if (!r.ok) throw new Error("密码错误");
|
||||
if (!r.ok) throw new Error("用户名或密码错误");
|
||||
const body = await r.json();
|
||||
if (body.token) setToken(body.token);
|
||||
showPage(state.page || "dash");
|
||||
@@ -182,6 +230,65 @@
|
||||
}
|
||||
});
|
||||
|
||||
async function loadSettings() {
|
||||
document.getElementById("settingsErr").textContent = "";
|
||||
document.getElementById("settingsOk").textContent = "";
|
||||
try {
|
||||
const d = await apiFetch("/api/settings/account").then(r => {
|
||||
if (!r.ok) throw new Error("settings " + r.status);
|
||||
return r.json();
|
||||
});
|
||||
document.getElementById("setUsername").value = d.username || "admin";
|
||||
state.loadedUsername = d.username || "admin";
|
||||
const hint = document.getElementById("settingsAuthHint");
|
||||
const disabled = !d.auth_required;
|
||||
hint.textContent = disabled ? "当前鉴权已关闭(AUTH_SECRET=disabled),请在 .env 中修改。" : "";
|
||||
["setUsername","setCurPass","setNewPass","setNewPass2","settingsSaveBtn"].forEach(id => {
|
||||
document.getElementById(id).disabled = disabled;
|
||||
});
|
||||
} catch (e) {
|
||||
document.getElementById("settingsErr").textContent = String(e);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("settingsForm").addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
document.getElementById("settingsErr").textContent = "";
|
||||
document.getElementById("settingsOk").textContent = "";
|
||||
const cur = document.getElementById("setCurPass").value;
|
||||
const nu = document.getElementById("setUsername").value.trim();
|
||||
const np = document.getElementById("setNewPass").value;
|
||||
const np2 = document.getElementById("setNewPass2").value;
|
||||
if (np && np !== np2) {
|
||||
document.getElementById("settingsErr").textContent = "两次输入的新密码不一致";
|
||||
return;
|
||||
}
|
||||
const body = { current_password: cur };
|
||||
if (nu !== state.loadedUsername) body.new_username = nu;
|
||||
if (np) body.new_password = np;
|
||||
try {
|
||||
const r = await apiFetch("/api/settings/account", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const d = await r.json();
|
||||
if (!r.ok) throw new Error(d.detail || "save failed");
|
||||
if (d.relogin_required) {
|
||||
setToken(null);
|
||||
document.getElementById("settingsOk").textContent = d.message || "已保存,请重新登录";
|
||||
setTimeout(showLogin, 800);
|
||||
return;
|
||||
}
|
||||
document.getElementById("settingsOk").textContent = d.message || "已保存";
|
||||
document.getElementById("setCurPass").value = "";
|
||||
document.getElementById("setNewPass").value = "";
|
||||
document.getElementById("setNewPass2").value = "";
|
||||
} catch (err) {
|
||||
document.getElementById("settingsErr").textContent = String(err.message || err);
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll("#rangeSeg button").forEach(b => b.addEventListener("click", () => {
|
||||
state.range = b.dataset.range;
|
||||
document.querySelectorAll("#rangeSeg button").forEach(x => x.classList.toggle("active", x === b));
|
||||
|
||||
Reference in New Issue
Block a user