- Rebuild checkin details modal HTML 1:1 with reference theme: colorful header c-blue, user card (points/level), daily reward card and 7-day continuous mini cards - Signed entry buttons (header dropdown, user-card badge, user center) become clickable checkin-details-link modal triggers instead of disabled buttons - Swap data-signin-btn to details link in place after successful signin (reference .initiate-checkin replacement behavior) - Add aksrt-checkin-details-modal runtime shell (modal-mini 400px) - Remove dead code: legacy lite modal, admin-ajax details handler, /signin/history route, signin calendar; rebuild public bundles - Update contract tests to the new checkin modal contract
273 lines
8.9 KiB
JavaScript
273 lines
8.9 KiB
JavaScript
/**
|
|
* AKSRT Enhancements Part 2 — 高级交互组件
|
|
*
|
|
* 包含:3D 悬停倾斜、滚动触发动画、封面轮播、打赏按钮
|
|
*
|
|
* @package AKSRT
|
|
*/
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
/* ============================================================
|
|
* 1. 3D 悬停倾斜效果 (vanilla-tilt 轻量版)
|
|
* ============================================================ */
|
|
function initTilt() {
|
|
document.querySelectorAll('[data-aksrt-tilt]').forEach(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';
|
|
});
|
|
});
|
|
}
|
|
|
|
/* ============================================================
|
|
* 2. 滚动触发动画 (IntersectionObserver)
|
|
* ============================================================ */
|
|
function initScrollAnimations() {
|
|
if (!('IntersectionObserver' in window)) {
|
|
// 不支持时直接显示所有元素
|
|
document.querySelectorAll('[data-aksrt-animate]').forEach(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) {
|
|
if (el.dataset.aksrtAnimateInit) return;
|
|
el.dataset.aksrtAnimateInit = '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)';
|
|
}
|
|
|
|
observer.observe(el);
|
|
});
|
|
}
|
|
|
|
/* ============================================================
|
|
* 5. 粘性元素 (Sticky)
|
|
* ============================================================ */
|
|
function initSticky() {
|
|
document.querySelectorAll('[data-aksrt-sticky]').forEach(function (el) {
|
|
if (el.dataset.aksrtStickyInit) return;
|
|
el.dataset.aksrtStickyInit = '1';
|
|
|
|
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. 视差滚动
|
|
* ============================================================ */
|
|
var parallaxScrollBound = false;
|
|
|
|
function initParallax() {
|
|
if (!document.querySelector('[data-aksrt-parallax]')) return;
|
|
if (parallaxScrollBound) return;
|
|
parallaxScrollBound = true;
|
|
|
|
var ticking = false;
|
|
|
|
function update() {
|
|
document.querySelectorAll('[data-aksrt-parallax]').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;
|
|
}
|
|
|
|
window.addEventListener('scroll', function () {
|
|
if (!ticking) {
|
|
requestAnimationFrame(update);
|
|
ticking = true;
|
|
}
|
|
}, { passive: true });
|
|
}
|
|
|
|
/* ============================================================
|
|
* 7. 涟漪点击效果 — document 委托只绑定一次
|
|
* ============================================================ */
|
|
var rippleBound = false;
|
|
|
|
function initRipple() {
|
|
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';
|
|
|
|
el.appendChild(wave);
|
|
|
|
wave.addEventListener('animationend', function () {
|
|
wave.remove();
|
|
});
|
|
});
|
|
}
|
|
|
|
/* ============================================================
|
|
* 8. 表单浮动标签自动初始化
|
|
* ============================================================ */
|
|
function initFloatLabels() {
|
|
document.querySelectorAll('.aksrt-float-label__input').forEach(function (input) {
|
|
if (input.dataset.aksrtFloatInit) return;
|
|
input.dataset.aksrtFloatInit = '1';
|
|
|
|
// 检查初始值
|
|
if (input.value) {
|
|
input.setAttribute('data-has-value', '1');
|
|
}
|
|
input.addEventListener('input', function () {
|
|
if (this.value) {
|
|
this.setAttribute('data-has-value', '1');
|
|
} else {
|
|
this.removeAttribute('data-has-value');
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/* ============================================================
|
|
* 初始化入口 — 先执行一次,随后监听 auto_fun 重扫(各 init 幂等)
|
|
* ============================================================ */
|
|
function init() {
|
|
initTilt();
|
|
initScrollAnimations();
|
|
initRipple();
|
|
initSticky();
|
|
initParallax();
|
|
initFloatLabels();
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
if (window.jQuery) {
|
|
window.jQuery(document).on('auto_fun', init);
|
|
}
|
|
|
|
// 暴露到全局供动态内容使用
|
|
window.AksrtEnhancements = {
|
|
initTilt: initTilt,
|
|
initScrollAnimations: initScrollAnimations,
|
|
initRipple: initRipple,
|
|
};
|
|
|
|
})();
|