/* ===================================================================== * 全局会员登录 / 注册 弹窗组件(UserAuth) * --------------------------------------------------------------------- * 自包含:自动注入弹层 DOM 与样式,不依赖任何页面原生 HTML / 外部 CSS。 * 任意页面只需引入一段脚本即可调用,无需再写重复的弹层结构: * * * * 可选配置(须在引入本脚本【之前】声明): * * * 调用方式: * 全局函数:openLogin() / openRegister() * 命名空间:UserAuth.open(mode) / .close() / .refresh() / .render(user) / .on(evt, cb) * 事件: 'login' / 'logout'(UserAuth.on('login', fn)) * ===================================================================== */ (function () { 'use strict'; /* ---------- 配置 ---------- */ var cfg = (window.__USERAUTH_CONFIG__ && typeof window.__USERAUTH_CONFIG__ === 'object') ? window.__USERAUTH_CONFIG__ : {}; cfg.apiBase = cfg.apiBase || '/haonav/account'; cfg.autoReload = cfg.autoReload !== false; // 默认刷新(适配依赖登录态的页面,如导航收藏) cfg.floating = !!cfg.floating; cfg.mount = cfg.mount || 'body'; var STYLE_ID = 'userauth-style'; var MASK_ID = 'authMask'; /* ---------- 工具 ---------- */ function $(s, c) { return (c || document).querySelector(s); } function $all(s, c) { return Array.prototype.slice.call((c || document).querySelectorAll(s)); } function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; }); } function defaultAvatar(name) { var ch = (name || '?').trim().charAt(0).toUpperCase() || '?'; return 'data:image/svg+xml;utf8,' + encodeURIComponent( '' + ch + '' ); } /* ---------- 组件自带样式(单一来源,全站通用,不依赖外部 CSS 变量) ---------- */ var STYLE = "\ :root{\ --ua-primary:#667eea;--ua-primary2:#764ba2;--ua-card:rgba(255,255,255,.97);\ --ua-card-text:#333;--ua-muted:#999;--ua-border:#f0f0f0;--ua-tag-bg:#f0f0f0;}\ [data-theme=\"dark\"]{\ --ua-primary:#7c8df0;--ua-primary2:#8e6fd0;--ua-card:rgba(40,42,58,.96);\ --ua-card-text:#e8e8ef;--ua-muted:#9a9ab0;--ua-border:#3a3c52;--ua-tag-bg:#33354a;}\ .qr-mask{position:fixed;inset:0;background:rgba(0,0,0,.5);display:none;align-items:center;justify-content:center;z-index:999;}\ .qr-mask.show{display:flex;}\ .auth-box{background:var(--ua-card);border-radius:14px;padding:24px 26px 26px;width:340px;max-width:92%;position:relative;box-shadow:0 12px 40px rgba(0,0,0,.25);color:var(--ua-card-text);}\ .auth-box .close{position:absolute;top:12px;right:14px;font-size:13px;color:var(--ua-muted);cursor:pointer;}\ .auth-tabs{display:flex;gap:22px;margin-bottom:18px;border-bottom:1px solid var(--ua-border);}\ .auth-tabs a{padding:8px 2px;font-size:16px;color:var(--ua-muted);cursor:pointer;position:relative;}\ .auth-tabs a.active{color:var(--ua-primary);font-weight:700;}\ .auth-tabs a.active::after{content:'';position:absolute;left:0;right:0;bottom:-1px;height:3px;background:var(--ua-primary);border-radius:3px;}\ .auth-form input{width:100%;padding:11px 14px;margin-bottom:12px;border:2px solid var(--ua-border);border-radius:8px;outline:none;background:transparent;color:var(--ua-card-text);font-size:14px;box-sizing:border-box;}\ .auth-form input:focus{border-color:var(--ua-primary);}\ .auth-msg{font-size:13px;min-height:18px;margin-bottom:8px;}\ .auth-msg.ok{color:#2ecc71;}\ .auth-msg.err{color:#e74c3c;}\ .auth-submit{width:100%;padding:11px;border:none;border-radius:8px;background:var(--ua-primary);color:#fff;font-size:15px;cursor:pointer;transition:filter .2s;}\ .auth-submit:hover{filter:brightness(1.08);}\ .user-area{display:flex;gap:8px;align-items:center;}\ .user-area-float{position:fixed;top:14px;right:14px;z-index:1001;background:var(--ua-card);padding:6px 10px;border-radius:22px;box-shadow:0 4px 16px rgba(0,0,0,.18);}\ .nav-btn{border:2px solid var(--ua-border);background:transparent;color:var(--ua-card-text);padding:7px 16px;border-radius:20px;cursor:pointer;font-size:14px;font-weight:500;transition:all .25s;}\ .nav-btn:hover{border-color:var(--ua-primary);color:var(--ua-primary);}\ .nav-btn-primary{background:var(--ua-primary);border-color:var(--ua-primary);color:#fff;}\ .nav-btn-primary:hover{filter:brightness(1.08);color:#fff;}\ .user-menu{position:relative;display:flex;align-items:center;gap:8px;cursor:pointer;padding:2px 6px;border-radius:20px;transition:background .2s;}\ .user-menu:hover{background:var(--ua-tag-bg);}\ .user-avatar{width:34px;height:34px;border-radius:50%;object-fit:cover;background:var(--ua-tag-bg);}\ .user-name{font-size:14px;color:var(--ua-card-text);max-width:90px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}\ .user-dropdown{position:absolute;top:46px;right:0;min-width:150px;background:var(--ua-card);border-radius:10px;box-shadow:0 8px 26px rgba(0,0,0,.18);padding:6px;display:none;z-index:1000;color:var(--ua-card-text);}\ .user-dropdown.show{display:block;}\ .user-dropdown .ud-name{font-size:13px;color:var(--ua-muted);padding:8px 12px;border-bottom:1px solid var(--ua-border);margin-bottom:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}\ .user-dropdown a{display:block;padding:9px 12px;color:var(--ua-card-text);text-decoration:none;border-radius:6px;font-size:14px;}\ .user-dropdown a:hover{background:var(--ua-primary);color:#fff;}\ "; function injectStyle() { if (document.getElementById(STYLE_ID)) { return; } var s = document.createElement('style'); s.id = STYLE_ID; s.textContent = STYLE; document.head.appendChild(s); } /* ---------- 弹层 DOM(自动注入,无需页面书写) ---------- */ function ensureMask() { var mask = document.getElementById(MASK_ID); if (mask) { return mask; } mask = document.createElement('div'); mask.className = 'qr-mask'; mask.id = MASK_ID; mask.innerHTML = '
' + ' 关闭' + '
' + ' 登录' + ' 注册' + '
' + '
' + ' ' + ' ' + '
' + ' ' + '
' + ' ' + '
'; document.body.appendChild(mask); return mask; } /* ---------- 会员区容器 ---------- */ function getUserArea() { var area = document.getElementById('userArea'); if (area) { return area; } if (!cfg.floating) { return null; } area = document.getElementById('globalUserArea'); if (area) { return area; } area = document.createElement('div'); area.id = 'globalUserArea'; area.className = 'user-area user-area-float'; document.body.appendChild(area); return area; } /* ---------- 事件系统 ---------- */ var listeners = {}; function on(evt, cb) { (listeners[evt] = listeners[evt] || []).push(cb); } function dispatch(evt, data) { (listeners[evt] || []).forEach(function (cb) { try { cb(data); } catch (e) {} }); } /* ---------- 渲染会员区 ---------- */ function renderUser(user) { var area = getUserArea(); if (!area) { return; } if (user && user.uid) { var av = (user.avatar && user.avatar.indexOf('http') === 0) ? user.avatar : (user.avatar || defaultAvatar(user.nickname || user.account)); var name = user.nickname || user.account || '会员'; var hasMyNav = !!document.getElementById('myNavList'); area.innerHTML = '
' + ' ' + ' ' + esc(name) + '' + '
' + '
' + esc(name) + '
' + (hasMyNav ? ' ⭐ 我的导航' : '') + ' 🚪 退出登录' + '
' + '
'; bindUserMenu(); } else { area.innerHTML = '' + ''; bindOpenButtons(); } } function bindUserMenu() { var menu = $('#userMenu'); var dropdown = $('#userDropdown'); if (menu) { menu.addEventListener('click', function (e) { e.stopPropagation(); if (dropdown) { dropdown.classList.toggle('show'); } }); } document.addEventListener('click', function () { if (dropdown) { dropdown.classList.remove('show'); } }); var sf = $('#scrollFav'); if (sf) { sf.addEventListener('click', function () { var el = $('#myNavList'); if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'center' }); } if (dropdown) { dropdown.classList.remove('show'); } }); } var lo = $('#btnLogout'); if (lo) { lo.addEventListener('click', function () { fetch(cfg.apiBase + '/logout', { method: 'POST', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } }) .then(function (r) { return r.json(); }) .then(function () { afterLogout(); }) .catch(function () { afterLogout(); }); }); } } /* ---------- 弹层控制 ---------- */ var modalBound = false; function openAuth(tab, opts) { ensureMask(); bindModalOnce(); if (opts && typeof opts.onSuccess === 'function') { pendingSuccess = opts.onSuccess; } switchTab(tab || 'login'); $('#' + MASK_ID).classList.add('show'); } function closeAuth() { var mask = $('#' + MASK_ID); if (mask) { mask.classList.remove('show'); } pendingSuccess = null; } function switchTab(tab) { $all('.auth-tabs a').forEach(function (a) { a.classList.toggle('active', a.getAttribute('data-tab') === tab); }); var lf = $('#loginForm'), rf = $('#registerForm'); if (lf) { lf.style.display = (tab === 'login') ? '' : 'none'; } if (rf) { rf.style.display = (tab === 'register') ? '' : 'none'; } var lm = $('#loginMsg'); if (lm) { lm.textContent = ''; } var rm = $('#registerMsg'); if (rm) { rm.textContent = ''; } } function bindOpenButtons() { var bl = $('#btnLogin'), br = $('#btnRegister'); if (bl) { bl.addEventListener('click', function () { openAuth('login'); }); } if (br) { br.addEventListener('click', function () { openAuth('register'); }); } } var pendingSuccess = null; function bindModalOnce() { if (modalBound) { return; } modalBound = true; var mask = $('#' + MASK_ID); var close = $('#authClose'); if (close) { close.addEventListener('click', closeAuth); } if (mask) { mask.addEventListener('click', function (e) { if (e.target === mask) { closeAuth(); } }); } $all('.auth-tabs a').forEach(function (a) { a.addEventListener('click', function () { switchTab(a.getAttribute('data-tab')); }); }); var lf = $('#loginForm'); if (lf) { lf.addEventListener('submit', function (e) { e.preventDefault(); var fd = new FormData(lf); var msg = $('#loginMsg'); msg.className = 'auth-msg'; msg.textContent = '登录中…'; fetch(cfg.apiBase + '/login', { method: 'POST', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/json' }, body: JSON.stringify({ account: fd.get('account'), password: fd.get('password') }) }).then(function (r) { return r.json(); }).then(function (res) { if (res.code === 0) { msg.className = 'auth-msg ok'; msg.textContent = '登录成功'; afterLogin(res.data); } else { msg.className = 'auth-msg err'; msg.textContent = res.message || '登录失败'; } }).catch(function () { msg.className = 'auth-msg err'; msg.textContent = '网络错误'; }); }); } var rf = $('#registerForm'); if (rf) { rf.addEventListener('submit', function (e) { e.preventDefault(); var fd = new FormData(rf); var msg = $('#registerMsg'); msg.className = 'auth-msg'; msg.textContent = '注册中…'; fetch(cfg.apiBase + '/register', { method: 'POST', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/json' }, body: JSON.stringify({ account: fd.get('account'), password: fd.get('password'), nickname: fd.get('nickname') }) }).then(function (r) { return r.json(); }).then(function (res) { if (res.code === 0) { msg.className = 'auth-msg ok'; msg.textContent = '注册成功,已自动登录'; afterLogin(res.data); } else { msg.className = 'auth-msg err'; msg.textContent = res.message || '注册失败'; } }).catch(function () { msg.className = 'auth-msg err'; msg.textContent = '网络错误'; }); }); } } /* ---------- 登录 / 退出 后续处理 ---------- */ function afterLogin(user) { var area = getUserArea(); if (area) { renderUser(user); } dispatch('login', user); var cb = pendingSuccess; pendingSuccess = null; setTimeout(closeAuth, 400); if (typeof cb === 'function') { cb(user); return; } if (cfg.autoReload) { setTimeout(function () { location.reload(); }, 450); } else if (typeof cfg.afterLogin === 'function') { cfg.afterLogin(user); } } function afterLogout() { renderUser(null); dispatch('logout'); if (cfg.autoReload) { location.reload(); } else if (typeof cfg.afterLogout === 'function') { cfg.afterLogout(); } } function refresh() { fetch(cfg.apiBase + '/profile', { credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } }) .then(function (r) { return r.json(); }) .then(function (res) { if (res.code === 0 && res.data && res.data.uid) { renderUser(res.data); } else { renderUser(null); } }).catch(function () { renderUser(null); }); } /* ---------- 初始化 ---------- */ function init() { injectStyle(); ensureMask(); bindModalOnce(); renderUser(null); // 默认显示登录/注册,避免闪烁 refresh(); // 探测登录态 } /* ---------- 全局导出 ---------- */ var UserAuth = { version: '2.0.0', open: function (mode, opts) { openAuth(mode || 'login', opts); }, close: closeAuth, refresh: refresh, render: renderUser, on: on, config: cfg }; window.UserAuth = UserAuth; window.openLogin = function (opts) { UserAuth.open('login', opts); }; window.openRegister = function (opts) { UserAuth.open('register', opts); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();