- 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 适配注册表初始化方式
258 lines
9.3 KiB
JavaScript
258 lines
9.3 KiB
JavaScript
/**
|
|
* AKSRT Enhancements Part 2 — 高级交互组件
|
|
*
|
|
* 包含:3D 悬停倾斜、滚动触发动画、封面轮播、打赏按钮
|
|
*
|
|
* @package AKSRT
|
|
*/
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
var animateObserver = null;
|
|
var rippleBound = false;
|
|
var stickyScrollBound = false;
|
|
var parallaxScrollBound = false;
|
|
var modalBound = false;
|
|
var stickyElements = [];
|
|
var parallaxElements = [];
|
|
var parallaxTicking = 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 initTilt(root) {
|
|
eachMatch(root, '[data-aksrt-tilt]', function (el) {
|
|
if (el.dataset.tiltInit) return;
|
|
el.dataset.tiltInit = '1';
|
|
|
|
var maxTilt = parseInt(el.dataset.aksrtTiltMax || '15', 10);
|
|
var perspective = parseInt(el.dataset.aksrtTiltPerspective || '1000', 10);
|
|
var scale = parseFloat(el.dataset.aksrtTiltScale || '1.03');
|
|
var speed = parseInt(el.dataset.aksrtTiltSpeed || '400', 10);
|
|
var glare = el.dataset.aksrtTiltGlare === '1';
|
|
|
|
el.style.transformStyle = 'preserve-3d';
|
|
el.style.transition = 'transform ' + speed + 'ms ease';
|
|
|
|
var glareEl = null;
|
|
if (glare) {
|
|
glareEl = document.createElement('div');
|
|
glareEl.style.cssText = 'position:absolute;inset:0;border-radius:inherit;pointer-events:none;opacity:0;transition:opacity ' + speed + 'ms;';
|
|
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;
|
|
var tiltX = (0.5 - y) * maxTilt;
|
|
var tiltY = (x - 0.5) * maxTilt;
|
|
|
|
el.style.transform = 'perspective(' + perspective + 'px) rotateX(' + tiltX + 'deg) rotateY(' + tiltY + 'deg) scale3d(' + scale + ',' + scale + ',' + scale + ')';
|
|
|
|
if (glareEl) {
|
|
glareEl.style.opacity = '1';
|
|
glareEl.style.background = 'linear-gradient(135deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.2) 50%, rgba(255,255,255,0) 100%)';
|
|
glareEl.style.transform = 'translate(' + ((x - 0.5) * 100) + '%, ' + ((y - 0.5) * 100) + '%)';
|
|
}
|
|
});
|
|
|
|
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 prepareAnimation(el) {
|
|
if (el.dataset.aksrtAnimateBound === '1') return;
|
|
el.dataset.aksrtAnimateBound = '1';
|
|
var animation = el.dataset.aksrtAnimate || 'fadeInUp';
|
|
el.style.opacity = '0';
|
|
switch (animation) {
|
|
case 'fadeIn': el.style.transform = 'none'; break;
|
|
case 'fadeInUp': el.style.transform = 'translateY(30px)'; break;
|
|
case 'fadeInDown': el.style.transform = 'translateY(-30px)'; break;
|
|
case 'fadeInLeft': el.style.transform = 'translateX(-30px)'; break;
|
|
case 'fadeInRight': el.style.transform = 'translateX(30px)'; break;
|
|
case 'zoomIn': el.style.transform = 'scale(0.9)'; break;
|
|
default: el.style.transform = 'translateY(30px)';
|
|
}
|
|
animateObserver.observe(el);
|
|
}
|
|
|
|
function initScrollAnimations(root) {
|
|
if (!('IntersectionObserver' in window)) {
|
|
eachMatch(root, '[data-aksrt-animate]', function (el) {
|
|
el.style.opacity = '1';
|
|
el.style.transform = 'none';
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (!animateObserver) {
|
|
animateObserver = new IntersectionObserver(function (entries) {
|
|
entries.forEach(function (entry) {
|
|
if (!entry.isIntersecting) return;
|
|
var el = entry.target;
|
|
var delay = parseInt(el.dataset.aksrtAnimateDelay || '0', 10);
|
|
var duration = parseInt(el.dataset.aksrtAnimateDuration || '600', 10);
|
|
animateObserver.unobserve(el);
|
|
window.setTimeout(function () {
|
|
el.style.transition = 'opacity ' + duration + 'ms ease, transform ' + duration + 'ms ease';
|
|
el.style.opacity = '1';
|
|
el.style.transform = 'translateY(0) translateX(0) scale(1)';
|
|
el.classList.add('aksrt-animated');
|
|
}, delay);
|
|
});
|
|
}, { threshold: 0.1, rootMargin: '0px 0px -50px 0px' });
|
|
}
|
|
|
|
eachMatch(root, '[data-aksrt-animate]', prepareAnimation);
|
|
}
|
|
|
|
function updateSticky() {
|
|
stickyElements = stickyElements.filter(function (el) { return document.documentElement.contains(el); });
|
|
stickyElements.forEach(function (el) {
|
|
var top = parseInt(el.dataset.aksrtStickyTop || '80', 10);
|
|
var parent = el.parentElement;
|
|
if (!parent) return;
|
|
var parentRect = parent.getBoundingClientRect();
|
|
if (parentRect.top < top && parentRect.bottom > el.offsetHeight + top) {
|
|
el.style.position = 'sticky';
|
|
el.style.top = top + 'px';
|
|
}
|
|
});
|
|
}
|
|
|
|
function initSticky(root) {
|
|
eachMatch(root, '[data-aksrt-sticky]', function (el) {
|
|
if (el.dataset.aksrtStickyBound === '1') return;
|
|
el.dataset.aksrtStickyBound = '1';
|
|
stickyElements.push(el);
|
|
});
|
|
if (!stickyScrollBound) {
|
|
stickyScrollBound = true;
|
|
window.addEventListener('scroll', updateSticky, { passive: true });
|
|
}
|
|
updateSticky();
|
|
}
|
|
|
|
function updateParallax() {
|
|
parallaxElements = parallaxElements.filter(function (el) { return document.documentElement.contains(el); });
|
|
parallaxElements.forEach(function (el) {
|
|
var speed = parseFloat(el.dataset.aksrtParallax || '0.3');
|
|
var rect = el.getBoundingClientRect();
|
|
var center = rect.top + rect.height / 2;
|
|
var offset = (center - window.innerHeight / 2) * speed;
|
|
el.style.transform = 'translateY(' + offset + 'px)';
|
|
});
|
|
parallaxTicking = false;
|
|
}
|
|
|
|
function initParallax(root) {
|
|
eachMatch(root, '[data-aksrt-parallax]', function (el) {
|
|
if (el.dataset.aksrtParallaxBound === '1') return;
|
|
el.dataset.aksrtParallaxBound = '1';
|
|
parallaxElements.push(el);
|
|
});
|
|
if (!parallaxScrollBound) {
|
|
parallaxScrollBound = true;
|
|
window.addEventListener('scroll', function () {
|
|
if (parallaxTicking) return;
|
|
parallaxTicking = true;
|
|
requestAnimationFrame(updateParallax);
|
|
}, { passive: true });
|
|
}
|
|
updateParallax();
|
|
}
|
|
|
|
function initRipple(root, reason) {
|
|
if (rippleBound) return;
|
|
rippleBound = true;
|
|
document.addEventListener('click', function (e) {
|
|
var el = e.target.closest('.aksrt-ripple');
|
|
if (!el) return;
|
|
var rect = el.getBoundingClientRect();
|
|
var size = Math.max(rect.width, rect.height) * 2;
|
|
var wave = document.createElement('span');
|
|
wave.className = 'aksrt-ripple__wave';
|
|
wave.style.width = wave.style.height = size + 'px';
|
|
wave.style.left = (e.clientX - rect.left - size / 2) + 'px';
|
|
wave.style.top = (e.clientY - rect.top - size / 2) + 'px';
|
|
el.appendChild(wave);
|
|
wave.addEventListener('animationend', function () { wave.remove(); });
|
|
});
|
|
}
|
|
|
|
function initFloatLabels(root) {
|
|
eachMatch(root, '.aksrt-float-label__input', function (input) {
|
|
if (input.value) input.setAttribute('data-has-value', '1');
|
|
if (input.dataset.aksrtFloatLabelBound === '1') return;
|
|
input.dataset.aksrtFloatLabelBound = '1';
|
|
input.addEventListener('input', function () {
|
|
if (this.value) this.setAttribute('data-has-value', '1');
|
|
else this.removeAttribute('data-has-value');
|
|
});
|
|
});
|
|
}
|
|
|
|
function initSigninCalendar(root) {
|
|
eachMatch(root, '[data-aksrt-signin-calendar]', function (calendar) {
|
|
if (calendar.dataset.aksrtSigninCalendarBound === '1') return;
|
|
calendar.dataset.aksrtSigninCalendarBound = '1';
|
|
var data = [];
|
|
try { data = JSON.parse(calendar.dataset.aksrtSigninCalendar || '[]'); } catch (e) {}
|
|
var today = new Date();
|
|
var html = '<div class="aksrt-signin-calendar__grid">';
|
|
for (var i = 29; i >= 0; i--) {
|
|
var d = new Date(today);
|
|
d.setDate(d.getDate() - i);
|
|
var dateStr = d.toISOString().split('T')[0];
|
|
var isSigned = data.indexOf(dateStr) !== -1;
|
|
html += '<div class="aksrt-signin-calendar__day' + (isSigned ? ' is-signed' : '') + (i === 0 ? ' is-today' : '') +
|
|
'" title="' + dateStr + (isSigned ? ' ✓' : '') + '"><span>' + d.getDate() + '</span></div>';
|
|
}
|
|
calendar.innerHTML = html + '</div>';
|
|
});
|
|
}
|
|
|
|
function bindModalLifecycle() {
|
|
if (modalBound) return;
|
|
modalBound = true;
|
|
document.addEventListener('aksrt:modal:open', function (event) {
|
|
var root = event.target && event.target.querySelectorAll ? event.target : document;
|
|
window.setTimeout(function () { initSigninCalendar(root); }, 30);
|
|
});
|
|
}
|
|
|
|
function init(root, reason) {
|
|
initTilt(root);
|
|
initScrollAnimations(root);
|
|
initRipple(root, reason);
|
|
initSticky(root);
|
|
initParallax(root);
|
|
initFloatLabels(root);
|
|
initSigninCalendar(root);
|
|
bindModalLifecycle();
|
|
}
|
|
|
|
window.AksrtFrontend.register('aksrt-enhancements-advanced', init);
|
|
|
|
window.AksrtEnhancements = {
|
|
initTilt: initTilt,
|
|
initScrollAnimations: initScrollAnimations,
|
|
initRipple: initRipple,
|
|
};
|
|
})();
|