封装loading指令

效果

在这里插入图片描述

代码

// loading.ts
import { nextTick } from 'vue'
import type { Directive, DirectiveBinding } from 'vue'

// 扩展HTMLElement类型,添加我们需要的属性
interface LoadingElement extends HTMLElement {
  __v_loading_element?: HTMLElement
}

/**
 * 自定义样式的加载指令
 * 用法:v-loading="isLoading"
 * 参数:布尔值,控制是否显示加载效果
 */
const loading: Directive<HTMLElement, boolean> = {
  mounted(el: HTMLElement, binding: DirectiveBinding<boolean>) {
    // 设置父元素为相对定位
    if (getComputedStyle(el).position === 'static') {
      el.style.position = 'relative'
    }

    // 创建加载层
    const loadingElement = document.createElement('div')

    // 设置加载层样式
    Object.assign(loadingElement.style, {
      position: 'absolute',
      top: '0',
      left: '0',
      width: '100%',
      height: '100%',
      backgroundColor: 'rgba(255, 255, 255, 0.7)',
      display: 'flex',
      justifyContent: 'center',
      alignItems: 'center',
      zIndex: '999'
    })

    // 创建新的立方体加载动画
    const spinner = document.createElement('div')
    spinner.className = 'spinner'

    // 确保spinner元素本身有明确的宽高
    Object.assign(spinner.style, {
      width: '60px',
      height: '60px'
    })

    // 创建立方体的六个面
    for (let i = 0; i < 6; i++) {
      const face = document.createElement('div')
      spinner.appendChild(face)
    }

    // 添加动画样式
    if (!document.getElementById('v-loading-styles')) {
      const styleElement = document.createElement('style')
      styleElement.id = 'v-loading-styles'
      // 使用新提供的CSS样式
      styleElement.textContent = `
        .spinner {
          --clr: rgb(94, 104, 255);
          --clr-alpha: rgb(94, 104, 255,.1);
          animation: spinner 1.6s infinite ease;
          transform-style: preserve-3d;
          position: relative;
        }
        
        .spinner > div {
          background-color: var(--clr-alpha);
          height: 100%;
          position: absolute;
          width: 100%;
          border: 3.5px solid var(--clr);
        }
        
        .spinner div:nth-of-type(1) {
          transform: translateZ(-30px) rotateY(180deg);
        }
        
        .spinner div:nth-of-type(2) {
          transform: rotateY(-270deg) translateX(50%);
          transform-origin: top right;
        }
        
        .spinner div:nth-of-type(3) {
          transform: rotateY(270deg) translateX(-50%);
          transform-origin: center left;
        }
        
        .spinner div:nth-of-type(4) {
          transform: rotateX(90deg) translateY(-50%);
          transform-origin: top center;
        }
        
        .spinner div:nth-of-type(5) {
          transform: rotateX(-90deg) translateY(50%);
          transform-origin: bottom center;
        }
        
        .spinner div:nth-of-type(6) {
          transform: translateZ(30px);
        }
        
        @keyframes spinner {
          0% {
            transform: rotate(45deg) rotateX(-25deg) rotateY(25deg);
          }
        
          50% {
            transform: rotate(45deg) rotateX(-385deg) rotateY(25deg);
          }
        
          100% {
            transform: rotate(45deg) rotateX(-385deg) rotateY(385deg);
          }
        }
      `
      document.head.appendChild(styleElement)
    }

    // 添加加载图标到加载层
    loadingElement.appendChild(spinner)

    // 将加载层元素存储在原始元素上
    ;(el as LoadingElement).__v_loading_element = loadingElement

    // 如果初始值为true,则显示加载效果
    if (binding.value) {
      el.appendChild(loadingElement)
    }
  },

  updated(el: HTMLElement, binding: DirectiveBinding<boolean>) {
    // 值变化时更新加载状态
    if (binding.value !== binding.oldValue) {
      nextTick(() => {
        const loadingElement = (el as LoadingElement).__v_loading_element

        if (binding.value) {
          // 显示加载效果
          if (loadingElement && !el.contains(loadingElement)) {
            el.appendChild(loadingElement)
          }
        } else {
          // 隐藏加载效果
          if (loadingElement && el.contains(loadingElement)) {
            el.removeChild(loadingElement)
          }
        }
      })
    }
  },

  unmounted(el: HTMLElement) {
    // 清理加载层
    const loadingElement = (el as LoadingElement).__v_loading_element
    if (loadingElement && el.contains(loadingElement)) {
      el.removeChild(loadingElement)
    }
  }
}

export default loading

使用

<script setup lang="ts">
import vLoading from '@/directives/loading'
</script>

<template>
  <div v-loading="true" class="flex flex-1 flex-col">
    <div class="w-full flex-1"></div>
  </div>
</template>

更多推荐