Files
arcticnewone/app/components/common/AppLoader.vue
T
virtheart b71ab1a342 feat(tools): 添加工具箱页面及国际化支持
添加新的工具箱页面,包含三个实用工具卡片。更新国际化文件支持多语言显示。优化页面滚动锁定功能,使其可配置自定义类名。调整导航栏菜单项显示逻辑。

工具箱页面包含入场动画效果,并添加SEO元信息。滚动锁定功能现在支持自定义类名,便于不同场景使用。导航栏现在动态显示中间菜单项,排除最后一项。
2026-04-28 14:32:57 +08:00

87 lines
3.1 KiB
Vue

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import gsap from 'gsap'
import { useAppStore } from '~/stores/app'
import { usePageScrollLock } from '~/composables/usePageScrollLock'
const loaderRef = ref<HTMLElement | null>(null)
const loaderBarRef = ref<HTMLElement | null>(null)
const appStore = useAppStore()
// 加载期间锁定页面滚动(注意:Pinia 的 state 是自动解包的,必须用 computed 包装才能保持响应式)
usePageScrollLock(computed(() => appStore.isAppLoading), { className: 'is-scroll-locked' })
let loaderTween: gsap.core.Tween | null = null
let removeLoadListener: (() => void) | null = null
let introStartTimer: ReturnType<typeof window.setTimeout> | null = null
onMounted(() => {
if (!loaderRef.value || !loaderBarRef.value) return
// 1. 初始化 GSAP 样式状态
gsap.set(loaderRef.value, { display: 'grid', yPercent: 0, autoAlpha: 1 })
gsap.set(loaderBarRef.value, { scaleX: 0.18, transformOrigin: 'left center' })
// 2. 进度条往复加载动画
loaderTween = gsap.to(loaderBarRef.value, {
scaleX: 0.82,
duration: 1.2,
ease: 'power1.inOut',
repeat: -1,
yoyo: true,
})
const loaderStartedAt = window.performance.now()
const minLoaderDuration = 1200 // 保证加载动画至少播放 1.2 秒,避免一闪而过
const finishLoading = () => {
removeLoadListener?.()
if (introStartTimer) clearTimeout(introStartTimer)
// 计算还需要等待多久才能达到最小加载时间
const remainingTime = Math.max(0, minLoaderDuration - (window.performance.now() - loaderStartedAt))
introStartTimer = setTimeout(() => {
// 停止往复动画
loaderTween?.kill()
// 3. 执行加载器退场动画
gsap.timeline({
onComplete: () => {
gsap.set(loaderRef.value, { display: 'none' })
appStore.setAppLoading(false)
window.dispatchEvent(new CustomEvent('app-loading-done'))
}
})
.to(loaderBarRef.value, { scaleX: 1, duration: 0.25, ease: 'power2.out' })
.to(loaderRef.value, { yPercent: -100, duration: 0.75, ease: 'power4.inOut' })
}, remainingTime)
}
// 监听浏览器 window.onload 事件以判断页面资源是否加载完毕
if (document.readyState === 'complete') {
finishLoading()
} else {
window.addEventListener('load', finishLoading, { once: true })
removeLoadListener = () => window.removeEventListener('load', finishLoading)
}
})
onBeforeUnmount(() => {
loaderTween?.kill()
if (introStartTimer) clearTimeout(introStartTimer)
removeLoadListener?.()
})
</script>
<template>
<div ref="loaderRef" class="fixed inset-0 z-[90] grid place-items-center bg-black text-white will-change-transform" aria-hidden="true">
<div class="w-[min(220px,calc(100vw-48px))] text-xs uppercase tracking-[0.18em]">
<p class="m-0 mb-[14px]">Loading</p>
<span class="block h-px w-full overflow-hidden bg-white/20">
<span ref="loaderBarRef" class="block h-full w-full origin-left scale-x-0 bg-white"></span>
</span>
</div>
</div>
</template>