(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: "", username: "", password: "", }; 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 formatLastUsed(value) { if (!value) return ""; const parsed = new Date(value); if (Number.isNaN(parsed.getTime())) return ""; const now = new Date(); const hh = String(parsed.getHours()).padStart(2, "0"); const mm = String(parsed.getMinutes()).padStart(2, "0"); if (parsed.toDateString() === now.toDateString()) return `今天 ${hh}:${mm}`; const yesterday = new Date(now); yesterday.setDate(now.getDate() - 1); if (parsed.toDateString() === yesterday.toDateString()) return `昨天 ${hh}:${mm}`; return `${parsed.getMonth() + 1}月${parsed.getDate()}日`; } function chipsFor(account, current) { const chips = []; if (current) chips.push('当前'); if (account.role === "admin") chips.push('管理员'); if (account.membership?.subscribed) chips.push('会员'); else if (account.role !== "admin") chips.push('普通用户'); return chips.join(""); } 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 ? "添加并进入" : "登录"); const lead = options.lead; const hint = options.hint; const invalid = state.error ? " is-invalid" : ""; return [ options.back ? '' : "", `
${escapeHtml(lead)}
`, '${escapeHtml(hint)}
`, ].join(""); } function accountRow(account) { const current = Number(account.user_id) === Number(state.currentUserId); const confirming = Number(state.confirmingId) === Number(account.user_id); const managing = state.view === "manage"; const classes = [ "login-account-row", current ? "is-current" : "", confirming ? "is-confirming" : "", !managing && !current ? "is-switchable" : "", ].filter(Boolean).join(" "); if (managing && confirming) { return [ `移除「${escapeHtml(account.username)}」的本机记录?
`, '${managing ? "移除只删除这台电脑上的登录记录,不会注销账号" : `这台电脑已记录 ${count} 个账号,点选即可进入,无需再次输入密码。`}
`, managing ? '' : "", `${escapeHtml(state.error)}
` : "", managing ? '移除后再次登录该账号需重新输入密码
' : '账号记录仅保存在这台电脑的浏览器中
', ].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: state.view === "add" ? "登录另一个账号,添加后可随时一键切换" : "登录后进入你的复盘空间", hint: state.view === "add" ? "添加后账号会保存在这台电脑,方便随时切换。" : "密码连续输错 5 次将锁定 10 分钟。还没有账号?切换到「注册」创建。", 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", (event) => { event.stopPropagation(); 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; state.username = username; state.password = password; 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);