401 lines
18 KiB
JavaScript
401 lines
18 KiB
JavaScript
/* 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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||
}
|
||
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(); });
|
||
});
|
||
}
|
||
|
||
/* ---------- 返回顶部(右下角按钮,滚动后出现;兼容内滚容器页面) ---------- */
|
||
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();
|
||
}
|
||
|
||
/* ---------- 自动初始化 ---------- */
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
initTheme();
|
||
initClocks();
|
||
initQuoteRefresh();
|
||
initBoardHover();
|
||
initSearch();
|
||
initIconControls();
|
||
initCopyButtons();
|
||
bindConfirmForms();
|
||
initToTop();
|
||
});
|
||
|
||
global.HomePage = {
|
||
$: $, $$: $$, copyText: copyText, applyTheme: applyTheme, currentTheme: currentTheme,
|
||
confirm: siteConfirm, ask: askConfirm
|
||
};
|
||
})(window);
|