mirror of
https://github.com/reonokiy/blog.nokiy.net.git
synced 2025-06-17 03:56:19 +02:00
refactor: optimize website performance and layout
- Replace ScrollButton with BackToTop component - Improve theme toggle and scrollbar event handling - Update layout and index page styling - Optimize client:load and client:idle directives - Remove deprecated Heti addon plugin
This commit is contained in:
parent
14f53a979a
commit
47951152d1
13 changed files with 164 additions and 559 deletions
77
src/components/BackToTop.astro
Normal file
77
src/components/BackToTop.astro
Normal file
|
@ -0,0 +1,77 @@
|
|||
<!-- Sentinel element for scroll detection -->
|
||||
<div
|
||||
id="top-sentinel"
|
||||
class="pointer-events-none absolute left-0 top-0 h-px w-full"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<button
|
||||
id="back-to-top-button"
|
||||
aria-label="Back to top"
|
||||
class="fixed bottom-8 right-8 h-10 w-10 rounded-full bg-background transition-all duration-300 ease-out"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
class="m-auto h-60% w-60%"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M18 15l-6-6-6 6" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<script>
|
||||
let observer: IntersectionObserver | null = null
|
||||
let backToTopButton: HTMLButtonElement | null = null
|
||||
|
||||
function initBackToTop() {
|
||||
// Get elements
|
||||
const sentinel = document.getElementById('top-sentinel')
|
||||
backToTopButton = document.getElementById('back-to-top-button') as HTMLButtonElement
|
||||
|
||||
if (!sentinel || !backToTopButton)
|
||||
return
|
||||
|
||||
// Initialize IntersectionObserver
|
||||
observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
backToTopButton?.classList.add('opacity-0', 'pointer-events-none', 'translate-y-4')
|
||||
}
|
||||
else {
|
||||
backToTopButton?.classList.remove('opacity-0', 'pointer-events-none', 'translate-y-4')
|
||||
}
|
||||
},
|
||||
{
|
||||
threshold: 0,
|
||||
rootMargin: '30% 0% 0% 0%',
|
||||
},
|
||||
)
|
||||
|
||||
// Observe sentinel
|
||||
observer.observe(sentinel)
|
||||
|
||||
// Add click handler
|
||||
backToTopButton.addEventListener('click', () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
// Cleanup observer
|
||||
if (observer) {
|
||||
observer.disconnect()
|
||||
observer = null
|
||||
}
|
||||
|
||||
// Remove event listeners
|
||||
backToTopButton = null
|
||||
}
|
||||
|
||||
// Handle page transitions
|
||||
document.addEventListener('astro:page-load', initBackToTop)
|
||||
document.addEventListener('astro:before-swap', cleanup)
|
||||
</script>
|
|
@ -10,6 +10,8 @@
|
|||
lightbox.destroy()
|
||||
lightbox = null
|
||||
}
|
||||
document.removeEventListener('astro:page-load', createPhotoSwipe)
|
||||
document.removeEventListener('astro:before-swap', cleanup)
|
||||
}
|
||||
|
||||
function createPhotoSwipe() {
|
||||
|
|
|
@ -1,91 +0,0 @@
|
|||
<button
|
||||
id="scroll-button"
|
||||
aria-label="Scroll to bottom"
|
||||
class="fixed bottom-8 right-8 h-10 w-10 rounded-full bg-background p-0 opacity-0 transition-all duration-300"
|
||||
>
|
||||
<svg
|
||||
class="m-auto h-6 w-6 c-primary transition-transform"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M15 18l-6-6 6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<script>
|
||||
const SCROLL_DISPLAY_THRESHOLD = 200
|
||||
const DIRECTION_CHANGE_THRESHOLD = 200
|
||||
const BOTTOM_PROXIMITY_THRESHOLD = 50
|
||||
|
||||
// Initialize scroll button functionality and return cleanup function
|
||||
function initScrollButton(): (() => void) | void {
|
||||
const button = document.getElementById('scroll-button')
|
||||
if (!button)
|
||||
return
|
||||
|
||||
const buttonElement = button as HTMLButtonElement
|
||||
let isScrollingDown = true
|
||||
let lastScrollPosition = window.scrollY
|
||||
let scrollTimeout: number | null = null
|
||||
|
||||
function updateButton() {
|
||||
const currentScrollPosition = window.scrollY
|
||||
const scrollDifference = currentScrollPosition - lastScrollPosition
|
||||
const maxScroll = document.documentElement.scrollHeight - window.innerHeight
|
||||
const isNearBottom = maxScroll - currentScrollPosition < BOTTOM_PROXIMITY_THRESHOLD
|
||||
|
||||
// Update button direction and behavior when scroll direction changes or near bottom
|
||||
if (Math.abs(scrollDifference) > DIRECTION_CHANGE_THRESHOLD || isNearBottom) {
|
||||
isScrollingDown = isNearBottom ? false : scrollDifference > 0
|
||||
lastScrollPosition = currentScrollPosition
|
||||
|
||||
const svg = buttonElement.querySelector('svg')
|
||||
if (svg instanceof SVGElement) {
|
||||
svg.style.transform = isScrollingDown ? 'rotate(-90deg)' : 'rotate(90deg)'
|
||||
}
|
||||
|
||||
buttonElement.setAttribute('aria-label', isScrollingDown ? 'Scroll to bottom' : 'Scroll to top')
|
||||
buttonElement.onclick = () => {
|
||||
window.scrollTo({
|
||||
top: isScrollingDown ? maxScroll : 0,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const shouldShow = currentScrollPosition > SCROLL_DISPLAY_THRESHOLD
|
||||
buttonElement.classList.toggle('opacity-100', shouldShow)
|
||||
buttonElement.classList.toggle('invisible', !shouldShow)
|
||||
}
|
||||
|
||||
// Handle scroll events with requestAnimationFrame for performance
|
||||
function scrollHandler() {
|
||||
if (scrollTimeout) {
|
||||
window.cancelAnimationFrame(scrollTimeout)
|
||||
}
|
||||
scrollTimeout = window.requestAnimationFrame(updateButton)
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', scrollHandler, { passive: true })
|
||||
updateButton()
|
||||
|
||||
// Return cleanup function to remove event listeners
|
||||
return () => {
|
||||
window.removeEventListener('scroll', scrollHandler)
|
||||
if (scrollTimeout) {
|
||||
window.cancelAnimationFrame(scrollTimeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cleanup: (() => void) | null = null
|
||||
|
||||
document.addEventListener('astro:page-load', () => {
|
||||
cleanup?.()
|
||||
cleanup = initScrollButton() || null
|
||||
})
|
||||
|
||||
document.addEventListener('astro:before-swap', () => cleanup?.())
|
||||
</script>
|
|
@ -41,7 +41,6 @@ document.addEventListener('astro:before-swap', () => {
|
|||
scrollbarsInstance = null
|
||||
})
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initScrollbar)
|
||||
document.addEventListener('astro:page-load', initScrollbar)
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,12 +1,29 @@
|
|||
<button
|
||||
aria-pressed="false"
|
||||
aria-label="Theme Toggle Button"
|
||||
class="absolute right-[calc(9.94vw-1.18rem)] top-[calc(7.3vw+2.6rem)] z-99 aspect-square w-7 c-secondary [@supports(-webkit-touch-callout:none)]:top-[calc(7.3vw+2.2rem)] active:scale-90"
|
||||
lg="hidden"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="m12 1c-6.1 0-11 4.9-11 11s4.9 11 11 11 11-4.9 11-11-4.9-11-11-11m0 20c-5.8 0-10.5-4-10.5-9s4.7-9 10.5-9 10.5 4 10.5 9-4.7 9-10.5 9" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<script>
|
||||
// Initialize theme toggle button
|
||||
const themeToggle = document.querySelector('button[aria-pressed]') as HTMLButtonElement
|
||||
themeToggle.setAttribute('aria-pressed', String(document.documentElement.classList.contains('dark')))
|
||||
const theme = localStorage.getItem('theme')
|
||||
document.documentElement.classList.toggle('dark', theme === 'dark')
|
||||
|
||||
function switchTheme() {
|
||||
document.body.removeAttribute('data-restore-theme')
|
||||
const isDark = document.documentElement.classList.toggle('dark')
|
||||
themeToggle.setAttribute('aria-pressed', String(isDark))
|
||||
const themeToggle = document.querySelector('button[aria-pressed]') as HTMLButtonElement
|
||||
if (themeToggle) {
|
||||
themeToggle.setAttribute('aria-pressed', String(isDark))
|
||||
}
|
||||
localStorage.setItem('theme', isDark ? 'dark' : 'light')
|
||||
document.dispatchEvent(new Event('theme-changed'))
|
||||
}
|
||||
|
@ -17,7 +34,6 @@ function toggleTheme() {
|
|||
return
|
||||
}
|
||||
|
||||
// Set temporary transition name for theme toggle
|
||||
document.documentElement.style.setProperty('view-transition-name', 'theme-transition')
|
||||
document.documentElement.setAttribute('data-theme-transition', '')
|
||||
|
||||
|
@ -25,7 +41,6 @@ function toggleTheme() {
|
|||
switchTheme()
|
||||
})
|
||||
|
||||
// Clean up after transition
|
||||
transition.finished.then(() => {
|
||||
document.documentElement.style.removeProperty('view-transition-name')
|
||||
document.documentElement.removeAttribute('data-theme-transition')
|
||||
|
@ -36,38 +51,44 @@ function syncTheme() {
|
|||
document.documentElement.setAttribute('data-restore-theme', 'true')
|
||||
const theme = localStorage.getItem('theme')
|
||||
document.documentElement.classList.toggle('dark', theme === 'dark')
|
||||
themeToggle.setAttribute('aria-pressed', String(theme === 'dark'))
|
||||
|
||||
const themeToggle = document.querySelector('button[aria-pressed]') as HTMLButtonElement
|
||||
if (themeToggle) {
|
||||
themeToggle.setAttribute('aria-pressed', String(theme === 'dark'))
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
document.documentElement.removeAttribute('data-restore-theme')
|
||||
})
|
||||
}
|
||||
|
||||
document.addEventListener('astro:page-load', () => {
|
||||
// 事件处理函数
|
||||
function handleAfterSwap() {
|
||||
syncTheme()
|
||||
const themeToggle = document.querySelector('button[aria-pressed]') as HTMLButtonElement
|
||||
themeToggle.setAttribute('aria-pressed', String(document.documentElement.classList.contains('dark')))
|
||||
themeToggle.addEventListener('click', toggleTheme)
|
||||
})
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener('click', toggleTheme)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('astro:after-swap', syncTheme)
|
||||
window.addEventListener('popstate', syncTheme)
|
||||
window.addEventListener('pageshow', (event) => {
|
||||
function handlePageShow(event: PageTransitionEvent) {
|
||||
if (event.persisted) {
|
||||
syncTheme()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
}
|
||||
|
||||
<button
|
||||
aria-pressed="false"
|
||||
aria-label="Theme Toggle Button"
|
||||
class="absolute right-[calc(9.94vw-1.18rem)] top-[calc(7.3vw+2.6rem)] z-99 aspect-square w-7 c-secondary [@supports(-webkit-touch-callout:none)]:top-[calc(7.3vw+2.2rem)] active:scale-90"
|
||||
lg="hidden"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="m12 1c-6.1 0-11 4.9-11 11s4.9 11 11 11 11-4.9 11-11-4.9-11-11-11m0 20c-5.8 0-10.5-4-10.5-9s4.7-9 10.5-9 10.5 4 10.5 9-4.7 9-10.5 9" />
|
||||
</svg>
|
||||
</button>
|
||||
// 集中管理所有事件监听器
|
||||
function initThemeEvents() {
|
||||
const themeToggle = document.querySelector('button[aria-pressed]') as HTMLButtonElement
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener('click', toggleTheme)
|
||||
}
|
||||
|
||||
document.addEventListener('astro:after-swap', handleAfterSwap)
|
||||
window.addEventListener('popstate', syncTheme)
|
||||
window.addEventListener('pageshow', handlePageShow)
|
||||
}
|
||||
|
||||
// 初始化
|
||||
initThemeEvents()
|
||||
</script>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue