🐼 PandaCSS 实现加载动画+ 🔧Vue3 自定义指令封装

前言

加载动画组件是一个非常常见且重要的组件。它可以用于提升用户体验,让用户了解应用正在加载数据或执行某些操作。在本文中,我们将使用 PandaCSS 和 Vue 3 的自定义指令,来实现一个加载动画组件和指令。通过这个加载指令,我们将可以很方便地在应用中添加不同的加载动画效果。下面我们一步一步来实现这个功能。

实现加载组件

样式实现

加载组件的实现原理就是:在某个组件中使用加载组件时,加载组件会生成一个与原组件大小相同,圆角相同的遮罩元素,以绝对布局的方式挡在原本的组件前,然后内部居中展示动画效果,如下图所示:

那我们先实现一下加载组件的样式:

ts 复制代码
import { RecipeVariantProps, sva } from "@/styled-system/css";

const loadingRecipe = sva({
  slots: ["backdrop", "container", "load1", "load2", "load3"],
  base: {
    backdrop: {
      position: "absolute",
      top: "0px",
      left: "0px",
      width: "full",
      height: "full",
      zIndex: 1,
      display: "flex",
      alignItems: "center",
      justifyContent: "center",
      transition: "all 0.25s ease",
      borderRadius: "inherit",
      background: "rgba(0, 0 ,0 ,0.2)",
    },
    container: {
      display: "flex",
      alignItems: "center",
      justifyContent: "center",
      flexDirection: "column",
      transform: "scale(80%)",
    },
  },
  variants: {
    type: {
      default: {
        container: {
          borderRadius: "50%",
        },
        load1: {
          position: "absolute",
          top: "0px",
          bottom: "0px",
          margin: "auto",
          border: `3px solid token(colors.colorPalette.200)`,
          borderRadius: "inherit",
          borderTop: "3px solid transparent",
          borderLeft: "3px solid transparent",
          borderRight: "3px solid transparent",
          animation: "spin 0.8s ease infinite",
        },
        load2: {
          position: "absolute",
          top: "0px",
          bottom: "0px",
          margin: "auto",
          border: "3px dashed token(colors.colorPalette.200)",
          borderRadius: "inherit",
          borderTop: "3px solid transparent",
          borderLeft: "3px solid transparent",
          borderRight: "3px solid transparent",
          animation: "spin 0.8s linear infinite",
          opacity: "0.4",
        },
      },
    },
  },
});

export default loadingRecipe;
export type LoadingVariants = Exclude<
  RecipeVariantProps<typeof loadingRecipe>,
  undefined
>;

这里使用了 pandaCSS 中的 sva (插槽配方)函数创建了一个组件配方,slots 声明了当前组件有哪些元素:

  • backdrop:遮罩元素
  • container:遮罩元素向内一层的加载容器元素
  • load1,load2,load3:用于实现加载动画的三个占位元素

样式的实现有一些注意点:

  • container:设置了 transform: "scale(80%)",确保加载动画不会直接占满整个组件,防止样式贴边
  • load1 :是通过设置一个正方形容器的某一个边框,就变成了一条直线,再加上圆角效果就变成了一条曲线,最后加上旋转的动画就实现了一个基础的加载样式
  • load2 :与 load2 实现方式一致,只是将边框样式从直线改为虚线,并且设置不同的动画执行曲线,这样就 跑的比 load1 更慢一点。

动画的效果如下图:

结构实现

接着我们实现组件的基本结构:

tsx 复制代码
import { PropType, Transition, computed, defineComponent, ref } from "vue";

import loadingRecipe, { LoadingVariants } from "./recipe";
import { css, cx } from "@/styled-system/css";

const ZLoading = defineComponent({
  name: "ZLoading",
  props: {
    color: {
      type: String,
      default: css({ colorPalette: "gray" }),
    },
    type: {
      type: String as PropType<LoadingVariants["type"]>,
      default: "default",
    },
    size: {
      type: String,
    },
  },
  setup(props) {
    const rootRef = ref<HTMLElement>();
    const classes = loadingRecipe({ type: props.type });

    const innerSize = computed(() => {
      if (rootRef.value) {
        const el = rootRef.value as HTMLElement;
        return `${Math.min(el.offsetWidth, el.offsetHeight)}px`;
      }
      return 0;
    });

    return () => (
      <Transition
        enterActiveClass={css({ transition: "opacity 0.5s" })}
        leaveActiveClass={css({ transition: "opacity 0.5s" })}
        enterToClass={css({ opacity: 0 })}
      >
        <div ref={rootRef} class={classes.backdrop}>
          <div
            class={classes.container}
            style={{
              width: props.size || innerSize.value,
              height: props.size || innerSize.value,
            }}
          >
            {([1, 2, 3] as const).map((item) => (
              <div key={item} class={cx(props.color, classes[`load${item}`])} />
            ))}
          </div>
        </div>
      </Transition>
    );
  },
});

