Spaces:
Sleeping
Sleeping
| <template> | |
| <div class="loading-container" v-if="isLoading"> | |
| <div class="loading-spinner"> | |
| <div class="spinner-ring" ref="ring1"></div> | |
| <div class="spinner-ring" ref="ring2"></div> | |
| <div class="spinner-ring" ref="ring3"></div> | |
| <div class="spinner-dot" ref="dot"></div> | |
| </div> | |
| <p class="loading-text">{{ loadingText }}</p> | |
| </div> | |
| </template> | |
| <script setup lang="ts"> | |
| import { ref, onMounted, watch } from 'vue' | |
| import { gsap } from 'gsap' | |
| // 定义组件名称 | |
| defineOptions({ | |
| name: 'LoadingSpinner' | |
| }) | |
| interface Props { | |
| isLoading: boolean | |
| loadingText?: string | |
| } | |
| const props = withDefaults(defineProps<Props>(), { | |
| loadingText: '加载中...' | |
| }) | |
| const ring1 = ref<HTMLElement>() | |
| const ring2 = ref<HTMLElement>() | |
| const ring3 = ref<HTMLElement>() | |
| const dot = ref<HTMLElement>() | |
| onMounted(() => { | |
| if (props.isLoading) { | |
| startAnimation() | |
| } | |
| }) | |
| watch(() => props.isLoading, (newVal) => { | |
| if (newVal) { | |
| startAnimation() | |
| } else { | |
| stopAnimation() | |
| } | |
| }) | |
| const startAnimation = () => { | |
| if (!ring1.value || !ring2.value || !ring3.value || !dot.value) return | |
| // 外环动画 | |
| gsap.to(ring1.value, { | |
| rotation: 360, | |
| duration: 2, | |
| ease: "none", | |
| repeat: -1 | |
| }) | |
| // 中环动画 | |
| gsap.to(ring2.value, { | |
| rotation: -360, | |
| duration: 1.5, | |
| ease: "none", | |
| repeat: -1 | |
| }) | |
| // 内环动画 | |
| gsap.to(ring3.value, { | |
| rotation: 360, | |
| duration: 1, | |
| ease: "none", | |
| repeat: -1 | |
| }) | |
| // 中心点脉冲动画 | |
| gsap.to(dot.value, { | |
| scale: 1.2, | |
| duration: 0.8, | |
| ease: "power2.inOut", | |
| repeat: -1, | |
| yoyo: true | |
| }) | |
| } | |
| const stopAnimation = () => { | |
| gsap.killTweensOf([ring1.value, ring2.value, ring3.value, dot.value]) | |
| } | |
| </script> | |
| <style scoped> | |
| .loading-container { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(255, 255, 255, 0.95); | |
| backdrop-filter: blur(10px); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 9999; | |
| } | |
| .loading-spinner { | |
| position: relative; | |
| width: 80px; | |
| height: 80px; | |
| margin-bottom: 2rem; | |
| } | |
| .spinner-ring { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| border: 3px solid transparent; | |
| border-radius: 50%; | |
| } | |
| .spinner-ring:nth-child(1) { | |
| border-top-color: var(--ramex-accent); | |
| border-right-color: var(--ramex-accent); | |
| } | |
| .spinner-ring:nth-child(2) { | |
| width: 60px; | |
| height: 60px; | |
| top: 10px; | |
| left: 10px; | |
| border-top-color: var(--ramex-text-secondary); | |
| border-left-color: var(--ramex-text-secondary); | |
| } | |
| .spinner-ring:nth-child(3) { | |
| width: 40px; | |
| height: 40px; | |
| top: 20px; | |
| left: 20px; | |
| border-top-color: var(--ramex-text-muted); | |
| border-right-color: var(--ramex-text-muted); | |
| } | |
| .spinner-dot { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| width: 12px; | |
| height: 12px; | |
| background: var(--ramex-accent); | |
| border-radius: var(--ramex-radius); | |
| transform: translate(-50%, -50%); | |
| } | |
| .loading-text { | |
| font-size: 1.1rem; | |
| font-weight: 500; | |
| color: var(--ramex-text-secondary); | |
| margin: 0; | |
| animation: pulse 2s ease-in-out infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { | |
| opacity: 0.6; | |
| } | |
| 50% { | |
| opacity: 1; | |
| } | |
| } | |
| </style> | |