chore: 重写初始提交(清空历史,整理后全量提交)
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
/* =====================================================================
|
||||
* 全局会员登录 / 注册 弹窗组件(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 { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,703 @@
|
||||
/* ========== 网址导航 前端交互 ========== */
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var ENGINE_URLS = {
|
||||
baidu: 'https://www.baidu.com/s?wd=',
|
||||
bing: 'https://www.bing.com/search?q=',
|
||||
google: 'https://www.google.com/search?q=',
|
||||
sogou: 'https://www.sogou.com/web?query=',
|
||||
site: '/haonav/search.html?q='
|
||||
};
|
||||
|
||||
var FAV_KEY = 'haonav_fav';
|
||||
var THEME_KEY = 'haonav_theme';
|
||||
var BG_KEY = 'haonav_bg';
|
||||
|
||||
function $(s, c) { return (c || document).querySelector(s); }
|
||||
function $all(s, c) { return Array.prototype.slice.call((c || document).querySelectorAll(s)); }
|
||||
|
||||
/* ---------- 主题(light / dark / auto 跟随系统) ---------- */
|
||||
function applyTheme(t) {
|
||||
var dark;
|
||||
if (t === 'dark') { dark = true; }
|
||||
else if (t === 'auto') {
|
||||
dark = !!(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
} else { dark = false; }
|
||||
if (dark) { document.documentElement.setAttribute('data-theme', 'dark'); }
|
||||
else { document.documentElement.removeAttribute('data-theme'); }
|
||||
}
|
||||
function updateThemeBtn(btn, t) {
|
||||
if (!btn) { return; }
|
||||
var map = {
|
||||
light: { icon: '🌙', title: '当前:浅色(点击切换深色)' },
|
||||
dark: { icon: '☀️', title: '当前:深色(点击切换跟随系统)' },
|
||||
auto: { icon: '🌗', title: '当前:跟随系统(点击切换浅色)' }
|
||||
};
|
||||
var s = map[t] || map.light;
|
||||
btn.textContent = s.icon;
|
||||
btn.title = s.title;
|
||||
}
|
||||
function initTheme() {
|
||||
var t = localStorage.getItem(THEME_KEY) || 'light';
|
||||
applyTheme(t);
|
||||
var btn = $('#themeToggle');
|
||||
updateThemeBtn(btn, t);
|
||||
// 跟随系统变化时实时刷新(仅 auto 模式生效)
|
||||
if (window.matchMedia) {
|
||||
var mq = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
var onChange = function () {
|
||||
if ((localStorage.getItem(THEME_KEY) || 'light') === 'auto') { applyTheme('auto'); }
|
||||
};
|
||||
if (mq.addEventListener) { mq.addEventListener('change', onChange); }
|
||||
else if (mq.addListener) { mq.addListener(onChange); }
|
||||
}
|
||||
if (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var cur = localStorage.getItem(THEME_KEY) || 'light';
|
||||
var next = cur === 'light' ? 'dark' : (cur === 'dark' ? 'auto' : 'light');
|
||||
localStorage.setItem(THEME_KEY, next);
|
||||
applyTheme(next);
|
||||
updateThemeBtn(btn, next);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 背景 ---------- */
|
||||
function applyBg(v) {
|
||||
if (v) { document.body.style.background = v; document.body.style.backgroundAttachment = 'fixed'; }
|
||||
}
|
||||
function initBg() {
|
||||
var v = localStorage.getItem(BG_KEY);
|
||||
if (v) { applyBg(v); }
|
||||
var sel = $('#bgPicker');
|
||||
if (sel) {
|
||||
sel.addEventListener('change', function () {
|
||||
var v = sel.value;
|
||||
if (v === 'default') { v = ''; document.body.style.background = ''; localStorage.removeItem(BG_KEY); }
|
||||
else { applyBg(v); localStorage.setItem(BG_KEY, v); }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 搜索(含站内自动补全下拉) ---------- */
|
||||
function initSearch() {
|
||||
var form = $('#searchForm');
|
||||
if (!form) { return; }
|
||||
var input = $('#searchInput');
|
||||
var select = $('#searchEngine');
|
||||
|
||||
// 自动补全下拉
|
||||
var box = document.createElement('div');
|
||||
box.className = 'suggest-box';
|
||||
box.id = 'suggestBox';
|
||||
form.appendChild(box);
|
||||
var items = [];
|
||||
var active = -1;
|
||||
var timer = null;
|
||||
|
||||
function hideSuggest() {
|
||||
box.style.display = 'none';
|
||||
box.innerHTML = '';
|
||||
items = [];
|
||||
active = -1;
|
||||
}
|
||||
function highlight() {
|
||||
$all('.suggest-item', box).forEach(function (el, i) {
|
||||
el.classList.toggle('active', i === active);
|
||||
});
|
||||
var el = box.querySelector('.suggest-item.active');
|
||||
if (el) { el.scrollIntoView({ block: 'nearest' }); }
|
||||
}
|
||||
function goSuggest(id, url) {
|
||||
hideSuggest();
|
||||
if (id) { location.href = '/haonav/site/' + id + '.html'; }
|
||||
else if (url) { window.open(url, '_blank'); }
|
||||
}
|
||||
function showSuggest(list) {
|
||||
if (!list.length) { hideSuggest(); return; }
|
||||
items = list;
|
||||
box.innerHTML = list.map(function (it, i) {
|
||||
var icon = '/haonav/favicon.html?url=' + encodeURI(it.url);
|
||||
return '<div class="suggest-item' + (i === 0 ? ' active' : '') + '" data-i="' + i + '" data-id="' + it.id + '" data-url="' + encodeURI(it.url) + '">' +
|
||||
'<img src="' + icon + '" alt=""><span class="s-title">' + esc(it.title) + '</span><span class="s-url">' + esc(it.url) + '</span></div>';
|
||||
}).join('');
|
||||
box.style.display = 'block';
|
||||
active = 0;
|
||||
$all('.suggest-item', box).forEach(function (el) {
|
||||
// mousedown 优先于 input blur,避免点击丢失
|
||||
el.addEventListener('mousedown', function (e) {
|
||||
e.preventDefault();
|
||||
goSuggest(el.getAttribute('data-id'), el.getAttribute('data-url'));
|
||||
});
|
||||
});
|
||||
}
|
||||
function fetchSuggest(q) {
|
||||
fetch('/haonav/suggest.html?q=' + encodeURIComponent(q))
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (res) {
|
||||
if (res.code === 0 && res.data) { showSuggest(res.data); }
|
||||
else { hideSuggest(); }
|
||||
})
|
||||
.catch(function () { hideSuggest(); });
|
||||
}
|
||||
|
||||
input.addEventListener('input', function () {
|
||||
var q = input.value.trim();
|
||||
clearTimeout(timer);
|
||||
if (q.length < 1) { hideSuggest(); return; }
|
||||
timer = setTimeout(function () { fetchSuggest(q); }, 250);
|
||||
});
|
||||
input.addEventListener('keydown', function (e) {
|
||||
if (box.style.display === 'none') { return; }
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
active = Math.min(items.length - 1, active + 1);
|
||||
highlight();
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
active = Math.max(0, active - 1);
|
||||
highlight();
|
||||
} else if (e.key === 'Enter' && active >= 0 && items[active]) {
|
||||
e.preventDefault();
|
||||
goSuggest(items[active].id, items[active].url);
|
||||
} else if (e.key === 'Escape') {
|
||||
hideSuggest();
|
||||
}
|
||||
});
|
||||
document.addEventListener('click', function (e) {
|
||||
if (!form.contains(e.target)) { hideSuggest(); }
|
||||
});
|
||||
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
hideSuggest();
|
||||
var q = input.value.trim();
|
||||
if (!q) { input.focus(); return; }
|
||||
var engine = select ? select.value : 'baidu';
|
||||
if (engine === 'site') {
|
||||
location.href = '/haonav/search.html?q=' + encodeURIComponent(q);
|
||||
return;
|
||||
}
|
||||
var url = (ENGINE_URLS[engine] || ENGINE_URLS.baidu) + encodeURIComponent(q);
|
||||
window.open(url, '_blank');
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- 网站评分(详情页星星组件) ---------- */
|
||||
function paintStars(stars, n) {
|
||||
stars.forEach(function (s, i) { s.classList.toggle('on', i < n); });
|
||||
}
|
||||
function submitRate(id, score, tip) {
|
||||
fetch('/haonav/rate.html', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ id: id, score: score })
|
||||
}).then(function (r) { return r.json(); }).then(function (res) {
|
||||
if (res.code === 0) {
|
||||
if ($('#ratingScore')) { $('#ratingScore').textContent = res.data.rating; }
|
||||
if ($('#ratingCount')) { $('#ratingCount').textContent = res.data.rating_count + ' 人评分'; }
|
||||
paintStars($all('.star'), Math.round(res.data.rating));
|
||||
if (tip) { tip.textContent = '感谢评分!'; }
|
||||
} else if (res.code === 401) {
|
||||
if (tip) { tip.textContent = '请先登录后再评分'; }
|
||||
if (typeof window.openLoginModal === 'function') { window.openLoginModal(); }
|
||||
} else {
|
||||
if (tip) { tip.textContent = res.message || '评分失败'; }
|
||||
}
|
||||
}).catch(function () { if (tip) { tip.textContent = '网络错误,请稍后重试'; } });
|
||||
}
|
||||
function initRating() {
|
||||
var box = $('#ratingBox');
|
||||
if (!box) { return; }
|
||||
var id = box.getAttribute('data-id');
|
||||
var stars = $all('.star', box);
|
||||
var tip = $('#ratingTip');
|
||||
function base() { return Math.round(parseFloat(($('#ratingScore') ? $('#ratingScore').textContent : '0')) || 0); }
|
||||
paintStars(stars, base());
|
||||
stars.forEach(function (s) {
|
||||
s.addEventListener('mouseenter', function () {
|
||||
paintStars(stars, parseInt(s.getAttribute('data-score'), 10));
|
||||
});
|
||||
s.addEventListener('click', function () {
|
||||
submitRate(id, parseInt(s.getAttribute('data-score'), 10), tip);
|
||||
});
|
||||
});
|
||||
box.addEventListener('mouseleave', function () { paintStars(stars, base()); });
|
||||
}
|
||||
|
||||
/* ---------- 站内搜索结果(搜索页) ---------- */
|
||||
function initSiteSearch() {
|
||||
var box = $('#searchResults');
|
||||
if (!box) { return; }
|
||||
var input = $('#searchInput');
|
||||
var q = input ? input.value.trim() : '';
|
||||
function render(list) {
|
||||
if (!list.length) {
|
||||
box.innerHTML = '<p style="color:var(--nav-muted);padding:20px;">没有找到相关网址,换个关键词试试~</p>';
|
||||
return;
|
||||
}
|
||||
box.innerHTML = list.map(function (it) {
|
||||
var icon = it.show_icon || it.favicon || it.icon || '/static/haonav/img/default.png';
|
||||
return '<a class="search-item" href="/haonav/site/' + it.id + '.html" target="_blank">' +
|
||||
'<span class="rank-icon"><img src="' + icon + '" alt=""></span>' +
|
||||
'<span class="rank-meta"><h4>' + esc(it.title) + '</h4><p>' + esc(it.description || it.url) + '</p>' +
|
||||
(it.rating_count ? '<span class="rank-rating">⭐ ' + it.rating + ' (' + it.rating_count + ')</span>' : '') +
|
||||
'</span>' +
|
||||
'</a>';
|
||||
}).join('');
|
||||
}
|
||||
if (q) {
|
||||
fetch('/haonav/search.html?ajax=1&q=' + encodeURIComponent(q))
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (res) { render(res.data || []); })
|
||||
.catch(function () { box.innerHTML = '<p style="color:var(--nav-muted)">搜索失败</p>'; });
|
||||
} else {
|
||||
render([]);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 我的导航(收藏:登录云端同步 / 未登录本地存储) ---------- */
|
||||
var CLOUD = false; // 是否已登录、使用云端
|
||||
var favCache = []; // 当前收藏内存镜像(云端或本地)
|
||||
|
||||
function localGet() {
|
||||
try { return JSON.parse(localStorage.getItem(FAV_KEY)) || []; }
|
||||
catch (e) { return []; }
|
||||
}
|
||||
function localSet(list) { localStorage.setItem(FAV_KEY, JSON.stringify(list)); }
|
||||
|
||||
function api(action, body) {
|
||||
return fetch('/haonav/favorite/' + action, {
|
||||
method: body ? 'POST' : 'GET',
|
||||
credentials: 'same-origin',
|
||||
headers: body ? { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } : { 'X-Requested-With': 'XMLHttpRequest' },
|
||||
body: body ? JSON.stringify(body) : undefined
|
||||
}).then(function (r) { return r.json(); });
|
||||
}
|
||||
|
||||
function setTip(text) { var t = $('#myNavTip'); if (t) { t.textContent = text; } }
|
||||
|
||||
function renderFav() {
|
||||
var list = $('#myNavList');
|
||||
var empty = $('#myNavEmpty');
|
||||
if (!list) { return; }
|
||||
if (!favCache.length) {
|
||||
list.innerHTML = '';
|
||||
if (empty) { empty.style.display = 'block'; }
|
||||
return;
|
||||
}
|
||||
if (empty) { empty.style.display = 'none'; }
|
||||
list.innerHTML = favCache.map(function (it, i) {
|
||||
return '<div class="mynav-item" draggable="true" data-idx="' + i + '">' +
|
||||
'<img src="' + (it.icon || '/static/haonav/img/default.png') + '" alt="">' +
|
||||
'<a href="' + encodeURI(it.url) + '" target="_blank" title="' + esc(it.title) + '">' + esc(it.title) + '</a>' +
|
||||
'<span class="del" data-idx="' + i + '" title="移除">✕</span>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
bindFavDrag(list);
|
||||
}
|
||||
|
||||
function persistOrder() {
|
||||
if (CLOUD) {
|
||||
var ids = favCache.map(function (x) { return x.id; }).filter(Boolean);
|
||||
api('sort', { ids: ids });
|
||||
} else {
|
||||
localSet(favCache);
|
||||
}
|
||||
}
|
||||
|
||||
function bindFavDrag(list) {
|
||||
var dragEl = null;
|
||||
$all('.mynav-item', list).forEach(function (el) {
|
||||
el.addEventListener('dragstart', function () { dragEl = el; el.style.opacity = '.4'; });
|
||||
el.addEventListener('dragend', function () { el.style.opacity = '1'; dragEl = null; });
|
||||
el.addEventListener('dragover', function (e) { e.preventDefault(); });
|
||||
el.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
if (!dragEl || dragEl === el) { return; }
|
||||
var from = parseInt(dragEl.getAttribute('data-idx'), 10);
|
||||
var to = parseInt(el.getAttribute('data-idx'), 10);
|
||||
var moved = favCache.splice(from, 1)[0];
|
||||
favCache.splice(to, 0, moved);
|
||||
renderFav();
|
||||
persistOrder();
|
||||
});
|
||||
});
|
||||
$all('.del', list).forEach(function (del) {
|
||||
del.addEventListener('click', function () {
|
||||
var idx = parseInt(del.getAttribute('data-idx'), 10);
|
||||
var it = favCache[idx];
|
||||
favCache.splice(idx, 1);
|
||||
renderFav();
|
||||
if (CLOUD) { api('remove', { id: it && it.id, url: it && it.url }); }
|
||||
else { localSet(favCache); }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function addFav(data) {
|
||||
if (favCache.some(function (x) { return x.url === data.url; })) {
|
||||
alert('已经在我的导航里啦');
|
||||
return;
|
||||
}
|
||||
if (CLOUD) {
|
||||
api('add', data).then(function (res) {
|
||||
if (res.code === 0) {
|
||||
data.id = (res.data && res.data.id) || 0;
|
||||
favCache.push(data);
|
||||
renderFav();
|
||||
} else if (res.code === 401) {
|
||||
CLOUD = false; addFav(data);
|
||||
} else { alert(res.message || '收藏失败'); }
|
||||
}).catch(function () { alert('网络错误'); });
|
||||
} else {
|
||||
favCache.push(data);
|
||||
localSet(favCache);
|
||||
renderFav();
|
||||
}
|
||||
}
|
||||
|
||||
function initFav() {
|
||||
// 先本地渲染,随后尝试云端
|
||||
favCache = localGet();
|
||||
renderFav();
|
||||
api('list').then(function (res) {
|
||||
if (res.code === 0) {
|
||||
CLOUD = true;
|
||||
var local = localGet();
|
||||
var cloud = res.data || [];
|
||||
if (local.length) {
|
||||
// 登录后把本地收藏合并上云,只做一次
|
||||
api('merge', { items: local }).then(function (m) {
|
||||
favCache = (m.code === 0 ? m.data : cloud) || [];
|
||||
localStorage.removeItem(FAV_KEY);
|
||||
setTip('(已登录,云端同步 · 可拖拽排序)');
|
||||
renderFav();
|
||||
});
|
||||
} else {
|
||||
favCache = cloud;
|
||||
setTip('(已登录,云端同步 · 可拖拽排序)');
|
||||
renderFav();
|
||||
}
|
||||
} else {
|
||||
// 未登录:保持本地
|
||||
CLOUD = false;
|
||||
setTip('(未登录,仅存本机;登录后可跨设备同步)');
|
||||
}
|
||||
}).catch(function () { CLOUD = false; });
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
var btn = e.target.closest && e.target.closest('.js-fav');
|
||||
if (btn) {
|
||||
addFav({
|
||||
id: btn.getAttribute('data-id'),
|
||||
title: btn.getAttribute('data-title'),
|
||||
url: btn.getAttribute('data-url'),
|
||||
icon: btn.getAttribute('data-icon')
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- 收藏夹分享(登录用户生成只读分享链接) ---------- */
|
||||
function initFavShare() {
|
||||
var btn = $('#favShareBtn');
|
||||
var mask = $('#favShareMask');
|
||||
if (!btn || !mask) { return; }
|
||||
|
||||
var toggle = $('#favShareToggle');
|
||||
var titleInp = $('#favShareTitle');
|
||||
var linkBox = $('#favShareLinkBox');
|
||||
var linkInp = $('#favShareLink');
|
||||
var viewsEl = $('#favShareViews');
|
||||
var msg = $('#favShareMsg');
|
||||
|
||||
function setMsg(t) { if (msg) { msg.textContent = t || ''; } }
|
||||
function fill(d) {
|
||||
toggle.checked = !!d.enabled;
|
||||
titleInp.value = d.title || '';
|
||||
linkInp.value = d.url || '';
|
||||
if (viewsEl) { viewsEl.textContent = d.views || 0; }
|
||||
linkBox.style.display = d.enabled ? 'block' : 'none';
|
||||
}
|
||||
function save(extra) {
|
||||
var body = { enabled: toggle.checked ? 1 : 0, title: titleInp.value.trim() };
|
||||
if (extra && extra.reset) { body.reset = 1; }
|
||||
setMsg('保存中…');
|
||||
api('sharesave', body).then(function (res) {
|
||||
if (res.code === 0) { fill(res.data); setMsg('已保存'); }
|
||||
else if (res.code === 401) { setMsg('请先登录'); if (typeof window.openLoginModal === 'function') { window.openLoginModal(); } }
|
||||
else { setMsg(res.message || '保存失败'); }
|
||||
}).catch(function () { setMsg('网络错误'); });
|
||||
}
|
||||
|
||||
btn.addEventListener('click', function () {
|
||||
setMsg('加载中…');
|
||||
mask.classList.add('show');
|
||||
api('share').then(function (res) {
|
||||
if (res.code === 0) { fill(res.data); setMsg(''); }
|
||||
else if (res.code === 401) {
|
||||
setMsg('登录后即可生成分享链接');
|
||||
linkBox.style.display = 'none';
|
||||
if (typeof window.openLoginModal === 'function') { window.openLoginModal(); }
|
||||
} else { setMsg(res.message || '加载失败'); }
|
||||
}).catch(function () { setMsg('网络错误'); });
|
||||
});
|
||||
|
||||
toggle.addEventListener('change', function () { save(); });
|
||||
titleInp.addEventListener('blur', function () { if (toggle.checked) { save(); } });
|
||||
|
||||
var copyBtn = $('#favShareCopy');
|
||||
if (copyBtn) {
|
||||
copyBtn.addEventListener('click', function () {
|
||||
linkInp.select();
|
||||
try { document.execCommand('copy'); setMsg('已复制链接'); }
|
||||
catch (e) {
|
||||
if (navigator.clipboard) { navigator.clipboard.writeText(linkInp.value); setMsg('已复制链接'); }
|
||||
}
|
||||
});
|
||||
}
|
||||
var resetBtn = $('#favShareReset');
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
if (confirm('重置后旧链接将立即失效,确定?')) { save({ reset: true }); }
|
||||
});
|
||||
}
|
||||
mask.addEventListener('click', function (e) {
|
||||
if (e.target === mask || e.target.classList.contains('close')) { mask.classList.remove('show'); }
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- 顶部小组件:天气 + 实时热搜 ---------- */
|
||||
function initWeather() {
|
||||
var box = $('#weatherWidget');
|
||||
if (!box) { return; }
|
||||
fetch('/haonav/widget/weather', { credentials: 'same-origin' })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (res) {
|
||||
var d = res.data;
|
||||
if (res.code !== 0 || !d || !d.city) { return; }
|
||||
$('#wCity').textContent = d.city;
|
||||
$('#wType').textContent = d.type || '';
|
||||
$('#wTemp').textContent = (d.low || '') + ' ~ ' + (d.high || '');
|
||||
if (d.tips) { $('#wTips').textContent = d.tips; }
|
||||
box.style.display = 'flex';
|
||||
}).catch(function () { });
|
||||
}
|
||||
|
||||
function loadHot(src) {
|
||||
var box = $('#hotSearchWidget');
|
||||
var ol = $('#hotSearchList');
|
||||
if (!ol) { return; }
|
||||
fetch('/haonav/widget/hot?source=' + encodeURIComponent(src), { credentials: 'same-origin' })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (res) {
|
||||
var list = res.data || [];
|
||||
if (res.code !== 0 || !list.length) {
|
||||
if (!box.dataset.shown) { box.style.display = 'none'; }
|
||||
return;
|
||||
}
|
||||
box.style.display = 'block';
|
||||
box.dataset.shown = '1';
|
||||
ol.innerHTML = list.map(function (it, i) {
|
||||
var link = it.url
|
||||
? '<a href="' + encodeURI(it.url) + '" target="_blank" title="' + esc(it.title) + '">' + esc(it.title) + '</a>'
|
||||
: '<a href="https://www.baidu.com/s?wd=' + encodeURIComponent(it.title) + '" target="_blank">' + esc(it.title) + '</a>';
|
||||
return '<li><i class="rk">' + (i + 1) + '</i>' + link + '</li>';
|
||||
}).join('');
|
||||
}).catch(function () { });
|
||||
}
|
||||
|
||||
function initHotSearch() {
|
||||
var tabs = $('#hsTabs');
|
||||
if (!tabs) { return; }
|
||||
var def = 'baidu';
|
||||
loadHot(def);
|
||||
tabs.addEventListener('click', function (e) {
|
||||
var a = e.target.closest && e.target.closest('a[data-src]');
|
||||
if (!a) { return; }
|
||||
$all('a', tabs).forEach(function (x) { x.classList.remove('active'); });
|
||||
a.classList.add('active');
|
||||
loadHot(a.getAttribute('data-src'));
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- 二维码 ---------- */
|
||||
function initQr() {
|
||||
var mask = $('#qrMask');
|
||||
if (!mask) { return; }
|
||||
var img = $('#qrImg');
|
||||
var txt = $('#qrText');
|
||||
var close = function () { mask.classList.remove('show'); };
|
||||
mask.addEventListener('click', function (e) { if (e.target === mask || e.target.classList.contains('close')) { close(); } });
|
||||
document.addEventListener('click', function (e) {
|
||||
var btn = e.target.closest && e.target.closest('.js-qr');
|
||||
if (btn) {
|
||||
var url = btn.getAttribute('data-url');
|
||||
img.src = 'https://api.qrserver.com/v1/create-qr-code/?size=220x220&data=' + encodeURIComponent(url);
|
||||
txt.textContent = url;
|
||||
mask.classList.add('show');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- 键盘快捷键 ---------- */
|
||||
function initShortcuts() {
|
||||
document.addEventListener('keydown', function (e) {
|
||||
var tag = (e.target.tagName || '').toLowerCase();
|
||||
if (tag === 'input' || tag === 'textarea' || tag === 'select') { return; }
|
||||
if (e.key === '/') {
|
||||
var inp = $('#searchInput');
|
||||
if (inp) { e.preventDefault(); inp.focus(); }
|
||||
return;
|
||||
}
|
||||
if (/^[1-9]$/.test(e.key)) {
|
||||
var blocks = $all('.category-block');
|
||||
var idx = parseInt(e.key, 10) - 1;
|
||||
if (blocks[idx]) { blocks[idx].scrollIntoView({ behavior: 'smooth', block: 'start' }); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- 投稿表单 ---------- */
|
||||
function initSubmit() {
|
||||
var form = $('#submitForm');
|
||||
if (!form) { return; }
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
var msg = $('#submitMsg');
|
||||
var fd = new FormData(form);
|
||||
fetch('/haonav/submit.html', { method: 'POST', body: fd, headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (res) {
|
||||
msg.textContent = res.msg || (res.code === 0 ? '提交成功' : '提交失败');
|
||||
msg.className = 'form-msg ' + (res.code === 0 ? 'ok' : 'err');
|
||||
if (res.code === 0) { form.reset(); }
|
||||
})
|
||||
.catch(function () { msg.textContent = '网络错误,请稍后重试'; msg.className = 'form-msg err'; });
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- 网站截图悬停预览 ---------- */
|
||||
function initSnapshotPreview() {
|
||||
if (!window.matchMedia || !window.matchMedia('(hover: hover)').matches) { return; }
|
||||
var cards = $all('.website-card[data-sid]');
|
||||
if (!cards.length) { return; }
|
||||
|
||||
var tip = document.createElement('div');
|
||||
tip.id = 'snapPreview';
|
||||
tip.style.cssText = 'position:fixed;z-index:9999;display:none;width:320px;padding:6px;background:var(--nav-card-bg,#fff);border-radius:10px;box-shadow:0 8px 30px rgba(0,0,0,.25);pointer-events:none;';
|
||||
var img = document.createElement('img');
|
||||
img.style.cssText = 'width:100%;border-radius:6px;display:block;min-height:60px;background:#f0f1f5;';
|
||||
tip.appendChild(img);
|
||||
document.body.appendChild(tip);
|
||||
|
||||
var timer = null;
|
||||
function hide() { clearTimeout(timer); timer = null; tip.style.display = 'none'; }
|
||||
|
||||
document.addEventListener('mouseover', function (e) {
|
||||
var card = e.target.closest && e.target.closest('.website-card[data-sid]');
|
||||
if (!card) { return; }
|
||||
var sid = card.getAttribute('data-sid');
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(function () {
|
||||
img.style.opacity = '0';
|
||||
img.onload = function () {
|
||||
// 默认占位小图不展示,只有真实截图(宽>120px)才显示
|
||||
if (img.naturalWidth <= 120) { hide(); return; }
|
||||
img.style.opacity = '1';
|
||||
};
|
||||
img.src = '/haonav/snapshot.html?id=' + sid;
|
||||
var rect = card.getBoundingClientRect();
|
||||
var left = rect.right + 12;
|
||||
if (left + 330 > window.innerWidth) { left = rect.left - 332; }
|
||||
if (left < 4) { left = 4; }
|
||||
var top = Math.min(rect.top, window.innerHeight - 260);
|
||||
tip.style.left = left + 'px';
|
||||
tip.style.top = Math.max(4, top) + 'px';
|
||||
tip.style.display = 'block';
|
||||
}, 450);
|
||||
});
|
||||
document.addEventListener('mouseout', function (e) {
|
||||
var card = e.target.closest && e.target.closest('.website-card[data-sid]');
|
||||
if (card) { hide(); }
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- PWA:Service Worker + 安装 + 设为首页引导 ---------- */
|
||||
function initPwa() {
|
||||
// 仅在导航首页(有 manifest)注册
|
||||
if (document.querySelector('link[rel="manifest"]') && 'serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/haonav/sw.html', { scope: '/haonav/' }).catch(function () { });
|
||||
}
|
||||
|
||||
var installBtn = $('#pwaInstallBtn');
|
||||
var deferredPrompt = null;
|
||||
window.addEventListener('beforeinstallprompt', function (e) {
|
||||
e.preventDefault();
|
||||
deferredPrompt = e;
|
||||
if (installBtn) { installBtn.style.display = ''; }
|
||||
});
|
||||
if (installBtn) {
|
||||
installBtn.addEventListener('click', function () {
|
||||
if (!deferredPrompt) { return; }
|
||||
deferredPrompt.prompt();
|
||||
deferredPrompt.userChoice.then(function () {
|
||||
deferredPrompt = null;
|
||||
installBtn.style.display = 'none';
|
||||
});
|
||||
});
|
||||
}
|
||||
window.addEventListener('appinstalled', function () {
|
||||
if (installBtn) { installBtn.style.display = 'none'; }
|
||||
});
|
||||
|
||||
var homeBtn = $('#setHomeBtn');
|
||||
var mask = $('#homeTipMask');
|
||||
if (homeBtn && mask) {
|
||||
homeBtn.addEventListener('click', function () {
|
||||
var u = $('#homeTipUrl');
|
||||
if (u) { u.textContent = location.origin + '/haonav/index.html'; }
|
||||
mask.classList.add('show');
|
||||
});
|
||||
mask.addEventListener('click', function (e) {
|
||||
if (e.target === mask || e.target.classList.contains('close')) { mask.classList.remove('show'); }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 惰性死链巡检 ping(服务端有缓存锁限频,前端只管发) ---------- */
|
||||
function initAutoCheck() {
|
||||
if (!$('#searchForm')) { return; } // 仅导航页面触发
|
||||
setTimeout(function () {
|
||||
fetch('/haonav/task/autocheck', { credentials: 'same-origin', keepalive: true }).catch(function () { });
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
|
||||
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
initTheme();
|
||||
initBg();
|
||||
initSearch();
|
||||
initSiteSearch();
|
||||
initFav();
|
||||
initFavShare();
|
||||
initQr();
|
||||
initShortcuts();
|
||||
initSubmit();
|
||||
initRating();
|
||||
initWeather();
|
||||
initHotSearch();
|
||||
initSnapshotPreview();
|
||||
initPwa();
|
||||
initAutoCheck();
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user