NewHome/assets/js/common.js

579 lines
26 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.

/* common.js —— 全站通用交互
* 功能:昼/夜主题切换、实时时钟、随机名言刷新、四大板块 hover 浮层、
* 搜索栏(引擎下拉/清除/回车搜索、复制到剪贴板、DOM 工具函数
*/
(function (global) {
'use strict';
/* ---------- 工具 ---------- */
function $(sel, root) { return (root || document).querySelector(sel); }
function $$(sel, root) { return Array.prototype.slice.call((root || document).querySelectorAll(sel)); }
/* ---------- 主题(昼/夜) ---------- */
var THEME_KEY = 'hp-theme';
function currentTheme() {
var saved = null;
try { saved = localStorage.getItem(THEME_KEY); } catch (e) { /* ignore */ }
if (saved === 'light' || saved === 'dark') return saved;
// 未设置:跟随系统
return (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
}
function applyTheme(t, persist) {
document.documentElement.setAttribute('data-theme', t);
if (persist) {
try { localStorage.setItem(THEME_KEY, t); } catch (e) { /* ignore */ }
}
var icons = $$('.js-theme-icon');
icons.forEach(function (el) { el.textContent = (t === 'dark') ? '☀' : '☾'; });
var btn = $('.js-theme-btn');
if (btn) btn.title = (t === 'dark') ? '切换到白天模式' : '切换到黑夜模式';
}
function initTheme() {
applyTheme(currentTheme(), false);
$$('.js-theme-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
var next = (document.documentElement.getAttribute('data-theme') === 'dark') ? 'light' : 'dark';
applyTheme(next, true);
});
});
if (window.matchMedia) {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () {
// 用户手动选过则不跟随系统变化
try {
if (!localStorage.getItem(THEME_KEY)) applyTheme(currentTheme(), false);
} catch (e) { applyTheme(currentTheme(), false); }
});
}
}
/* ---------- 实时时钟 ---------- */
function pad(n) { return (n < 10 ? '0' : '') + n; }
function formatTime(d) {
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' +
pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());
}
function initClocks() {
function tick() { $$('.js-clock').forEach(function (el) { el.textContent = formatTime(new Date()); }); }
tick();
setInterval(tick, 1000);
}
/* ---------- 名言条:点击格言本身切换 ---------- */
function initQuoteRefresh() {
$$('.js-quote').forEach(function (quote) {
quote.addEventListener('click', function () {
var list = $$('.js-quote-pool span', document); // 服务端把所有名言放入隐藏池
if (!list.length) return;
// 定位当前展示文本在池中的位置,避免换到下一条仍是同一条
var currentText = quote.textContent;
var cur = -1;
for (var i = 0; i < list.length; i++) {
if (list[i].textContent === currentText) { cur = i; break; }
}
var n = list.length;
var idx = Math.floor(Math.random() * n);
if (n > 1 && cur !== -1) {
while (idx === cur) idx = (idx + 1) % n;
}
quote.textContent = list[idx].textContent;
});
});
}
/* ---------- 首页四大板块 hover 展开说明 ---------- */
function initBoardHover() {
var section = $('.js-boards');
if (!section) return;
var cards = $$('.js-board-card', section);
var overlay = $('.js-board-panel', section);
if (!cards.length || !overlay) return;
var titleEl = $('.js-board-panel-title', overlay);
var descEl = $('.js-board-panel-desc', overlay);
var bodyEl = section;
function showPanel(card) {
var name = card.getAttribute('data-name') || '';
var desc = card.getAttribute('data-desc') || '';
var idx = cards.indexOf(card);
if (titleEl) titleEl.textContent = name;
if (descEl) descEl.textContent = desc;
overlay.classList.remove('panel-left', 'panel-right');
// 第 0 / 1 块吸附左侧边缘,第 2 / 3 块吸附右侧边缘(实际板块位置不变)
if (idx === 0 || idx === 1) overlay.classList.add('panel-left');
else overlay.classList.add('panel-right');
overlay.classList.add('visible');
}
cards.forEach(function (card) {
card.addEventListener('mouseenter', function () { showPanel(card); });
card.addEventListener('click', function () { /* 点击由 <a> 包裹,这里无额外处理 */ });
});
section.addEventListener('mouseleave', function () { overlay.classList.remove('visible'); });
}
/* ---------- 搜索栏 ---------- */
var DEFAULT_ENGINES = [
{ key: 'bing', name: 'Bing', icon: 'B', url: 'https://www.bing.com/search?q={kw}' },
{ key: 'baidu', name: '百度', icon: '度', url: 'https://www.baidu.com/s?wd={kw}' },
{ key: 'github', name: 'GitHub', icon: 'G', url: 'https://github.com/search?q={kw}' },
{ key: 'google', name: 'Google', icon: 'G', url: 'https://www.google.com/search?q={kw}' }
];
function initSearch() {
var box = $('.js-search');
if (!box) return;
var cfg = null;
try { cfg = JSON.parse(box.getAttribute('data-engines') || 'null'); } catch (e) { cfg = null; }
var engines = (cfg && cfg.length) ? cfg : DEFAULT_ENGINES;
var curKey = box.getAttribute('data-engine') || engines[0].key;
var cur = engines.filter(function (e) { return e.key === curKey; })[0] || engines[0];
var input = $('.js-search-input', box);
var clearBtn = $('.js-search-clear', box);
var engineBtn = $('.js-engine-btn', box);
var engineName = $('.js-engine-name', box);
var menu = $('.js-engine-menu', box);
function setEngine(e) {
cur = e;
// 按钮区域只显示图标(全称在展开下拉列表时展示)
if (engineName) engineName.textContent = e.icon || '';
if (engineBtn) engineBtn.title = '搜索引擎:' + e.name;
box.setAttribute('data-engine', e.key);
hideMenu();
}
function showMenu() {
if (!menu) return;
if (!menu.children.length) {
engines.forEach(function (e) {
var li = document.createElement('li');
li.textContent = e.icon + ' ' + e.name;
li.className = (e.key === cur.key) ? 'active' : '';
li.addEventListener('click', function () { setEngine(e); });
menu.appendChild(li);
});
}
menu.classList.add('open');
}
function hideMenu() { if (menu) menu.classList.remove('open'); }
function updateClear() {
if (clearBtn) clearBtn.style.visibility = (input && input.value) ? 'visible' : 'hidden';
}
function doSearch() {
var kw = (input ? input.value : '').trim();
if (!kw) { if (input) input.focus(); return; }
window.open(cur.url.replace('{kw}', encodeURIComponent(kw)), '_blank', 'noopener');
}
if (engineBtn) engineBtn.addEventListener('click', function (ev) {
ev.stopPropagation();
if (menu && menu.classList.contains('open')) hideMenu(); else showMenu();
});
if (clearBtn) clearBtn.addEventListener('click', function () {
if (input) { input.value = ''; input.focus(); }
updateClear();
});
if (input) {
input.addEventListener('input', updateClear);
input.addEventListener('keydown', function (ev) { if (ev.key === 'Enter') doSearch(); });
}
document.addEventListener('click', function () { hideMenu(); });
var goBtn = $('.js-search-go', box);
if (goBtn) goBtn.addEventListener('click', doSearch);
setEngine(cur);
updateClear();
}
/* ---------- 后台图标控件:文本 / 图片上传≤200KB → dataURL ---------- */
function isIconImage(v) {
v = String(v || '').trim();
if (!v) return false;
if (/^data:image\//i.test(v) || /^https?:\/\//i.test(v)) return true;
if (/\.(png|jpe?g|gif|svg|webp)(\?|#|$)/i.test(v)) return true;
return v.charAt(0) === '/' || v.indexOf('./') === 0 || v.indexOf('../') === 0;
}
function updateIconPreview(ctl) {
var input = ctl.querySelector('.icon-input');
var box = ctl.querySelector('.icon-preview');
if (!input || !box) return;
var v = input.value.trim();
box.innerHTML = '';
if (!v) return; // 空:显示空心占位框
if (isIconImage(v)) {
var img = document.createElement('img');
img.src = v;
img.alt = '';
img.loading = 'lazy';
box.appendChild(img);
} else {
box.textContent = v;
}
}
function initIconControls() {
// 点击“上传”→ 触发隐藏 file
document.addEventListener('click', function (ev) {
var btn = ev.target && ev.target.closest ? ev.target.closest('.icon-upload') : null;
if (!btn) return;
var ctl = btn.closest('.icon-ctl');
var file = ctl ? ctl.querySelector('.icon-file') : null;
if (file) file.click();
});
// 选择图片 → 校验并转 dataURL 写入同控件文本框
document.addEventListener('change', function (ev) {
var file = ev.target;
if (!file || !file.classList || !file.classList.contains('icon-file')) return;
var ctl = file.closest('.icon-ctl');
var input = ctl ? ctl.querySelector('.icon-input') : null;
var f = file.files && file.files[0];
function reset() { file.value = ''; }
if (!f) return;
if (f.type.indexOf('image/') !== 0) { alert('请选择图片文件。'); reset(); return; }
if (f.size > 200 * 1024) { alert('图片不能超过 200KB。'); reset(); return; }
var rd = new FileReader();
rd.onload = function () {
if (input) {
input.value = rd.result;
updateIconPreview(ctl);
}
reset();
};
rd.onerror = function () { alert('图片读取失败,请重试。'); reset(); };
rd.readAsDataURL(f);
});
// 手动输入文本 → 实时预览
document.addEventListener('input', function (ev) {
var t = ev.target;
if (t && t.classList && t.classList.contains('icon-input')) {
updateIconPreview(t.closest('.icon-ctl'));
}
});
// 初始化页面既有控件预览
$$('.icon-ctl').forEach(updateIconPreview);
}
/* ---------- 复制 ---------- */
function copyText(text, btnEl) {
function done(ok) {
if (!btnEl) return;
var old = btnEl.textContent;
btnEl.textContent = ok ? '✓ 已复制' : '复制失败';
setTimeout(function () { btnEl.textContent = old; }, 1500);
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function () { done(true); }, function () { fallbackCopy(text, done); });
} else {
fallbackCopy(text, done);
}
}
function fallbackCopy(text, done) {
var ta = document.createElement('textarea');
ta.value = text;
ta.style.cssText = 'position:fixed;opacity:0;';
document.body.appendChild(ta);
ta.select();
try { done(document.execCommand('copy')); } catch (e) { done(false); }
document.body.removeChild(ta);
}
function initCopyButtons() {
document.addEventListener('click', function (ev) {
var btn = ev.target.closest ? ev.target.closest('.js-copy') : null;
if (!btn) return;
var text = btn.getAttribute('data-copy');
if (text === null || text === undefined) {
var target = $(btn.getAttribute('data-target') || '');
text = target ? target.value : '';
}
copyText(text, btn);
});
}
/* ---------- 站点风格确认弹窗(回车确认 / Esc 取消 / 点击遮罩取消) ---------- */
function escHtml(s) {
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function siteConfirm(message, opts) {
opts = opts || {};
return new Promise(function (resolve) {
var danger = opts.danger !== false;
var okText = opts.okText || (danger ? '删除' : '确认');
var overlay = document.createElement('div');
overlay.className = 'sc-overlay';
overlay.innerHTML =
'<div class="sc-card" role="alertdialog" aria-modal="true">' +
'<div class="sc-head">' + escHtml(opts.title || '操作确认') + '</div>' +
'<div class="sc-body">' + escHtml(message || '') + '</div>' +
'<div class="sc-foot">' +
'<button type="button" class="btn btn-sm sc-cancel">' + escHtml(opts.cancelText || '取消') + '</button>' +
'<button type="button" class="btn btn-sm ' + (danger ? 'btn-danger' : 'btn-primary') + ' sc-ok">' + escHtml(okText) + '</button>' +
'</div></div>';
var okBtn = overlay.querySelector('.sc-ok');
var cancelBtn = overlay.querySelector('.sc-cancel');
var finished = false;
function close(val) {
if (finished) return;
finished = true;
document.removeEventListener('keydown', onKey, true);
if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
resolve(val);
}
function onKey(ev) {
if (ev.key === 'Escape') { ev.preventDefault(); close(false); }
else if (ev.key === 'Enter') { ev.preventDefault(); close(true); }
}
okBtn.addEventListener('click', function () { close(true); });
cancelBtn.addEventListener('click', function () { close(false); });
overlay.addEventListener('mousedown', function (ev) { if (ev.target === overlay) close(false); });
document.addEventListener('keydown', onKey, true);
document.body.appendChild(overlay);
okBtn.focus();
});
}
// 统一入口:旧浏览器/异常时回落原生 confirm仍然以 Promise 风格使用)
function askConfirm(message, opts) {
return siteConfirm(message, opts);
}
// 表单:带 data-confirm="提示文案" 的表单提交前弹窗确认
function bindConfirmForms() {
document.addEventListener('submit', function (ev) {
var f = ev.target;
if (!f || !f.getAttribute || !f.hasAttribute('data-confirm')) return;
ev.preventDefault();
siteConfirm(f.getAttribute('data-confirm')).then(function (ok) {
if (ok) { f.submit(); } else { hpClearScrollY(); }
});
});
}
/* ---------- 同页 POST 提交后保持滚动位置(整页刷新不再跳回顶部) ---------- */
var HP_SCROLL_KEY = 'hp-scroll-pos';
function hpSamePagePost(f) {
if (!f || !f.method) return false;
if (!/post/i.test(f.method)) return false;
if (f.target === '_blank') return false;
var a = (f.getAttribute('action') || '').trim();
if (a === '') return true; // 无 action = 提交当前页
try {
var cur = new URL(location.href.split('#')[0]);
var u = new URL(a, cur.href);
return u.origin === cur.origin && u.pathname === cur.pathname;
} catch (e) { return false; }
}
function hpSaveScrollY() {
try {
var y = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
sessionStorage.setItem(HP_SCROLL_KEY, JSON.stringify({ p: location.pathname + location.search, y: y }));
} catch (e) { /* ignore */ }
}
function hpClearScrollY() {
try { sessionStorage.removeItem(HP_SCROLL_KEY); } catch (e) { /* ignore */ }
}
var hpScrollTarget = -1;
function hpRestoreScrollY() {
if (hpScrollTarget >= 0) { window.scrollTo(0, hpScrollTarget); return; }
try {
var raw = sessionStorage.getItem(HP_SCROLL_KEY);
if (!raw) return;
sessionStorage.removeItem(HP_SCROLL_KEY);
var d = JSON.parse(raw);
if (d && d.p === (location.pathname + location.search) && typeof d.y === 'number' && d.y > 0) {
hpScrollTarget = d.y;
window.scrollTo(0, hpScrollTarget);
}
} catch (e) { hpClearScrollY(); }
}
// 捕获阶段:同页 POST 提交前保存(按回车等也会触发 submit 事件)
document.addEventListener('submit', function (ev) {
if (hpSamePagePost(ev.target)) hpSaveScrollY();
}, true);
// 捕获阶段点击提交按钮时也保存bindConfirmForms 确认后走 f.submit() 不触发 submit 事件)
document.addEventListener('click', function (ev) {
var t = ev.target;
var btn = (t && t.closest) ? t.closest('button[type="submit"], input[type="submit"], button:not([type])') : null;
if (!btn) return;
var f = btn.form || null;
if (hpSamePagePost(f)) hpSaveScrollY();
}, true);
// 资源加载完成后再定位一次,防止图片等异步资源改变高度造成轻微偏移
window.addEventListener('load', function () {
if (hpScrollTarget >= 0) {
window.scrollTo(0, hpScrollTarget);
hpScrollTarget = -1;
}
});
/* ---------- 首页访客信息hero 信息模式:补齐屏幕分辨率 / 系统语言) ---------- */
function initVisitorInfo() {
var box = $('.js-visitor-box');
if (!box) return;
var langEl = $('.js-vlang', box);
if (langEl) {
var lang = '';
try { lang = navigator.language || (navigator.languages && navigator.languages[0]) || ''; } catch (e) { /* ignore */ }
langEl.textContent = lang || '--';
}
var scrEl = $('.js-vscreen', box);
if (scrEl) {
var w = 0, h = 0, dpr = 1;
try { w = window.screen.width || 0; h = window.screen.height || 0; dpr = window.devicePixelRatio || 1; } catch (e) { /* ignore */ }
if (w && h) {
var ratio = Math.round(dpr * 100);
scrEl.textContent = (ratio !== 100) ? (w + ' × ' + h + '' + ratio + '%') : (w + ' × ' + h);
}
}
}
/* ---------- 返回顶部(右下角按钮,滚动后出现;兼容内滚容器页面) ---------- */
function initToTop() {
var btn = document.querySelector('.js-to-top');
if (!btn) return;
var scrollables = [];
var doc = document.scrollingElement || document.documentElement;
if (doc) scrollables.push(doc);
Array.prototype.forEach.call(document.querySelectorAll('*'), function (el) {
if (el === doc || el === btn) return;
if (el.scrollHeight <= el.clientHeight + 40) return;
var cs;
try { cs = window.getComputedStyle(el); } catch (e) { cs = null; }
if (!cs) return;
if (cs.overflowY === 'auto' || cs.overflowY === 'scroll') scrollables.push(el);
});
function measure() {
var v = 0;
for (var i = 0; i < scrollables.length; i++) {
if (scrollables[i].scrollTop > v) v = scrollables[i].scrollTop;
}
btn.classList.toggle('show', v > 260);
}
for (var s = 0; s < scrollables.length; s++) {
scrollables[s].addEventListener('scroll', measure, { passive: true });
}
window.addEventListener('resize', measure);
btn.addEventListener('click', function () {
var root = doc || document.documentElement;
if (root && root.scrollTop > 0) root.scrollTo({ top: 0, behavior: 'smooth' });
else window.scrollTo({ top: 0, behavior: 'smooth' });
});
measure();
}
/* ---------- 功能区静态工具页:库卡片 ?from=nav 进入时,返回按钮改为“返回上一页” ---------- */
function initFuncBack() {
var el = document.querySelector('.js-fb-back');
if (!el) return;
if (!/(?:^|[?&])from=nav(?:&|$)/.test(location.search)) return;
el.textContent = '← 返回上一页';
el.title = '返回上一页(回到来源库首页)';
el.addEventListener('click', function (ev) {
// 有浏览历史时返回来源页;否则保留默认 href功能区首页
if (window.history.length > 1) {
ev.preventDefault();
window.history.back();
}
});
}
/* ---------- 右下角 Toast 通知(后台整页刷新后的成功/失败提示使用,不影响页面布局) ---------- */
function hpToast(kind, text, dur) {
if (text === '' || text === null || typeof text === 'undefined') return;
kind = (kind === 'ok' || kind === 'err') ? kind : 'ok';
var wrap = document.querySelector('.hp-toasts');
if (!wrap) {
wrap = document.createElement('div');
wrap.className = 'hp-toasts';
document.body.appendChild(wrap);
}
var box = document.createElement('div');
box.className = 'hp-toast ' + kind;
var ic = document.createElement('span');
ic.className = 'ht-ic';
ic.textContent = kind === 'ok' ? '✓' : '!';
var tx = document.createElement('div');
tx.className = 'ht-tx';
tx.textContent = String(text);
var x = document.createElement('button');
x.type = 'button';
x.className = 'ht-x';
x.setAttribute('aria-label', '关闭提示');
x.textContent = '×';
function close() {
if (!box.parentNode) return;
box.classList.add('hide');
setTimeout(function () {
if (box.parentNode) box.parentNode.removeChild(box);
if (wrap.parentNode && !wrap.children.length) wrap.parentNode.removeChild(wrap);
}, 240);
}
x.addEventListener('click', close);
box.appendChild(ic);
box.appendChild(tx);
box.appendChild(x);
wrap.appendChild(box);
var delay = (typeof dur === 'number' && dur > 0) ? dur : (kind === 'err' ? 6000 : 3600);
setTimeout(close, delay);
}
/* 由 PHP 注入的整页操作提示window.hpToastMsg = {kind, text})自动弹出 */
function hpShowPageToast() {
var m = window.hpToastMsg;
if (!m || typeof m !== 'object') return;
hpToast(m.kind, m.text);
}
/* ---------- 静态工具页 favicon 跟随后台站点 logoapi/site.php 探测,异常静默回退默认) ---------- */
function initSiteIcon() {
// 仅纯静态 .html 页需要PHP 页面 favicon 已由 layout_head 服务端输出)
if (!/\.html$/i.test(location.pathname)) return;
var iconEl = document.querySelector('link[rel~="icon"]');
if (!iconEl) return;
var abs = '';
try { abs = new URL(iconEl.getAttribute('href') || '', location.href).href; } catch (e) { return; }
var marker = '/assets/img/logo.svg';
var pos = abs.indexOf(marker);
if (pos < 0) return; // 非默认图标(已自定义)则无需处理
var root = abs.slice(0, pos + 1);
var xhr = new XMLHttpRequest();
try {
xhr.open('GET', root + 'api/site.php', true);
xhr.timeout = 4000;
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) return;
try {
var d = JSON.parse(xhr.responseText || '{}');
if (d && typeof d.logo === 'string' && d.logo !== '') {
iconEl.href = root + d.logo;
}
} catch (e) { /* 接口异常:保持默认图标 */ }
};
xhr.onerror = function () { /* file:// 或网络异常:保持默认图标 */ };
xhr.send();
} catch (e) { /* 同源限制等:保持默认图标 */ }
}
/* ---------- 自动初始化 ---------- */
document.addEventListener('DOMContentLoaded', function () {
initSiteIcon();
initTheme();
initClocks();
initQuoteRefresh();
initBoardHover();
initSearch();
initIconControls();
initCopyButtons();
bindConfirmForms();
initVisitorInfo();
initToTop();
initFuncBack();
hpShowPageToast();
});
global.HomePage = {
$: $, $$: $$, copyText: copyText, applyTheme: applyTheme, currentTheme: currentTheme,
confirm: siteConfirm, ask: askConfirm, toast: hpToast
};
// 底部脚本执行时 DOM 已解析完毕:在首帧绘制前后尽早恢复滚动位置,避免“先停在顶部再跳回”的大幅跳动
hpRestoreScrollY();
})(window);