|
|
@@ -0,0 +1,90 @@ |
|
|
|
|
|
/* |
|
|
|
|
|
Copyright (C) 2025 QuantumNous |
|
|
|
|
|
|
|
|
|
|
|
This program is free software: you can redistribute it and/or modify |
|
|
|
|
|
it under the terms of the GNU Affero General Public License as |
|
|
|
|
|
published by the Free Software Foundation, either version 3 of the |
|
|
|
|
|
License, or (at your option) any later version. |
|
|
|
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful, |
|
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
|
|
|
|
GNU Affero General Public License for more details. |
|
|
|
|
|
|
|
|
|
|
|
You should have received a copy of the GNU Affero General Public License |
|
|
|
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>. |
|
|
|
|
|
|
|
|
|
|
|
For commercial licensing, please contact support@quantumnous.com |
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
|
|
|
|
import React, { useState, useEffect, useCallback } from 'react'; |
|
|
|
|
|
|
|
|
|
|
|
const ScrollNavigator = ({ sections }) => { |
|
|
|
|
|
const [activeIndex, setActiveIndex] = useState(0); |
|
|
|
|
|
|
|
|
|
|
|
const getActiveSection = useCallback(() => { |
|
|
|
|
|
const container = document.querySelector('.home-snap-container'); |
|
|
|
|
|
if (!container) return 0; |
|
|
|
|
|
|
|
|
|
|
|
const scrollTop = container.scrollTop; |
|
|
|
|
|
const containerHeight = container.clientHeight; |
|
|
|
|
|
const scrollCenter = scrollTop + containerHeight * 0.4; |
|
|
|
|
|
|
|
|
|
|
|
let closestIndex = 0; |
|
|
|
|
|
let closestDistance = Infinity; |
|
|
|
|
|
|
|
|
|
|
|
sections.forEach((section, index) => { |
|
|
|
|
|
const el = document.getElementById(section.id); |
|
|
|
|
|
if (el) { |
|
|
|
|
|
const rect = el.getBoundingClientRect(); |
|
|
|
|
|
const elCenter = rect.top + rect.height / 2 + scrollTop; |
|
|
|
|
|
const distance = Math.abs(elCenter - scrollCenter); |
|
|
|
|
|
|
|
|
|
|
|
if (distance < closestDistance) { |
|
|
|
|
|
closestDistance = distance; |
|
|
|
|
|
closestIndex = index; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
return closestIndex; |
|
|
|
|
|
}, [sections]); |
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
const container = document.querySelector('.home-snap-container'); |
|
|
|
|
|
if (!container) return; |
|
|
|
|
|
|
|
|
|
|
|
const handleScroll = () => { |
|
|
|
|
|
const newIndex = getActiveSection(); |
|
|
|
|
|
setActiveIndex(newIndex); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
container.addEventListener('scroll', handleScroll, { passive: true }); |
|
|
|
|
|
handleScroll(); // 初始化 |
|
|
|
|
|
|
|
|
|
|
|
return () => container.removeEventListener('scroll', handleScroll); |
|
|
|
|
|
}, [getActiveSection]); |
|
|
|
|
|
|
|
|
|
|
|
const scrollToSection = (id) => { |
|
|
|
|
|
const el = document.getElementById(id); |
|
|
|
|
|
if (el) { |
|
|
|
|
|
el.scrollIntoView({ behavior: 'smooth' }); |
|
|
|
|
|
} |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
|
<nav className="scroll-navigator"> |
|
|
|
|
|
{sections.map((section, index) => ( |
|
|
|
|
|
<button |
|
|
|
|
|
key={section.id} |
|
|
|
|
|
className={`scroll-navigator-dot ${activeIndex === index ? 'active' : ''}`} |
|
|
|
|
|
onClick={() => scrollToSection(section.id)} |
|
|
|
|
|
title={section.label} |
|
|
|
|
|
aria-label={section.label} |
|
|
|
|
|
/> |
|
|
|
|
|
))} |
|
|
|
|
|
</nav> |
|
|
|
|
|
); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
export default ScrollNavigator; |