| @@ -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; | |||||