/* ========== 网址导航 前端交互 ========== */ (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 '
' + '' + esc(it.title) + '' + esc(it.url) + '
'; }).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 = '

没有找到相关网址,换个关键词试试~

'; return; } box.innerHTML = list.map(function (it) { var icon = it.show_icon || it.favicon || it.icon || '/static/haonav/img/default.png'; return '' + '' + '

' + esc(it.title) + '

' + esc(it.description || it.url) + '

' + (it.rating_count ? '⭐ ' + it.rating + ' (' + it.rating_count + ')' : '') + '
' + '
'; }).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 = '

搜索失败

'; }); } 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 '
' + '' + '' + esc(it.title) + '' + '' + '
'; }).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 ? '' + esc(it.title) + '' : '' + esc(it.title) + ''; return '
  • ' + (i + 1) + '' + link + '
  • '; }).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(); }); })();