export default ZLoading;

export type ZLoadingProps = InstanceType<typeof ZLoading>["$props"];

loadingRecipe 是一个前面定义的插槽配方函数,它接受一个对象参数 { type: props.type },并返回一个类名对象用于生成样式类名。这个函数是根据 props 中的 type 属性来决定加载组件的样式变体。

这里有一个比较特殊的逻辑,innerSize 是一个计算属性,根据 rootRef 的值获取加载组件的宽度和高度,取其中的较小值,加载容器元素的宽度和高度使用 props.size 或计算属性 innerSize 的值。

之所以要取宽度和高度间的最小值,是为了适配宽高不等的容器。例如一个长条的按钮组件,如果还按照容器的宽高去实现加载动画,内部的曲线就会变成这样:

因此我们需要保证内部加载动画容器的始终是是一个正方形,且加载动画不要超出外层容器,这样才能实现正常的加载效果:

加载组件的外层还使用 <Transition> 组件包裹加载组件,实现加载动画的淡入淡出效果。

指令实现

Vue 中自定义指令的使用场景有很多,通常用于处理与 DOM 直接交互的逻辑。加载动画可以是一个很好的使用自定义指令的场景。当你想要在某个元素或组件上添加加载动画时,通过自定义指令可以将相关逻辑封装起来,使其具备可重用性,并可以在整个应用中进行统一的使用和管理。

具体的使用方式大家直接看官方文档:Vue3 自定义指令,这里直接展示我的实现:

tsx 复制代码
import { Directive, DirectiveBinding, render } from "vue";

import ZLoading, { ZLoadingProps } from "./loading";

const domId = "LOADING_CONTAINER";

const mountLoading = (el: HTMLElement, binding: DirectiveBinding) => {
  const loadingDom = document.createElement("div");
  loadingDom.id = domId;
  loadingDom.style.borderRadius = "inherit";
  render(<ZLoading type={binding.arg as ZLoadingProps["type"]} />, loadingDom);
  el.appendChild(loadingDom);
};

const unmountLoading = (el: HTMLElement) => {
  const dom = el.querySelector(`#${domId}`);
  if (dom) {
    el.removeChild(dom);
  }
};

const ZLoadingDirective: Directive<HTMLElement, boolean> = {
  mounted(el, binding) {
    // eslint-disable-next-line no-param-reassign
    el.style.position = "relative";
    if (binding.value !== false) {
      mountLoading(el, binding);
    }
  },
  updated(el, binding) {
    if (binding.value !== binding.oldValue) {
      if (binding.value !== false) {
        mountLoading(el, binding);
      } else {
        unmountLoading(el);
      }
    }
  },
  unmounted(el) {
    unmountLoading(el);
  },
};

export default ZLoadingDirective;
  1. mountLoading 函数用于将加载组件渲染到指定的元素中。它会创建一个容器元素 loadingDom,设置其 iddomId 和样式,然后使用 render 方法将 ZLoading 组件渲染到 loadingDom 容器中。最后,将其添加到指定的元素 el 中。

  2. unmountLoading 函数用于从指定的元素中移除加载组件。它会查找指定的容器元素,并从父元素 el 中移除该容器。

  3. ZLoadingDirective 是一个自定义指令对象,包含了 mountedupdatedunmounted 三个生命周期钩子函数的实现。

    • mounted 钩子函数中,首先设置元素的 position 为相对定位,然后根据绑定的值判断是否需要添加加载动画,如果需要则调用 mountLoading 函数添加加载动画。
    • updated 钩子函数中,根据绑定的值的变化判断是否需要添加或移除加载动画。如果绑定值不为 false,则调用 mountLoading 函数添加加载动画;如果绑定值变为 false,则调用 unmountLoading 函数移除加载动画。
    • 之所以使用 binding.value !== false 的判断方式,是为了方便指令使用时可以直接 v-loading 展示动画,只有手动声明 v-loading={false} 才隐藏会隐藏动画。
    • unmounted 钩子函数中,调用 unmountLoading 函数移除加载动画。

接下来我们就可以在其他组件中使用这个自定义指令了,以按钮组件为例:

html 复制代码
<button v-loading:default={props.loading}>
    {slots?.default?.()}
  </button>

default 代表了加载动画的样式,props.loading 代表是否需要展示加载动画。

实现更多的加载动画效果

前面我们将默认的动画效果设置为 default ,这意味着我们可以添加更多的变体,实现不同的加载动画效果:

tsx 复制代码
const loadingRecipe = sva({
  slots: ["backdrop", "container", "load1", "load2", "load3"],
  //...
  variants: {
    type: {
    // 默认圆圈加载变体
      default: {
        container: {
          borderRadius: "50%",
        },
        load1: {
          position: "absolute",
          top: "0px",
          bottom: "0px",
          margin: "auto",
          border: `3px solid token(colors.colorPalette.200)`,
          borderRadius: "inherit",
          borderTop: "3px solid transparent",
          borderLeft: "3px solid transparent",
          borderRight: "3px solid transparent",
          animation: "spin 0.8s ease infinite",
        },
        load2: {
          position: "absolute",
          top: "0px",
          bottom: "0px",
          margin: "auto",
          border: "3px dashed token(colors.colorPalette.200)",
          borderRadius: "inherit",
          borderTop: "3px solid transparent",
          borderLeft: "3px solid transparent",
          borderRight: "3px solid transparent",
          animation: "spin 0.8s linear infinite",
          opacity: "0.4",
        },
      },
      // 圆角加载变体
      corner: {
        load1: {
          width: "80%",
          height: "80%",
          background: "transparent",
          position: "absolute",
          animation: "corner 1s ease infinite",
          border: `3px solid token(colors.colorPalette.200)`,
          borderRadius: "50%",
        },
      },
      // 跳跃圆点加载变体
      point: {
        container: {
          flexDirection: "row",
        },
        load1: {
          width: "8px",
          height: "8px",
          background: " token(colors.colorPalette.200)",
          borderRadius: "50%",
          margin: "3px",
          animation: "point 0.75s ease infinite",
        },
        load2: {
          width: "8px",
          height: "8px",
          background: " token(colors.colorPalette.200)",
          borderRadius: "50%",
          margin: "3px",
          animation: "point 0.75s ease infinite 0.25s",
        },
        load3: {
          width: "8px",
          height: "8px",
          background: "token(colors.colorPalette.200)",
          borderRadius: "50%",
          margin: "3px",
          animation: "point 0.75s ease infinite 0.5s",
        },
      },
      // 方块加载变体
      square: {
        load1: {
          position: "absolute",
          width: "60%",
          height: "60%",
          animation: "rotateSquare 3s ease infinite",
          background: "token(colors.colorPalette.200)",
        },
      },
    },
  },
});

这里补充了三种加载样式变体,然后我们将加载动画的类型修改为变量,在 JSX/TSX 中可以这么写:

tsx: 复制代码
 v-loading={[props.loading, props.loadingType]}

效果相当于 vue 中:

tsx: 复制代码
 v-loading:[props.loadingType]="props.loading"

最终实现的效果如下图:

总结

看到这里,相信大家应该学会如何使用 PandaCSS 和 Vue 3 的自定义指令来创建和使用加载动画组件了,本篇文章中的所有代码可以在这里看到:github.com/oil-oil/zax... , 后续的更新也都在这个仓库中, 快去点个 star 🌟

如果文章对你有帮助在收藏的同时也可以为我点个赞👍,respect!

相关推荐
工业互联网专业17 分钟前
毕业设计选题:基于springboot+vue+uniapp的驾校报名小程序
vue.js·spring boot·小程序·uni-app·毕业设计·源码·课程设计
J不A秃V头A1 小时前
Vue3:编写一个插件(进阶)
前端·vue.js
司篂篂1 小时前
axios二次封装
前端·javascript·vue.js
姚*鸿的博客2 小时前
pinia在vue3中的使用
前端·javascript·vue.js
宇文仲竹2 小时前
edge 插件 iframe 读取
前端·edge
Kika写代码2 小时前
【基于轻量型架构的WEB开发】【章节作业】
前端·oracle·架构
天下无贼!3 小时前
2024年最新版Vue3学习笔记
前端·vue.js·笔记·学习·vue
Jiaberrr3 小时前
JS实现树形结构数据中特定节点及其子节点显示属性设置的技巧(可用于树形节点过滤筛选)
前端·javascript·tree·树形·过滤筛选
赵啸林3 小时前
npm发布插件超级简单版
前端·npm·node.js
罔闻_spider4 小时前
爬虫----webpack
前端·爬虫·webpack