/* common.js —— 全站通用交互
* 功能:昼/夜主题切换、实时时钟、随机名言刷新、返回顶部、
* 搜索栏(引擎下拉/清除/回车搜索)、复制到剪贴板、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) {
var swapping = false;
quote.addEventListener('click', function () {
if (swapping) return;
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;
}
// 先淡出、换字后再淡入,避免文字瞬间跳变
swapping = true;
quote.classList.add('quote-fade');
setTimeout(function () {
quote.textContent = list[idx].textContent;
quote.classList.remove('quote-fade');
swapping = false;
}, 140);
});
});
}
/* ---------- 搜索栏 ---------- */
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, '"');
}
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 =
'
' +
'
' + escHtml(opts.title || '操作确认') + '
' +
'
' + escHtml(message || '') + '
' +
'
';
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 doc = document.scrollingElement || document.documentElement;
var containers = [];
var ticking = false;
function currentY() {
var y = window.pageYOffset || (doc && doc.scrollTop) || 0;
for (var i = 0; i < containers.length; i++) {
var t = containers[i].scrollTop || 0;
if (t > y) y = t;
}
return y;
}
function apply() {
ticking = false;
btn.classList.toggle('show', currentY() > 180);
}
function schedule() {
if (ticking) return;
ticking = true;
if (window.requestAnimationFrame) window.requestAnimationFrame(apply);
else setTimeout(apply, 30);
}
// 捕获阶段监听:主文档与任意内滚容器的滚动都能收到,避免漏触发
document.addEventListener('scroll', schedule, true);
window.addEventListener('resize', schedule, { passive: true });
// 采集内滚容器:DOM 就绪与资源加载后各测一次(布局完成后才能正确判断是否溢出)
function collect() {
var list = document.querySelectorAll('main, section, article, div, ul, ol');
Array.prototype.forEach.call(list, function (el) {
if (el === btn || containers.indexOf(el) >= 0) return;
if (el.closest('.admin-side, .hp-toasts, .art-topbar')) return; // 侧栏/弹窗/顶栏自身滚动不应触发
if (el.scrollHeight <= el.clientHeight + 40) return;
var cs;
try { cs = window.getComputedStyle(el); } catch (e) { return; }
if (cs.position === 'fixed' || cs.position === 'sticky') return;
if (cs.overflowY === 'auto' || cs.overflowY === 'scroll') containers.push(el);
});
apply();
}
collect();
if (document.readyState !== 'complete') window.addEventListener('load', collect);
btn.addEventListener('click', function () {
btn.classList.remove('show'); // 乐观隐藏,避免到顶后仍残留
for (var i = 0; i < containers.length; i++) {
if (containers[i].scrollTop > 0) containers[i].scrollTop = 0;
}
if (doc && doc.scrollTop > 0) {
try { doc.scrollTo({ top: 0, behavior: 'smooth' }); } catch (e) { doc.scrollTop = 0; }
} else {
try { window.scrollTo({ top: 0, behavior: 'smooth' }); } catch (e) { window.scrollTo(0, 0); }
}
schedule();
});
schedule();
}
/* ---------- 后台侧边栏:收缩 / 移动端抽屉 / 本页功能锚点 ---------- */
function initAdminSide() {
var side = document.querySelector('.admin-side');
if (!side) return;
var body = document.body;
var mq = window.matchMedia ? window.matchMedia('(max-width: 900px)') : null;
function isMobile() { return mq ? mq.matches : window.innerWidth <= 900; }
// 收缩(桌面)/ 抽屉开关(小屏)
Array.prototype.forEach.call(document.querySelectorAll('.js-admin-side-toggle'), function (btn) {
btn.addEventListener('click', function () {
if (isMobile()) {
body.classList.toggle('hp-side-open');
} else {
var collapsed = body.classList.toggle('hp-side-collapsed');
try { localStorage.setItem('hp-admin-side-collapsed', collapsed ? '1' : '0'); } catch (e) { /* ignore */ }
}
});
});
var mask = document.getElementById('adminSideMask');
if (mask) mask.addEventListener('click', function () { body.classList.remove('hp-side-open'); });
window.addEventListener('resize', function () { if (!isMobile()) body.classList.remove('hp-side-open'); });
Array.prototype.forEach.call(side.querySelectorAll('a'), function (a) {
a.addEventListener('click', function () { if (isMobile()) body.classList.remove('hp-side-open'); });
});
// 采集当前页“本页功能”:fieldset-card 的标题
var secWrap = document.getElementById('adminSideSections');
var secList = document.getElementById('adminSideSectionList');
if (!secWrap || !secList) return;
var items = [];
Array.prototype.forEach.call(document.querySelectorAll('.fieldset-card'), function (card) {
var t = card.querySelector('.fs-title');
if (!t) return;
var clone = t.cloneNode(true);
Array.prototype.forEach.call(clone.querySelectorAll('.tip'), function (n) { if (n.parentNode) n.parentNode.removeChild(n); });
var text = (clone.textContent || '').replace(/\s+/g, ' ').trim();
if (text === '') return;
items.push({ el: card, text: text });
});
if (items.length < 1) return;
var idx = 0;
items.forEach(function (it) {
idx++;
if (!it.el.id) it.el.id = 'hp-sec-' + idx;
var a = document.createElement('a');
a.className = 'as-sub';
a.href = '#' + it.el.id;
a.textContent = it.text;
a.title = it.text;
a.addEventListener('click', function (ev) {
ev.preventDefault();
try { it.el.scrollIntoView({ behavior: 'smooth', block: 'start' }); }
catch (e) { it.el.scrollIntoView(); }
});
secList.appendChild(a);
});
secWrap.hidden = false;
// 高亮当前可视区块
var subs = secList.querySelectorAll('.as-sub');
function spy() {
var best = -1, bestTop = -Infinity;
for (var i = 0; i < items.length; i++) {
var top = items[i].el.getBoundingClientRect().top - 120;
if (top <= 0 && top > bestTop) { bestTop = top; best = i; }
}
for (var j = 0; j < subs.length; j++) subs[j].classList.toggle('active', j === best);
}
var st = false;
function onScroll() {
if (st) return;
st = true;
if (window.requestAnimationFrame) window.requestAnimationFrame(function () { st = false; spy(); });
else { st = false; spy(); }
}
document.addEventListener('scroll', onScroll, true);
window.addEventListener('resize', onScroll);
spy();
}
/* ---------- 登录页:采集设备信息(系统 / 机型 / 屏幕等,能拿到什么就带什么)随表单一并提交 ---------- */
function initLoginDevice() {
var field = document.getElementById('devField');
if (!field) return;
function baseInfo() {
var info = {};
try {
info.ua = navigator.userAgent || '';
info.platform = navigator.platform || '';
info.lang = navigator.language || '';
info.screen = (window.screen && screen.width && screen.height) ? (screen.width + '×' + screen.height + '@' + (window.devicePixelRatio || 1)) : '';
info.touch = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
} catch (e) { /* ignore */ }
return info;
}
function fill() {
try { field.value = JSON.stringify(baseInfo()); } catch (e) { /* ignore */ }
}
fill();
// Chromium 系 UA-CH:可拿到机型(model)、系统版本、架构等高熵值
try {
if (navigator.userAgentData && navigator.userAgentData.getHighEntropyValues) {
navigator.userAgentData.getHighEntropyValues(['platform', 'platformVersion', 'architecture', 'model', 'uaFullVersion'])
.then(function (h) {
var info = baseInfo();
info.uch = h || {};
field.value = JSON.stringify(info);
})
.catch(function () { /* ignore */ });
}
} catch (e) { /* ignore */ }
}
/* ---------- 风控管理页:筛选 / 翻页局部刷新(不整页跳转、不回到顶部) ---------- */
function initVisitsAjax() {
var boxes = {
access: document.getElementById('visResults'),
loginfail: document.getElementById('lfResults')
};
if (!boxes.access && !boxes.loginfail) return;
function serialize(form) {
var parts = [];
var els = form.elements;
for (var i = 0; i < els.length; i++) {
var el = els[i];
if (!el.name || el.disabled) continue;
var t = (el.type || '').toLowerCase();
if ((t === 'checkbox' || t === 'radio') && !el.checked) continue;
parts.push(encodeURIComponent(el.name) + '=' + encodeURIComponent(el.value));
}
return parts.join('&');
}
function load(section, query, pushUrl) {
var box = boxes[section];
if (!box) return;
box.classList.add('vis-loading');
var xhr = new XMLHttpRequest();
try {
xhr.open('GET', 'visits_data.php?section=' + encodeURIComponent(section) + (query ? ('&' + query) : ''), true);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) return;
box.classList.remove('vis-loading');
if (xhr.status === 200) {
box.innerHTML = xhr.responseText;
if (pushUrl && history.replaceState) {
history.replaceState(null, '', 'visits.php' + (query ? ('?' + query) : ''));
}
} else {
box.innerHTML = '加载失败(可能登录状态已失效),请刷新页面后重试。
';
}
};
xhr.send();
} catch (e) {
box.classList.remove('vis-loading');
}
}
// 筛选表单(事件委托,局部刷新后依然有效)
document.addEventListener('submit', function (ev) {
var form = ev.target;
if (!form || !form.getAttribute || !form.hasAttribute('data-vis-form')) return;
if (!/get/i.test(form.method || '')) return;
ev.preventDefault();
load(form.getAttribute('data-vis-form'), serialize(form), true);
}, true);
// 分页链接(委托到 document,避开局部刷新后监听丢失)
document.addEventListener('click', function (ev) {
var a = (ev.target && ev.target.closest) ? ev.target.closest('a[data-vis-page]') : null;
if (!a) return;
ev.preventDefault();
load(a.getAttribute('data-vis-section') || 'access', a.getAttribute('data-vis-page') || '', true);
}, true);
}
/* ---------- 功能区静态工具页:库卡片 ?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 跟随后台站点 logo(api/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) { /* 同源限制等:保持默认图标 */ }
}
/* ---------- 首页顶部组件:红队 / 蓝队渗透测试流程(hero_mode=red|blue)动态步进 ---------- */
function initHeroFlows() {
var boxes = $$('.js-ptflow');
if (!boxes.length) return;
boxes.forEach(function (box) {
var steps = $$('.ptflow-step', box);
if (!steps.length) return;
var phaseEl = $('.js-ptflow-phase', box);
var idx = 0;
function paint() {
steps.forEach(function (s, i) {
s.classList.toggle('active', i === idx);
s.classList.toggle('done', i < idx);
});
if (phaseEl) {
var cur = steps[idx];
var name = cur ? (cur.getAttribute('data-phase') || '') : '';
phaseEl.textContent = '当前阶段:' + name + '(' + (idx + 1) + '/' + steps.length + ')';
}
idx = (idx + 1) % steps.length;
}
paint();
setInterval(paint, 1800);
});
}
/* ---------- 首页顶部组件:安全态势数字增长(hero_mode=posture) ---------- */
function initHeroCountUp() {
var els = $$('.js-count');
if (!els.length) return;
els.forEach(function (el) {
var to = parseInt(el.getAttribute('data-to') || '0', 10);
if (isNaN(to) || to <= 0) { el.textContent = '0'; return; }
var dur = 900, t0 = null;
function step(now) {
if (t0 === null) t0 = now;
var p = Math.min(1, (now - t0) / dur);
var eased = 1 - Math.pow(1 - p, 3);
if (p < 1) { el.textContent = String(Math.round(to * eased)); requestAnimationFrame(step); }
else { el.textContent = String(to); }
}
requestAnimationFrame(step);
});
}
/* ---------- 农历 / 节日库(1900-2100,纯前端本地计算,不依赖任何第三方接口) ---------- */
var LUNAR_INFO = [
0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2,
0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977,
0x04970, 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970,
0x06566, 0x0d4a0, 0x0ea50, 0x06e95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950,
0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2, 0x0a950, 0x0b557,
0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5b0, 0x14573, 0x052b0, 0x0a9a8, 0x0e950, 0x06aa0,
0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0,
0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b6a0, 0x195a6,
0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570,
0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x055c0, 0x0ab60, 0x096d5, 0x092e0,
0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5,
0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930,
0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530,
0x05aa0, 0x076a3, 0x096d0, 0x04afb, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45,
0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0,
0x14b63, 0x09370, 0x049f8, 0x04970, 0x064b0, 0x168a6, 0x0ea50, 0x06b20, 0x1a6c4, 0x0aae0,
0x0a2e0, 0x0d2e3, 0x0c960, 0x0d557, 0x0d4a0, 0x0da50, 0x05d55, 0x056a0, 0x0a6d0, 0x055d4,
0x052d0, 0x0a9b8, 0x0a950, 0x0b4a0, 0x0b6a6, 0x0ad50, 0x055a0, 0x0aba4, 0x0a5b0, 0x052b0,
0x0b273, 0x06930, 0x07337, 0x06aa0, 0x0ad50, 0x14b55, 0x04b60, 0x0a570, 0x054e4, 0x0d160,
0x0e968, 0x0d520, 0x0daa0, 0x16aa6, 0x056d0, 0x04ae0, 0x0a9d4, 0x0a2d0, 0x0d150, 0x0f252,
0x0d520
];
var LUNAR_GAN = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
var LUNAR_ZHI = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
var LUNAR_MON = ['正', '二', '三', '四', '五', '六', '七', '八', '九', '十', '冬', '腊'];
var LUNAR_D1 = ['日', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十'];
var LUNAR_D2 = ['初', '十', '廿', '三'];
var LUNAR_FEST = [
[1, 1, '春节'], [1, 15, '元宵节'], [2, 2, '龙抬头'], [5, 5, '端午节'], [7, 7, '七夕'],
[7, 15, '中元节'], [8, 15, '中秋节'], [9, 9, '重阳节'], [12, 8, '腊八节'], [12, 23, '小年']
];
var SOLAR_FEST = {
'1-1': '元旦', '2-14': '情人节', '3-8': '妇女节', '3-12': '植树节', '4-1': '愚人节',
'5-1': '劳动节', '5-4': '青年节', '6-1': '儿童节', '7-1': '建党节', '8-1': '建军节',
'9-10': '教师节', '10-1': '国庆节', '12-25': '圣诞节'
};
function lunLeapMonth(y) { return LUNAR_INFO[y - 1900] & 0xf; }
function lunLeapDays(y) { return lunLeapMonth(y) ? ((LUNAR_INFO[y - 1900] & 0x10000) ? 30 : 29) : 0; }
function lunMonthDays(y, m) { return (LUNAR_INFO[y - 1900] & (0x10000 >> m)) ? 30 : 29; }
function lunYearDays(y) {
var i, sum = 348;
for (i = 0x8000; i > 0x8; i >>= 1) { sum += (LUNAR_INFO[y - 1900] & i) ? 1 : 0; }
return sum + lunLeapDays(y);
}
function solar2lunar(date) {
var offset = (Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) - Date.UTC(1900, 0, 31)) / 86400000;
var i, temp = 0;
for (i = 1900; i < 2101 && offset > 0; i++) { temp = lunYearDays(i); offset -= temp; }
if (offset < 0) { offset += temp; i--; }
var year = i, leap = lunLeapMonth(i), isLeap = false;
for (i = 1; i < 13 && offset > 0; i++) {
if (leap > 0 && i === (leap + 1) && isLeap === false) { --i; isLeap = true; temp = lunLeapDays(year); }
else { temp = lunMonthDays(year, i); }
if (isLeap === true && i === (leap + 1)) { isLeap = false; }
offset -= temp;
}
if (offset === 0 && leap > 0 && i === leap + 1) {
if (isLeap) { isLeap = false; } else { isLeap = true; --i; }
}
if (offset < 0) { offset += temp; --i; }
return { year: year, month: i, day: offset + 1, isLeap: isLeap };
}
function lunDayName(d) {
if (d === 10) { return '初十'; }
if (d === 20) { return '二十'; }
if (d === 30) { return '三十'; }
return LUNAR_D2[Math.floor(d / 10)] + LUNAR_D1[d % 10];
}
function ganzhiYear(y) { return LUNAR_GAN[(y - 4) % 10] + LUNAR_ZHI[(y - 4) % 12]; }
function lunMonthName(m, isLeap) { return (isLeap ? '闰' : '') + (LUNAR_MON[m - 1] || m) + '月'; }
function festivalsOf(date) {
var out = [];
var lu = solar2lunar(date);
for (var i = 0; i < LUNAR_FEST.length; i++) {
if (!lu.isLeap && lu.month === LUNAR_FEST[i][0] && lu.day === LUNAR_FEST[i][1]) { out.push(LUNAR_FEST[i][2]); }
}
if (!lu.isLeap && lu.month === 12 && lu.day === lunMonthDays(lu.year, 12)) { out.push('除夕'); }
var key = (date.getMonth() + 1) + '-' + date.getDate();
if (SOLAR_FEST[key]) { out.push(SOLAR_FEST[key]); }
var w = date.getDay(), mo = date.getMonth() + 1, dd = date.getDate();
if (mo === 5 && w === 0 && dd >= 8 && dd <= 14) { out.push('母亲节'); }
if (mo === 6 && w === 0 && dd >= 15 && dd <= 21) { out.push('父亲节'); }
return out;
}
function lunarText(date) {
var lu = solar2lunar(date);
return '农历 ' + ganzhiYear(lu.year) + '年 ' + lunMonthName(lu.month, lu.isLeap) + lunDayName(lu.day);
}
/* ---------- 首页顶部组件:大字时钟(hero_mode=clock,含农历/节日) ---------- */
function initHeroClock() {
var boxes = $$('.js-hero-clock');
if (!boxes.length) return;
var WK = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
function pad(n) { return n < 10 ? '0' + n : '' + n; }
function greet(h) {
if (h < 6) return '凌晨好';
if (h < 9) return '早上好';
if (h < 12) return '上午好';
if (h < 14) return '中午好';
if (h < 18) return '下午好';
if (h < 23) return '晚上好';
return '夜深了';
}
var cacheDay = '', lunarStr = '', festStr = '';
var nextFestName = '', nextFestTs = 0;
/* 下一个节日:从明天起向后找(含当天已过的),最多一年 */
function findNextFest(now) {
var y = now.getFullYear(), m = now.getMonth(), d = now.getDate();
for (var i = 1; i <= 370; i++) {
var dt = new Date(y, m, d + i);
var f = festivalsOf(dt);
if (f.length) {
return { name: f[0], ts: new Date(dt.getFullYear(), dt.getMonth(), dt.getDate(), 0, 0, 0).getTime() };
}
}
return null;
}
function pad2(n) { return n < 10 ? '0' + n : '' + n; }
function fmtCountdown(ms) {
if (ms < 0) ms = 0;
var total = Math.floor(ms / 1000);
var days = Math.floor(total / 86400);
var hh = Math.floor((total % 86400) / 3600);
var mm = Math.floor((total % 3600) / 60);
var ss = total % 60;
var t = pad2(hh) + ':' + pad2(mm) + ':' + pad2(ss);
return days > 0 ? (days + '天 ' + t) : t;
}
/* 每日只需重算一次(农历 / 今日节日 / 下一个节日) */
function refreshDaily(now) {
var key = now.getFullYear() + '-' + (now.getMonth() + 1) + '-' + now.getDate();
if (key === cacheDay) return;
cacheDay = key;
lunarStr = lunarText(now);
var todayFest = festivalsOf(now);
festStr = todayFest.length ? todayFest.join(' · ') : '';
var nf = findNextFest(now);
nextFestName = nf ? nf.name : '';
nextFestTs = nf ? nf.ts : 0;
}
function tick() {
var d = new Date();
refreshDaily(d);
var timeStr = pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());
var dateStr = d.getFullYear() + '年' + pad(d.getMonth() + 1) + '月' + pad(d.getDate()) + '日';
var wk = WK[d.getDay()];
var gr = greet(d.getHours());
var left = nextFestTs ? fmtCountdown(nextFestTs - d.getTime()) : '--';
boxes.forEach(function (box) {
var te = $('.js-ch-time', box), de = $('.js-ch-date', box),
we = $('.js-ch-week', box), ge = $('.js-ch-greet', box),
le = $('.js-ch-lunar', box), fe = $('.js-ch-fest', box),
cl = $('.js-cc-label', box), ct = $('.js-cc-time', box);
if (te) te.textContent = timeStr;
if (de) de.textContent = dateStr;
if (we) we.textContent = wk;
if (ge) ge.textContent = gr;
if (le) le.textContent = lunarStr;
if (fe) {
fe.textContent = festStr;
fe.className = 'ch-fest js-ch-fest' + (festStr ? ' on' : '');
}
if (cl) cl.textContent = nextFestName ? ('距 ' + nextFestName) : '下一个节日';
if (ct) ct.textContent = left;
});
}
tick();
setInterval(tick, 1000);
}
/* ---------- 首页顶部组件:公告滚动条(hero_mode=notice) ---------- */
function initNoticeTicker() {
var track = $('.js-tk-track');
if (!track) return;
var viewport = $('.js-tk-viewport');
if (!$$('.tk-item', track).length) return;
var oneSet = track.innerHTML;
var targetW = viewport ? viewport.clientWidth : 0;
var guard = 0;
while (track.scrollWidth < targetW && guard < 20) {
track.innerHTML += oneSet;
guard++;
}
track.innerHTML += track.innerHTML; /* 再补一组,配合 -50% 位移无缝循环 */
var half = track.scrollWidth / 2;
var dur = Math.max(12, half / 60); /* 约 60px/s */
track.style.animation = 'tkScroll ' + dur.toFixed(1) + 's linear infinite';
}
/* ---------- 首页顶部组件:打字机标语(hero_mode=slogan) ---------- */
function initHeroSlogan() {
var out = $('.js-sg-text');
if (!out) return;
var pool = $$('.js-sg-pool span');
var list = [];
for (var i = 0; i < pool.length; i++) {
var t = (pool[i].textContent || '').trim();
if (t) list.push(t);
}
if (!list.length) list = ['知识就是力量。'];
var idx = 0, pos = 0, deleting = false;
function run() {
var full = list[idx];
if (!deleting) {
pos++;
out.textContent = full.slice(0, pos);
if (pos >= full.length) { deleting = true; setTimeout(run, 1800); return; }
setTimeout(run, 90);
} else {
pos--;
out.textContent = full.slice(0, pos);
if (pos <= 0) { deleting = false; idx = (idx + 1) % list.length; setTimeout(run, 360); return; }
setTimeout(run, 38);
}
}
run();
}
/* ---------- 自动初始化 ---------- */
document.addEventListener('DOMContentLoaded', function () {
initSiteIcon();
initTheme();
initClocks();
initQuoteRefresh();
initSearch();
initIconControls();
initCopyButtons();
bindConfirmForms();
initVisitorInfo();
initHeroFlows();
initHeroCountUp();
initHeroClock();
initNoticeTicker();
initHeroSlogan();
initLoginDevice();
initVisitsAjax();
initAdminSide();
initToTop();
initFuncBack();
hpShowPageToast();
});
global.HomePage = {
$: $, $$: $$, copyText: copyText, applyTheme: applyTheme, currentTheme: currentTheme,
confirm: siteConfirm, ask: askConfirm, toast: hpToast
};
// 底部脚本执行时 DOM 已解析完毕:在首帧绘制前后尽早恢复滚动位置,避免“先停在顶部再跳回”的大幅跳动
hpRestoreScrollY();
})(window);