- rehydrate.js 新增 boot()/aksrt:ready 事件,支持 boot 后晚注册立即以 init 触发 - aksrt-runtime.js 初始加载改走 AksrtFrontend.boot,tooltip/倒计时注册为组件 - 约 30 个前端脚本(首页、评论、支付、路由、页面叶子、widgets) 从直接 DOMContentLoaded 迁移为 AksrtFrontend.register,并加幂等标记 - section-navs 依赖改为 aksrt-runtime,保证注册时序 - 清理死代码:删除孤儿 loader.js/main.js 及 runtime/page-navs 遗留副本 - loader.js 移除已迁移模块的 tbquire 路径映射,main.js 相应瘦身 - build-public-assets.mjs 增加 runtime/main.min.js 自动压缩步骤 - 新增 4 个契约测试(homepage-dom/rehydrate-runtime/global-dom-init/ public-bundle-parity),3 个既有 harness 适配注册表初始化方式
156 lines
6.0 KiB
JavaScript
156 lines
6.0 KiB
JavaScript
/**
|
|
* AKSRT Effects — count-up / countdown / marquee / 3D tilt / FPS detection
|
|
*
|
|
* 全部渐进增强,尊重 prefers-reduced-motion。
|
|
*
|
|
* @package AKSRT
|
|
*/
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
var reducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
var countUpObserver = null;
|
|
var countdownElements = [];
|
|
var countdownTimer = null;
|
|
var fpsDetectionStarted = false;
|
|
|
|
function scopeFor(root) {
|
|
return root && root.querySelectorAll ? root : document;
|
|
}
|
|
|
|
function eachMatch(root, selector, callback) {
|
|
var scope = scopeFor(root);
|
|
if (scope.nodeType === 1 && scope.matches(selector)) callback(scope);
|
|
scope.querySelectorAll(selector).forEach(callback);
|
|
}
|
|
|
|
function animateCountUp(el) {
|
|
if (el.dataset.aksrtCountupBound === '1') return;
|
|
el.dataset.aksrtCountupBound = '1';
|
|
countUpObserver.observe(el);
|
|
}
|
|
|
|
function initCountUp(root) {
|
|
if (reducedMotion || !('IntersectionObserver' in window)) return;
|
|
if (!countUpObserver) {
|
|
countUpObserver = new IntersectionObserver(function (entries) {
|
|
entries.forEach(function (entry) {
|
|
if (!entry.isIntersecting) return;
|
|
var el = entry.target;
|
|
countUpObserver.unobserve(el);
|
|
var target = parseFloat(el.dataset.target || '0');
|
|
var duration = parseInt(el.dataset.duration || '2000', 10);
|
|
var decimals = (el.dataset.decimals || '0|').split('|');
|
|
var prefix = decimals[1] || '';
|
|
var dec = parseInt(decimals[0], 10) || 0;
|
|
var startTime = null;
|
|
|
|
function step(timestamp) {
|
|
if (!startTime) startTime = timestamp;
|
|
var progress = Math.min((timestamp - startTime) / duration, 1);
|
|
var current = target * (1 - Math.pow(1 - progress, 3));
|
|
el.textContent = prefix + current.toFixed(dec).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
if (progress < 1) requestAnimationFrame(step);
|
|
else el.textContent = prefix + target.toFixed(dec).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
}
|
|
|
|
requestAnimationFrame(step);
|
|
});
|
|
}, { threshold: 0.3 });
|
|
}
|
|
eachMatch(root, '[data-aksrt-countup]', animateCountUp);
|
|
}
|
|
|
|
function pad(n) { return n < 10 ? '0' + n : '' + n; }
|
|
|
|
function updateCountdowns() {
|
|
var now = Date.now();
|
|
countdownElements = countdownElements.filter(function (el) { return document.documentElement.contains(el); });
|
|
countdownElements.forEach(function (el) {
|
|
var end = new Date(el.dataset.end).getTime();
|
|
var diff = end - now;
|
|
if (diff <= 0) {
|
|
el.textContent = el.dataset.expired || '已结束';
|
|
return;
|
|
}
|
|
var d = Math.floor(diff / 86400000);
|
|
var h = Math.floor((diff % 86400000) / 3600000);
|
|
var m = Math.floor((diff % 3600000) / 60000);
|
|
var s = Math.floor((diff % 60000) / 1000);
|
|
var format = el.dataset.format || 'dhms';
|
|
if (format === 'dhms') el.textContent = (d > 0 ? d + '天 ' : '') + pad(h) + ':' + pad(m) + ':' + pad(s);
|
|
else if (format === 'hms') el.textContent = pad(h + d * 24) + ':' + pad(m) + ':' + pad(s);
|
|
else el.textContent = (d > 0 ? d + '天 ' : '') + pad(h) + '时' + pad(m) + '分' + pad(s) + '秒';
|
|
});
|
|
}
|
|
|
|
function initCountdown(root) {
|
|
eachMatch(root, '[data-aksrt-countdown]', function (el) {
|
|
if (el.dataset.aksrtCountdownBound === '1') return;
|
|
el.dataset.aksrtCountdownBound = '1';
|
|
countdownElements.push(el);
|
|
});
|
|
updateCountdowns();
|
|
if (!countdownTimer && countdownElements.length) countdownTimer = window.setInterval(updateCountdowns, 1000);
|
|
}
|
|
|
|
function initTilt(root) {
|
|
if (reducedMotion) return;
|
|
eachMatch(root, '[data-aksrt-tilt]', function (el) {
|
|
if (el.dataset.tiltBound) return;
|
|
el.dataset.tiltBound = '1';
|
|
var maxDeg = parseFloat(el.dataset.tiltMax || '10');
|
|
var perspective = parseFloat(el.dataset.tiltPerspective || '800');
|
|
var scale = parseFloat(el.dataset.tiltScale || '1.03');
|
|
var glare = el.dataset.tiltGlare === '1';
|
|
var glareEl = null;
|
|
if (glare) {
|
|
glareEl = document.createElement('div');
|
|
glareEl.style.cssText = 'position:absolute;inset:0;border-radius:inherit;pointer-events:none;background:linear-gradient(135deg,rgba(255,255,255,0) 0%,rgba(255,255,255,0.25) 100%);opacity:0;transition:opacity 0.2s;';
|
|
el.style.position = el.style.position || 'relative';
|
|
el.style.overflow = 'hidden';
|
|
el.appendChild(glareEl);
|
|
}
|
|
el.addEventListener('mousemove', function (e) {
|
|
var rect = el.getBoundingClientRect();
|
|
var x = (e.clientX - rect.left) / rect.width;
|
|
var y = (e.clientY - rect.top) / rect.height;
|
|
el.style.transform = 'perspective(' + perspective + 'px) rotateX(' + ((0.5 - y) * maxDeg) + 'deg) rotateY(' + ((x - 0.5) * maxDeg) + 'deg) scale3d(' + scale + ',' + scale + ',' + scale + ')';
|
|
if (glareEl) glareEl.style.opacity = '1';
|
|
});
|
|
el.addEventListener('mouseleave', function () {
|
|
el.style.transform = 'perspective(' + perspective + 'px) rotateX(0deg) rotateY(0deg) scale3d(1,1,1)';
|
|
if (glareEl) glareEl.style.opacity = '0';
|
|
});
|
|
});
|
|
}
|
|
|
|
function initFpsDetection(root, reason) {
|
|
if (fpsDetectionStarted || !('requestAnimationFrame' in window)) return;
|
|
fpsDetectionStarted = true;
|
|
var frames = 0;
|
|
var startTime = performance.now();
|
|
function countFrames() {
|
|
frames++;
|
|
var elapsed = performance.now() - startTime;
|
|
if (elapsed >= 1000) {
|
|
if (Math.round(frames * 1000 / elapsed) < 24 && document.body) document.body.classList.add('fps-accelerat');
|
|
return;
|
|
}
|
|
requestAnimationFrame(countFrames);
|
|
}
|
|
requestAnimationFrame(countFrames);
|
|
}
|
|
|
|
function init(root, reason) {
|
|
initCountUp(root);
|
|
initCountdown(root);
|
|
if (typeof initMarquee === 'function') initMarquee(root, reason);
|
|
initTilt(root);
|
|
initFpsDetection(root, reason);
|
|
}
|
|
|
|
window.AksrtFrontend.register('aksrt-effects', init);
|
|
})();
|