(function bootLoginPortal(global) { "use strict"; const THEME_KEY = "xiaobaiTheme"; const api = global.XiaobaiAPI; const card = document.querySelector("#loginCard"); const themeButton = document.querySelector("#loginThemeToggle"); const state = { view: "first", mode: "login", accounts: [], currentUserId: null, loading: false, confirmingId: null, error: "", }; function escapeHtml(value) { return String(value ?? "").replace(/[&<>"']/g, (ch) => ( { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[ch] )); } function preferredTheme() { try { const stored = global.localStorage.getItem(THEME_KEY); if (stored === "dark" || stored === "light") return stored; } catch (_error) { // Fall through to the system preference. } return global.matchMedia && global.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; } function applyTheme(theme, persist) { const normalized = theme === "dark" ? "dark" : "light"; document.documentElement.dataset.theme = normalized; document.documentElement.style.colorScheme = normalized; themeButton.textContent = normalized === "dark" ? "☀ 日间" : "🌙 夜间"; themeButton.setAttribute("aria-label", normalized === "dark" ? "切换到日间模式" : "切换到夜间模式"); if (persist) { try { global.localStorage.setItem(THEME_KEY, normalized); } catch (_error) { // Theme still applies for the current page when storage is unavailable. } } } function setError(message) { state.error = message || ""; } function membershipLabel(account) { if (account.role === "admin") return account.membership?.subscribed ? "管理员 · 会员" : "管理员"; return account.membership?.subscribed ? "会员" : "普通用户"; } function enterApp() { const next = new URLSearchParams(global.location.search).get("next"); global.location.replace(next && next.startsWith("/") ? next : "/"); } function formMarkup(options) { const registering = state.mode === "register"; const submitLabel = options.submitLabel || (state.loading ? "正在登录..." : registering ? "注册并进入" : options.add ? "添加并进入" : "登录"); return [ options.back ? '' : "", `
${escapeHtml(options.lead)}
`, '密码连续输错 5 次将锁定 10 分钟。还没有账号?切换到「注册」创建。
', ].join(""); } function accountRow(account) { const current = Number(account.user_id) === Number(state.currentUserId); const confirming = Number(state.confirmingId) === Number(account.user_id); const classes = `login-account-row${current ? " is-current" : ""}${confirming ? " is-confirming" : ""}`; if (state.view === "manage" && confirming) { return [ `移除「${escapeHtml(account.username)}」的本机记录?
`, '这台电脑已记录 ${count} 个账号,可直接进入,无需再次输入密码。
`, managing ? '' : "", `${escapeHtml(state.error)}
` : "", '账号记录仅保存在这台电脑的浏览器中
', ].join(""); } function render() { card.classList.toggle("is-loading", state.loading); if (state.view === "first" || state.view === "add") { card.innerHTML = formMarkup({ title: state.view === "add" ? "添加账号" : "欢迎回来", lead: "登录后进入你的复盘空间", add: state.view === "add", back: state.view === "add", }); } else { card.innerHTML = pickerMarkup(); } bindCard(); } function bindCard() { card.querySelectorAll("[data-auth-mode]").forEach((button) => { button.addEventListener("click", () => { state.mode = button.dataset.authMode === "register" ? "register" : "login"; setError(""); render(); }); }); card.querySelectorAll("[data-login-action]").forEach((button) => { button.addEventListener("click", () => { const action = button.dataset.loginAction; if (action === "picker") { state.view = state.accounts.length ? "picker" : "first"; state.confirmingId = null; } else if (action === "add") { state.view = "add"; state.mode = "login"; } else if (action === "manage") { state.view = "manage"; } else if (action === "cancel-forget") { state.confirmingId = null; } setError(""); render(); }); }); card.querySelectorAll("[data-switch-id]").forEach((button) => { button.addEventListener("click", () => switchAccount(Number(button.dataset.switchId))); }); card.querySelectorAll("[data-confirm-id]").forEach((button) => { button.addEventListener("click", () => { state.confirmingId = Number(button.dataset.confirmId); render(); }); }); card.querySelectorAll("[data-forget-id]").forEach((button) => { button.addEventListener("click", () => forgetAccount(Number(button.dataset.forgetId))); }); const form = card.querySelector("#loginForm"); if (form) form.addEventListener("submit", submitCredentials); } async function loadAccounts() { const payload = await api.request("/api/auth/accounts"); state.accounts = payload.accounts || []; state.currentUserId = payload.current_user_id ?? null; const params = new URLSearchParams(global.location.search); if (params.get("mode") === "register") state.mode = "register"; if (params.get("notice")) setError(params.get("notice")); if (state.accounts.length) state.view = "picker"; else state.view = "first"; } async function submitCredentials(event) { event.preventDefault(); const username = document.querySelector("#loginUsername").value.trim(); const password = document.querySelector("#loginPassword").value; if (state.mode === "register" && password !== document.querySelector("#loginPasswordConfirm").value) { setError("两次输入的密码不一致。"); render(); return; } state.loading = true; setError(""); render(); try { await api.request(`/api/auth/${state.mode}`, "POST", { username, password }); enterApp(); } catch (error) { state.loading = false; setError(error.message || "账号操作失败"); render(); } } async function switchAccount(userId) { state.loading = true; setError(""); render(); try { await api.request("/api/auth/switch", "POST", { user_id: userId }); enterApp(); } catch (error) { state.loading = false; setError(error.message || "该账号需重新验证"); render(); } } async function forgetAccount(userId) { try { await api.request("/api/auth/forget", "POST", { user_id: userId }); state.accounts = state.accounts.filter((item) => Number(item.user_id) !== Number(userId)); state.confirmingId = null; if (!state.accounts.length) state.view = "first"; setError(""); render(); } catch (error) { setError(error.message || "移除失败"); render(); } } themeButton.addEventListener("click", () => { applyTheme(document.documentElement.dataset.theme === "dark" ? "light" : "dark", true); }); applyTheme(preferredTheme(), false); loadAccounts() .then(render) .catch((error) => { setError(error.message || "无法连接本地服务"); state.view = "first"; render(); }); })(window);