NewHome/assets/js/common.js
2026-09-14 22:06:21 +08:00

1034 lines
48 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 —— 全站通用交互
* 功能:昼/夜主题切换、实时时钟、随机名言刷新、返回顶部、
* 搜索栏(引擎下拉/清除/回车搜索、复制到剪贴板、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, '&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 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 = '<div class="err-msg">加载失败(可能登录状态已失效),请刷新页面后重试。</div>';
}
};
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 跟随后台站点 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) { /* 同源限制等:保持默认图标 */ }
}
/* ---------- 首页顶部组件:红队 / 蓝队渗透测试流程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);