commit 20260703

This commit is contained in:
2026-07-03 20:34:27 +08:00
parent 296db4a164
commit c1630b8c10
63 changed files with 3497 additions and 1110 deletions
+183 -1
View File
@@ -1,10 +1,183 @@
/**
* Home hero carousel — autoplay + dot navigation + mobile swipe
* Home hero carousel — autoplay + dot navigation + mobile swipe + layout sync
*
* Layout contract (extensible per banner):
* data-hero-layout-reference — slide that defines carousel height
* data-hero-layout-anchor — element whose bottom anchors dot position
* --hero-coach-dots-gap — gap between anchor bottom and dots top (default 35px)
* Mobile: .is-banner-active fixes dots to viewport bottom while banner is visible
*/
(function () {
var AUTOPLAY_MS = 5000;
var SWIPE_THRESHOLD = 48;
var MOBILE_MQ = '(max-width: 991.98px)';
var DESKTOP_MQ = '(min-width: 992px)';
function readPx(styles, name, fallback) {
var value = parseFloat(styles.getPropertyValue(name));
return Number.isFinite(value) ? value : fallback;
}
function getLayoutScope(root) {
return root.closest('#homepage') || root;
}
function clearMeasuredLayout(scope, root) {
[
'--hero-reference-content-bottom',
'--hero-reference-viewport-height',
'--hero-reference-dots-top',
'--hero-mobile-dots-reserve',
'--hero-dots-y',
].forEach(function (name) {
scope.style.removeProperty(name);
});
if (root) {
var dots = root.querySelector('.home-hero-carousel__dots');
if (dots) {
dots.removeAttribute('data-hero-dots-positioned');
}
}
}
function syncHeroLayout(root) {
var inner = root.querySelector('.home-hero-carousel__inner');
var viewport = root.querySelector('.home-hero-carousel__viewport');
if (!inner || !viewport) return;
var referenceSlide = root.querySelector('[data-hero-layout-reference]')
|| root.querySelector('[data-home-hero-slide]');
if (!referenceSlide) return;
var anchor = referenceSlide.querySelector('[data-hero-layout-anchor]')
|| referenceSlide.querySelector('.home-hero-carousel__ai-coach')
|| referenceSlide.querySelector('.home-hero-carousel__actions :last-child');
if (!anchor) return;
var scope = getLayoutScope(root);
var styles = getComputedStyle(scope);
var coachDotsGap = readPx(styles, '--hero-coach-dots-gap', 35);
var dotsHeight = readPx(styles, '--hero-dots-height', 10);
var innerPaddingBottom = readPx(styles, '--hero-inner-padding-bottom', 40);
var isDesktop = window.matchMedia(DESKTOP_MQ).matches;
if (isDesktop) {
var innerRect = inner.getBoundingClientRect();
var viewportRect = viewport.getBoundingClientRect();
var anchorRect = anchor.getBoundingClientRect();
var contentBottom = Math.ceil(anchorRect.bottom - viewportRect.top);
var viewportHeight = contentBottom + coachDotsGap;
var dotsTop = Math.ceil(anchorRect.bottom - innerRect.top + coachDotsGap);
var dotsBottomY = dotsTop + dotsHeight + innerPaddingBottom;
scope.style.setProperty('--hero-reference-content-bottom', contentBottom + 'px');
scope.style.setProperty('--hero-reference-viewport-height', viewportHeight + 'px');
scope.style.setProperty('--hero-reference-dots-top', dotsTop + 'px');
scope.style.setProperty('--hero-dots-y', dotsBottomY + 'px');
root.setAttribute('data-hero-layout-synced', 'desktop');
var dots = root.querySelector('.home-hero-carousel__dots');
if (dots) {
dots.setAttribute('data-hero-dots-positioned', 'true');
}
root.classList.remove('is-banner-active');
return;
}
clearMeasuredLayout(scope, root);
var viewportRect = viewport.getBoundingClientRect();
var slideGrid = referenceSlide.querySelector('.home-hero-carousel__slide-grid');
var visual = referenceSlide.querySelector('.home-hero-carousel__visual');
var mobileFixedDotsBottom = readPx(styles, '--hero-mobile-fixed-dots-bottom', 16);
var imageScale = readPx(styles, '--hero-visual-image-scale', 1.06);
if (!slideGrid) return;
var visualBottom = visual
? visual.getBoundingClientRect().bottom - viewportRect.top
: slideGrid.getBoundingClientRect().bottom - viewportRect.top;
if (visual) {
var img = visual.querySelector('img');
if (img && imageScale > 1) {
var imgHeight = img.getBoundingClientRect().height;
visualBottom += Math.ceil((imgHeight * imageScale - imgHeight) / 2);
}
}
var viewportHeight = Math.ceil(visualBottom);
var dotsReserve = dotsHeight + mobileFixedDotsBottom;
var homepageRect = scope.getBoundingClientRect();
var dotsBottomY = Math.ceil(
viewportRect.top - homepageRect.top + viewportHeight + dotsReserve + innerPaddingBottom
);
scope.style.setProperty('--hero-reference-viewport-height', viewportHeight + 'px');
scope.style.setProperty('--hero-mobile-dots-reserve', dotsReserve + 'px');
scope.style.setProperty('--hero-dots-y', dotsBottomY + 'px');
root.setAttribute('data-hero-layout-synced', 'mobile');
}
function bindMobileFixedDots(root) {
if (typeof IntersectionObserver === 'undefined') return;
var mobileMq = window.matchMedia(MOBILE_MQ);
var observer = new IntersectionObserver(
function (entries) {
if (!mobileMq.matches) {
root.classList.remove('is-banner-active');
return;
}
root.classList.toggle('is-banner-active', entries[0].isIntersecting);
},
{
threshold: 0,
rootMargin: '0px 0px -8% 0px',
}
);
observer.observe(root);
mobileMq.addEventListener('change', function () {
if (!mobileMq.matches) {
root.classList.remove('is-banner-active');
}
});
}
function bindHeroLayoutSync(root) {
var sync = function () {
syncHeroLayout(root);
};
sync();
if (typeof ResizeObserver !== 'undefined') {
var observer = new ResizeObserver(sync);
observer.observe(root);
var inner = root.querySelector('.home-hero-carousel__inner');
var referenceSlide = root.querySelector('[data-hero-layout-reference]')
|| root.querySelector('[data-home-hero-slide]');
if (inner) observer.observe(inner);
if (referenceSlide) observer.observe(referenceSlide);
var visual = referenceSlide.querySelector('.home-hero-carousel__visual');
if (visual) observer.observe(visual);
}
var resizeTimer;
window.addEventListener('resize', function () {
window.clearTimeout(resizeTimer);
resizeTimer = window.setTimeout(sync, 100);
});
if (document.fonts && document.fonts.ready) {
document.fonts.ready.then(sync);
}
}
function initHomeHeroCarousel(root) {
if (root.getAttribute('data-carousel-ready') === 'true') return;
@@ -133,6 +306,8 @@
});
bindTouchSwipe();
bindHeroLayoutSync(root);
bindMobileFixedDots(root);
setSlide(0);
startAutoplay();
}
@@ -142,6 +317,13 @@
}
window.AifoHomeHeroCarouselInit = initHomeHeroCarousel;
window.AifoHomeHeroCarouselSyncLayout = function (root) {
if (root) {
syncHeroLayout(root);
return;
}
document.querySelectorAll('[data-home-hero-carousel]').forEach(syncHeroLayout);
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot);