VUE3组件封装系列-->01(骨架屏,面包屑,轮播图)

303 阅读1分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第N天,点击查看活动详情 >>PS:第4天。

首先全局组件准备

  • 创建components文件夹,在这个文件夹里创建index.ts文件
  • main.ts中进行挂载
import XtxUI from '@/components'
createApp(App).use(pinia).use(XtxUI).use(router).mount('#app')
  • components文件夹里的index.ts文件中使用install方法进行挂载
export default {
  install(app: App) {
    app.component(XtxSkeleton.name, XtxSkeleton)
    app.component(XtxCarousel.name, XtxCarousel)
    app.component(XtxMore.name, XtxMore)
    app.component(XtxBread.name, XtxBread)
    app.component(XtxBreadItem.name, XtxBreadItem)
    }
    }

VUE3还需要生命一个类型文件,使得在代码中输入全局组件时出现提示

src下创建文件global.d.ts

import XtxSkeleton from '@/components/skeleton/index.vue'
import XtxCarousel from '@/components/carousel/index.vue'
import XtxMore from '@/components/more/index.vue'
import XtxBread from '@/components/bread/index.vue'
import XtxBreadItem from '@/components/bread/item.vue'
declare module 'vue' {
 export interface GlobalComponents {
   XtxSkeleton: typeof XtxSkeleton
   XtxCarousel: typeof XtxCarousel
   XtxMore: typeof XtxMore
   XtxBread: typeof XtxBread
   XtxBreadItem: typeof XtxBreadItem
     }
}
export {}

组件创建开始

1.骨架屏

组件代码(动画闪动的效果)

<script lang="ts" setup name="XtxSkeleton">
    defineProps({
        // 背景颜色
        bg: {
            type: String,
            default: '#efefef',
        },
        // 宽度
        width: {
            type: Number,
            required: true,
        },
        // 高度
        height: {
            type: Number,
            required: true,
        },
        // 动画
        animated: {
            type: Boolean,
            default: false,
        },
        // 淡出淡入
        fade: {
            type: Boolean,
            default: false,
        },
    })
</script>
<template>
    <div class="xtx-skeleton"  :style="{ width: width + 'px', height: height + 'px' }" :class="{ shan: animated, fade: fade }">
        <!-- 1 盒子-->
        <div class="block"></div>
        <!-- 2 闪效果 xtx-skeleton 伪元素 --->
    </div>
</template>
<style scoped lang="less">
 .xtx-skeleton {
        display: inline-block;
        position: relative;
        overflow: hidden;
        vertical-align: middle;
        .block {
            width: 100%;
            height: 100%;
            border-radius: 2px;
        }
    }
    .shan {
        &::after {
            content: '';
            position: absolute;
            animation: shan 1.5s ease 0s infinite;
            top: 0;
            width: 50%;
            height: 100%;
            background: linear-gradient(to left, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, 0.3) 50%, rgba(255, 255, 255, 0) 100%);
            transform: skewX(-45deg);
        }
    }
    @keyframes shan {
        0% {
            left: -100%;
        }
        100% {
            left: 120%;
        }
    }    .fade {
        animation: fade 1s linear infinite alternate;
    }
    @keyframes fade {
        from {
            opacity: 0.2;
        }
        to {
            opacity: 1;
        }
    }
</style>

父组件使用

如果指定内容还未加载出来就会显示骨架屏

       <template v-else>
          <Skeleton
            :width="60"
            :height="18"
            style="margin-right: 5px"
            bg="rgba(255,255,255,0.2)"
            animated
          />
          <Skeleton
            :width="50"
            :height="18"
            bg="rgba(255,255,255,0.2)"
            animated
          />
        </template>


效果展示

chrome-capture-2022-7-23.gif

2.轮播图封装

<template>
  <div class="center">
    <div class="my-carousel" @mouseenter="stop" @mouseleave="start">
      <ul class="carousel-body">
        <li
          v-for="(item, i) in findBannerList"
          :key="item.id"
          class="carousel-item"
          :class="{ fade: index === i }"
        >
          <RouterLink to="/">
            <img :src="item.imgUrl" alt="图片" />
          </RouterLink>
        </li>
      </ul>
      <a @click="clickFn(-1)" href="javascript:;" class="carousel-btn prev"
        ><i class="iconfont icon-angle-left"></i
      ></a>
      <a @click="clickFn(1)" href="javascript:;" class="carousel-btn next"
        ><i class="iconfont icon-angle-right"></i
      ></a>
      <div class="carousel-indicator">
        <span
          @click="active(i)"
          v-for="(item, i) in findBannerList"
          :key="i"
          :class="{ active: index === i }"
        ></span>
      </div>
    </div>
  </div>
</template>
<script>
import { onUnmounted, ref, watch } from "vue";
export default {
  name: "Carousel",
  props: {
    findBannerList: {
      type: Array,
      default: () => [],
    },
    autoplay: {
      type: Boolean,
      default: true,
    },
    duration: {
      type: Number,
      default: 3,
    },
  },
  setup(props) {
    const index = ref(0);
    // 定义一个常量存储定时器
    const timer = ref(null);
    // 定时器方法,实现自动轮播效果
    const autoplayFn = () => {
      // 防抖,防止多次触发定时器
      clearInterval(timer.value);
      timer.value = setInterval(() => {
        index.value += 1;
        if (index.value >= props.findBannerList.length) {
          index.value = 0;
        }
      }, props.duration * 1000);
    };
    // 侦听器,根据接口返回的数据与传递的相关属性参数 autoplay 开启轮播
    // 监听返回的数据的长度,当长度大于1的时候并且 autoplay 的为 true 的时候开启轮播
    watch(
      () => props.findBannerList,
      () => {
        if (props.findBannerList.length > 1 && props.autoplay) {
          autoplayFn();
        }
      }
    );
    // 鼠标移入轮播图,停止自动播放
    const stop = () => {
      if (timer.value) clearInterval(timer.value);
    };
    // 鼠标移出轮播图,开启定时器
    const start = () => {
      if (props.findBannerList.length > 1 && props.autoplay) {
        autoplayFn();
      }
    };
    // 点击轮播图上的左右按钮,切换轮播图,通过传递进来的参数,决定轮播图往左往右
    const clickFn = (e) => {
      index.value += e;
      if (index.value >= props.findBannerList.length) {
        index.value = 0;
      }
      if (index.value < 0) {
        index.value = props.findBannerList.length - 1;
      }
    };
    // 点击指示器(轮播图底下的小点)切换轮播图
    const active = (e) => {
      index.value = e;
    };
    // 组件销毁的时候情书定时器,避免性能损耗
    onUnmounted(() => {
      if (timer.value) clearInterval(timer.value);
    });
    return { index, stop, start, clickFn, active };
  },
};
</script>


