Files
YwxAppThink/public/static/haonav/js/auth.js
T

361 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
* 全局会员登录 / 注册 弹窗组件(UserAuth
* ---------------------------------------------------------------------
* 自包含:自动注入弹层 DOM 与样式,不依赖任何页面原生 HTML / 外部 CSS。
* 任意页面只需引入一段脚本即可调用,无需再写重复的弹层结构:
*
* <script src="/static/haonav/js/auth.js"></script>
*
* 可选配置(须在引入本脚本【之前】声明):
* <script>
* window.__USERAUTH_CONFIG__ = {
* apiBase: '/haonav/account', // 接口前缀(默认 haonav 会员接口)
* autoReload: true, // 登录/注册/退出后是否刷新当前页(默认 true)
* floating: false, // 页面没有 #userArea 时是否自动创建浮动入口(默认 false)
* afterLogin: function(user){}, // 登录成功回调(autoReload=false 时生效)
* afterLogout: function(){} // 退出成功回调
* };
* </script>
*
* 调用方式:
* 全局函数: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 { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
});
}
function defaultAvatar(name) {
var ch = (name || '?').trim().charAt(0).toUpperCase() || '?';
return 'data:image/svg+xml;utf8,' + encodeURIComponent(
'<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><rect width="64" height="64" rx="32" fill="#667eea"/><text x="50%" y="54%" font-size="30" fill="#fff" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif">' + ch + '</text></svg>'
);
}
/* ---------- 组件自带样式(单一来源,全站通用,不依赖外部 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 =
'<div class="auth-box">' +
' <span class="close" id="authClose">关闭</span>' +
' <div class="auth-tabs">' +
' <a data-tab="login" class="active">登录</a>' +
' <a data-tab="register">注册</a>' +
' </div>' +
' <form class="auth-form" id="loginForm">' +
' <input type="text" name="account" placeholder="账号 / 手机号 / 邮箱" autocomplete="username" required>' +
' <input type="password" name="password" placeholder="密码" autocomplete="current-password" required>' +
' <div class="auth-msg" id="loginMsg"></div>' +
' <button type="submit" class="auth-submit">登 录</button>' +
' </form>' +
' <form class="auth-form" id="registerForm" style="display:none;">' +
' <input type="text" name="account" placeholder="设置账号(字母/数字/中文)" autocomplete="username" required>' +
' <input type="text" name="nickname" placeholder="昵称(可选)" autocomplete="nickname">' +
' <input type="password" name="password" placeholder="设置密码(至少 6 位)" autocomplete="new-password" required>' +
' <div class="auth-msg" id="registerMsg"></div>' +
' <button type="submit" class="auth-submit">注 册</button>' +
' </form>' +
'</div>';
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 =
'<div class="user-menu" id="userMenu">' +
' <img class="user-avatar" src="' + esc(av) + '" alt="">' +
' <span class="user-name">' + esc(name) + '</span>' +
' <div class="user-dropdown" id="userDropdown">' +
' <div class="ud-name">' + esc(name) + '</div>' +
(hasMyNav ? ' <a href="javascript:;" id="scrollFav">⭐ 我的导航</a>' : '') +
' <a href="javascript:;" id="btnLogout">🚪 退出登录</a>' +
' </div>' +
'</div>';
bindUserMenu();
} else {
area.innerHTML =
'<button class="nav-btn" id="btnLogin">登录</button>' +
'<button class="nav-btn nav-btn-primary" id="btnRegister">注册</button>';
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();
}
})();