feat(runtime): 全站前端脚本统一接入 AksrtFrontend 全局 DOM 生命周期

- 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 适配注册表初始化方式
This commit is contained in:
kate522 committed 2026-09-02 21:48:56 +08:00
1 parent 5984d4e42d
commit 1f4a39d8f8
60 files changed
+1810 -1742

No files matched your search

+164 -194
View File
@@ -9,11 +9,27 @@
(function () {
'use strict';
/* ============================================================
* 1. 3D 悬停倾斜效果 (vanilla-tilt 轻量版)
* ============================================================ */
function initTilt() {
document.querySelectorAll('[data-aksrt-tilt]').forEach(function (el) {
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';
@@ -58,230 +74,184 @@
});
}
/* ============================================================
* 2. 滚动触发动画 (IntersectionObserver)
* ============================================================ */
function initScrollAnimations() {
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)) {
// 不支持时直接显示所有元素
document.querySelectorAll('[data-aksrt-animate]').forEach(function (el) {
eachMatch(root, '[data-aksrt-animate]', function (el) {
el.style.opacity = '1';
el.style.transform = 'none';
});
return;
}
var observer = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (!entry.isIntersecting) return;
var el = entry.target;
var animation = el.dataset.aksrtAnimate || 'fadeInUp';
var delay = parseInt(el.dataset.aksrtAnimateDelay || '0', 10);
var duration = parseInt(el.dataset.aksrtAnimateDuration || '600', 10);
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);
observer.unobserve(el);
});
}, {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px',
});
document.querySelectorAll('[data-aksrt-animate]').forEach(function (el) {
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)';
}
observer.observe(el);
});
}
/* ============================================================
* 5. 粘性元素 (Sticky)
* ============================================================ */
function initSticky() {
document.querySelectorAll('[data-aksrt-sticky]').forEach(function (el) {
var top = parseInt(el.dataset.aksrtStickyTop || '80', 10);
var offset = parseInt(el.dataset.aksrtStickyOffset || '0', 10);
function update() {
var rect = el.getBoundingClientRect();
var parentRect = el.parentElement.getBoundingClientRect();
if (parentRect.top < top && parentRect.bottom > el.offsetHeight + top) {
el.style.position = 'sticky';
el.style.top = top + 'px';
}
}
window.addEventListener('scroll', update, { passive: true });
});
}
/* ============================================================
* 6. 视差滚动
* ============================================================ */
function initParallax() {
var elements = document.querySelectorAll('[data-aksrt-parallax]');
if (elements.length === 0) return;
var ticking = false;
function update() {
var scrollTop = window.pageYOffset;
elements.forEach(function (el) {
var speed = parseFloat(el.dataset.aksrtParallax || '0.3');
var rect = el.getBoundingClientRect();
var center = rect.top + rect.height / 2;
var windowCenter = window.innerHeight / 2;
var offset = (center - windowCenter) * speed;
el.style.transform = 'translateY(' + offset + 'px)';
});
ticking = false;
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' });
}
window.addEventListener('scroll', function () {
if (!ticking) {
requestAnimationFrame(update);
ticking = true;
}
}, { passive: true });
eachMatch(root, '[data-aksrt-animate]', prepareAnimation);
}
/* ============================================================
* 7. 涟漪点击效果
* ============================================================ */
function initRipple() {
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 x = e.clientX - rect.left;
var y = e.clientY - rect.top;
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 = (x - size / 2) + 'px';
wave.style.top = (y - size / 2) + '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();
});
wave.addEventListener('animationend', function () { wave.remove(); });
});
}
/* ============================================================
* 8. 表单浮动标签自动初始化
* ============================================================ */
function initFloatLabels() {
document.querySelectorAll('.aksrt-float-label__input').forEach(function (input) {
// 检查初始值
if (input.value) {
input.setAttribute('data-has-value', '1');
}
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');
}
if (this.value) this.setAttribute('data-has-value', '1');
else this.removeAttribute('data-has-value');
});
});
}
/* ============================================================
* 8. 签到日历可视化
* ============================================================ */
function initSigninCalendar() {
var calendar = document.querySelector('[data-aksrt-signin-calendar]');
if (!calendar) return;
var data = [];
try {
data = JSON.parse(calendar.dataset.aksrtSigninCalendar || '[]');
} catch (e) {}
// 生成最近 30 天的格子
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;
var isToday = i === 0;
html += '<div class="aksrt-signin-calendar__day' +
(isSigned ? ' is-signed' : '') +
(isToday ? ' is-today' : '') +
'" title="' + dateStr + (isSigned ? ' ✓' : '') + '">' +
'<span>' + d.getDate() + '</span>' +
'</div>';
}
html += '</div>';
calendar.innerHTML = html;
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>';
});
}
/* ============================================================
* 初始化入口
* ============================================================ */
document.addEventListener('DOMContentLoaded', function () {
initTilt();
initScrollAnimations();
initRipple();
initSticky();
initParallax();
initFloatLabels();
initSigninCalendar();
document.addEventListener('aksrt:modal:open', function () {
// Remote modal may inject calendar markup after open.
window.setTimeout(initSigninCalendar, 30);
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,
};
})();