<style scoped lang="less">
li {
  list-style: none;
}
.center {
  margin: 0 auto;
  width: 500px;
  height: 300px;
}
.my-carousel {
  width: 100%;
  height: 100%;
  min-width: 300px;
  min-height: 150px;
  position: relative;
  margin: 0 auto;
  .carousel {
    &-body {
      width: 100%;
      height: 100%;
    }
    &-item {
      width: 100%;
      height: 100%;
      position: absolute;
      left: 0;
      top: 0;
      opacity: 0;
      transition: opacity 0.5s linear;
      &.fade {
        opacity: 1;
        z-index: 1;
      }
      img {
        width: 100%;
        height: 100%;
      }
    }
    &-indicator {
      position: absolute;
      left: 0;
      bottom: 20px;
      z-index: 2;
      width: 100%;
      text-align: center;
      span {
        display: inline-block;
        width: 12px;
        height: 12px;
        background: rgba(0, 0, 0, 0.2);
        border-radius: 50%;
        cursor: pointer;
        ~ span {
          margin-left: 12px;
        }
        &.active {
          background: #fff;
        }
      }
    }
    &-btn {
      width: 44px;
      height: 44px;
      background: rgba(0, 0, 0, 0.2);
      color: #fff;
      border-radius: 50%;
      position: absolute;
      top: 228px;
      z-index: 2;
      text-align: center;
      line-height: 44px;
      opacity: 0;
      transition: all 0.5s;
      &.prev {
        left: 20px;
      }
      &.next {
        right: 20px;
      }
    }
  }
  &:hover {
    .carousel-btn {
      opacity: 1;
    }
  }
}
</style> 

父组件使用

   <XtxCarousel :slides="home.list" style="height: 500px" auto-play />

效果展示

chrome-capture-2022-7-23 (1).gif

3.查看全部按钮

<script lang="ts" setup name="XtxMore">
import { defineProps } from 'vue'
defineProps({
  path: {
    type: String,
    default: '/',
  },
})
</script>
<template>
  <RouterLink :to="path" class="xtx-more">
    <span><slot>查看全部</slot></span>
    <i class="iconfont icon-angle-right"></i>
  </RouterLink>
</template>

<style scoped lang="less">
.xtx-more {
  margin-bottom: 2px;
  span {
    font-size: 16px;
    vertical-align: middle;
    margin-right: 4px;
    color: #999;
  }
  i {
    font-size: 14px;
    vertical-align: middle;
    position: relative;
    top: 2px;
    color: #ccc;
  }
  &:hover {
    span,
    i {
      color: @xtxColor;
    }
  }
}
</style>

父组件使用

  <XtxMore to="/" />

效果展示

image.png

4.面包屑

文件创建(需创建两个文件,以便面包屑的嵌套)

image.png

item.vue代码

<script lang="ts" setup name="XtxBreadItem">
import { inject } from 'vue'

defineProps({
  to: {
    type: String,
  },
})

const separator = inject('separator')
</script>
<template>
  <div class="xtx-bread-item">
    <!--
            如果to存在 有值 我们就渲染一个router-link标签
            如果to不存在  那就渲染一个span标签
        -->
    <router-link v-if="to" :to="to"><slot /></router-link>
    <span v-else><slot /></span>
    <!-- 分隔符 -->
    <i v-if="separator">{{ separator }}</i>
    <i v-else class="iconfont icon-angle-right"></i>
  </div>
</template>

<style lang="less" scoped>
.xtx-bread-item {
  i {
    margin: 0 6px;
    font-size: 10px;
  }
  // 最后一个i隐藏
  &:nth-last-of-type(1) {
    i {
      display: none;
    }
  }
}
</style>

index.vue代码

<script lang="ts" setup name="XtxBread">
// 分隔符数据是位于Bread组件中 而对于分隔符数据的使用是在底层的组件中使用
// provide/inject
import { provide } from 'vue'

const props = defineProps({
  separator: {
    type: String,
    default: '',
  },
})

// 为底层组件提供数据
provide('separator', props.separator)
</script>
<template>
  <div class="xtx-bread">
    <slot />
  </div>
</template>
<style scoped lang="less">
.xtx-bread {
  display: flex;
  padding: 25px 10px;
  &-item {
    a {
      color: #666;
      transition: all 0.4s;
      &:hover {
        color: @xtxColor;
      }
    }
  }
  i {
    font-size: 12px;
    margin-left: 5px;
    margin-right: 5px;
    line-height: 22px;
  }
}
</style>

父组件使用

      <XtxBread>
        <XtxBreadItem to="/">首页</XtxBreadItem>
        <XtxBreadItem>购物车</XtxBreadItem>
      </XtxBread>

效果展示

image.png

朋友们,下期再见!!!

image.png