vue 常见组件封装

1,401 阅读3分钟

vue 常见组件封装

实际开发的时候,为了减少重复造轮子,提高工作效率,节省开发时间成本, 免不了会使用ui组件库,比如在web前端很受欢迎的element-ui。 但有的时候,我们需要在原组件的基础上做些改造,比如一个image组件, 我们需要统一在图片加载失败的时候展示的特定图,每次使用组件都加一遍, 麻烦耗时,关键是维护成本高,当需要更新这个加载出错的图片时, 得再次一个个去找到使用该组件的地方修改。

再例如自定义分页组件也很常见,组件的样式,默认支持的每页数目, 封装之后再用,在可维护性和开发效率上都很有好处。

常用组件:在项目多处重复,在多处场景调用的组件,可以封装为公共组件 我们在项目开发过程中,某个界面的详情,可以在系统各个地方调用,这样的组件需要我们封装为公共的业务组件供系统使用

vue 的组件封装,有两种方式:

  1. 普通公共组件封装:父子通信,v-model
  2. 通过渲染函数封装组件:h 函数与 render 函数运用

共性:

vue 的插槽使用:

  1. 普通插槽
  2. 具名插槽
  3. 作用域插槽

前置工作:创建组件 + 全局注册

例:

1. 轮播图组件

<!-- 轮播图组件 -->
<script lang="ts" setup name="XtxCarousel">import { BannerItem } from '@/types/data';
import { onBeforeMount, onMounted, ref } from 'vue';

const { slides, autoplay=false, duration=1000 } = defineProps<{
  slides: BannerItem[], // 轮播图内容
  autoplay?: boolean, // 自动播放
  duration?: number // 控制时长
}>()
// 是否开启自动播放
onMounted(() => {
  if(autoplay) {
    start()
  }
})
// 控制高亮的下标
const num = ref(0)
// 点击右箭头切换下一张
const next = () => {
  // console.log('测试')
  num.value ++
  if(num.value > slides.length - 1) num.value = 0
}
// 点击左箭头切换上一张
const prev = () => {
  // console.log('测试')
  num.value --
  if(num.value < 0) num.value = slides.length - 1
}
// 鼠标经过小圆点切换图片
const mOver = (idx: number) => {
  num.value = idx 
}
// 轮播图自动播放
let timer = -1
const start = () => {
  if(autoplay) {
    clearTimeout(timer)
    timer = window.setInterval(() => {
    next()
  }, duration)
  }
}
// 挂载组件之前调用
onBeforeMount(() => {
  stop()
})
// 鼠标离开继续播放
const stop = () => {
  clearTimeout(timer)
}

</script>

<template>
<!-- mouseenter 鼠标进入轮播图停止播放 mouseleave 鼠标离开轮播图开启播放 -->
  <div class="xtx-carousel" @mouseenter="stop" @mouseleave="start">
    <ul class="carousel-body">
      <!-- 图片 -->
      <!-- fade: 控制图片显示隐藏 -->
      <li class="carousel-item" :class="{fade: index === num}"  v-for="(item, index) in slides" :key="item.id">
          <img
            :src="item.imgUrl"
            alt=""
          />
      </li>
    </ul>
    <!-- 左右箭头 -->
    <a @click="prev" href="javascript:;" class="carousel-btn prev"><i class="iconfont icon-angle-left"></i
    ></a>
    <!-- 右箭头 -->
    <a @click="next" href="javascript:;" class="carousel-btn next"
      ><i class="iconfont icon-angle-right"></i
    ></a>
    <div class="carousel-indicator">
      <!-- 小圆点 -->
      <span @mouseover="mOver(index)" :class="{active: index === num}" v-for="(item, index) in 5" :key="index"></span>
    </div>
  </div>
</template>

<style scoped lang="less">
.xtx-carousel {
  width: 100%;
  height: 100%;
  min-width: 300px;
  min-height: 150px;
  position: relative;
  .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>

使用

// slides 为轮播图图片数据
<Carousel autoplay :slides="home.list" style="height: 500px" />

2. 骨架屏组件

<!-- 骨架屏组件 -->
<script setup lang="ts">
// 属性:宽、高、背景、是否有闪动画、是否有淡入淡出动画
const { width=100, height=100, animated=false, fade=false} = defineProps<{
  width: number,
  height: number,
  bgc?: string,
  animated?: boolean, // 淡入淡出动画
  fade?: boolean // 闪动画
}>()
</script>

<template>
  <div
    class="xtx-skeleton"
    :class="{fade, shan: animated}"
    :style="{width: width + 'px', height: height + 'px'}"
  >
    <!-- 1 盒子-->
    <div class="block" :style="{ backgroundColor: '#ccc' }">
    </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>

使用

<Skeleton :width="100" fade animated :height="300"></Skeleton>

3. 复选框组件

<!-- 复选框组件 -->
<script lang="ts" setup name="XtxCheckbox">
import { ref } from 'vue';
const {modelValue} = defineProps<{
  modelValue: boolean
}>()
const emit = defineEmits<{
  (event: 'update:modelValue', value:boolean): void
}>()
const change1 = () => {
  emit('update:modelValue', false)
  console.log(modelValue)
  console.log('111')
}
const change2 = () => {
  emit('update:modelValue', true)
  console.log(modelValue)
  console.log('111')
}
</script>

<template>
  <div class="xtx-checkbox">
    <i v-if="modelValue" class="iconfont icon-checked" @click="change1"
    ></i>
    <i @click="change2" v-else class="iconfont icon-unchecked"
    ></i>
    <span><slot></slot></span>
  </div>
</template>

<style scoped lang="less">
.xtx-checkbox {
  display: inline-block;
  margin-right: 2px;
  .icon-checked {
    color: @xtxColor;
    ~ span {
      color: @xtxColor;
    }
  }
  i {
    position: relative;
    top: 1px;
  }
  span {
    margin-left: 2px;
  }
}
</style>

使用

<Checkbox v-model="isAgree">学习</Checkbox>

4. 对话框组件

<!-- 对话框组件 -->
<script lang="ts" setup name="XtxDialog">
import {onClickOutside} from '@vueuse/core'
import { ref } from 'vue';
const {title='对话框', modelValue=true} = defineProps<{
  title: string,
  modelValue: boolean
}>()
const emit = defineEmits<{
  (event: 'update:modelValue', value: boolean): void
}>()
const target = ref(null)
// 关闭弹层
const close = () => {
  emit('update:modelValue', false)
  // console.log('11111')
}
emit('update:modelValue', modelValue)
onClickOutside(target, () => {
  close()
})
</script>
<template>
  <div class="xtx-dialog" v-show="modelValue">
    <div class="wrapper" ref="target">
      <div class="header">
        <h3>{{title}}</h3>
        <a @click="modelValue = false" href="JavaScript:;" class="iconfont icon-close-new"></a>
      </div>
      <div class="body">
        <slot></slot>
      </div>
      <div class="footer">
        <!-- 具名插槽 -->
        <slot name="footer">
        </slot>
      </div>
    </div>
  </div>
</template>

<style scoped lang="less">
.xtx-dialog {
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  z-index: 8887;
  background: rgba(0, 0, 0, 0.5);
  // background: rgba(0, 0, 0, 0);
  // &.fade {
  //   transition: all 0.4s;
  //   background: rgba(0, 0, 0, 0.5);
  // }
  .wrapper {
    width: 600px;
    background: #fff;
    border-radius: 4px;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    // transform: translate(-50%, -60%);
    // opacity: 0;
    // &.fade {
    //   transition: all 0.4s;
    //   transform: translate(-50%, -50%);
    //   opacity: 1;
    // }
    .body {
      padding: 20px 40px;
      font-size: 16px;
      .icon-warning {
        color: @priceColor;
        margin-right: 3px;
        font-size: 16px;
      }
    }
    .footer {
      text-align: center;
      padding: 10px 0 30px 0;
    }
    .header {
      position: relative;
      height: 70px;
      line-height: 70px;
      padding: 0 20px;
      border-bottom: 1px solid #f5f5f5;
      h3 {
        font-weight: normal;
        font-size: 18px;
      }
      a {
        position: absolute;
        right: 25px;
        top: 25px;
        font-size: 24px;
        width: 20px;
        height: 20px;
        line-height: 20px;
        text-align: center;
        color: #999;
        &:hover {
          color: #666;
        }
      }
    }
  }
}
</style>
<Dialog title="title" v-model="visible">
        <p>这是对话框的内容</p>
        底部两个按钮
        <template #footer>
          <Button @click="visible = false" type="gray" style="margin-right: 20px">取消</Button>
          <Button type="primary">确认</Button>
        </template>
 </Dialog>

5. 消息提示组件

<script lang="ts" setup name="XtxMessage">
import { MessageType } from './type';
defineProps<{
  type: MessageType // 消息提示类型
  text: string // 消息提示文字
  duration:number // 消息提示时间
}>()

// 定义一个对象,包含三种情况的样式,对象key就是类型字符串
const style = {
  warning: {
    icon: 'icon-warning',
    color: '#E6A23C',
    backgroundColor: 'rgb(253, 246, 236)',
    borderColor: 'rgb(250, 236, 216)',
  },
  error: {
    icon: 'icon-shanchu',
    color: '#F56C6C',
    backgroundColor: 'rgb(254, 240, 240)',
    borderColor: 'rgb(253, 226, 226)',
  },
  success: {
    icon: 'icon-queren2',
    color: '#67C23A',
    backgroundColor: 'rgb(240, 249, 235)',
    borderColor: 'rgb(225, 243, 216)',
  },
}
</script>

<template>
  <div class="xtx-message" :style="style[type]">
    <i class="iconfont" :class="style[type]"></i>
    <span class="text">{{text}}</span>
  </div>
</template>

<style scoped lang="less">
.xtx-message {
  width: 300px;
  height: 50px;
  position: fixed;
  z-index: 9999;
  left: 50%;
  margin-left: -150px;
  top: 25px;
  line-height: 50px;
  padding: 0 25px;
  border: 1px solid #e4e4e4;
  background: #f5f5f5;
  color: #999;
  border-radius: 4px;
  i {
    margin-right: 4px;
    vertical-align: middle;
  }
  .text {
    vertical-align: middle;
  }
}
</style>
/**
 * <template>
 * <p>{{name}}</p>
 * </template>
 * 1. 分析模板的内容得到函数
 * 2. 把 data 中的数据传给这个函数
 * 3. 变成虚拟 dom
 * 4. 挂载到页面上
 * 5. 更改 data 中的数据,得到一个新的虚拟 dom
 * 6. 新旧虚拟 dom 进行比较,再差异化更新页面。
 * 比较的过程:diff 算法
 */

// h(): 产生虚拟 dom
// render(): 把虚拟 dom 渲染真实的 dom 中
import { h, render } from 'vue'
import { Message } from './type'
// import { Message } from './type'
// 导入XtxMessage.vue组件
import XtxMessage from './XtxMessage.vue'
// 通过代码的方式去渲染它
// 参数1:创建的虚拟DOM的节点类型  比如  div h1  a  img
// 参数2:虚拟DOM拥有的属性,是一个对象
// 参数3:虚拟DOM节点的内容
// 创建一个div
// 创建一个 dom 容器, 把这个容器添加在 body 上
const div = document.createElement('div')
// body 下新建 div
document.body.appendChild(div)
// 新建类名 
div.className = 'xtx-message-container'
export default function Message(obj: Message) {
const vNode = h(XtxMessage, {type: obj.type, text: obj.text})
  console.log(vNode, 'vNode')
   // 参数1: 虚拟 dom
   // 参数2: 真实的 dom,虚拟 dom 的挂载点
  render(vNode, div)
  setTimeout(() => {
    render(null, div)
  }, obj.duration)
}

// 错误提示
Message.error = function(text: string, duration=2000) {
  Message({
    type: 'error',
    text,
    duration
  })
}
// 警告提示
Message.warning = function(text: string, duration=2000) {
  Message({
    type: 'warning',
    text,
    duration
  })
}
// 成功提示
Message.success = function(text: string, duration=2000) {
  Message({
    type: 'success',
    text,
    duration
  })
}
export type MessageType = 'success' | 'error' | 'warning'
export type Message = {
  type: MessageType // 消息提示类型
  text: string // 消息提示文字
  duration:number // 消息提示时间
}

使用

import Message from '@/components/XtxMessage'
Message.success('封装成功')

6. 城市选择组件

<!-- 城市选择组件 -->
<script lang="ts" setup name="XtxCity">

import { ref, watch } from 'vue'
// import { onClickOutside } from '@vueuse/core'
import { AreaList, CityResult } from '@/types/data'
import request from '@/utils/request'
// 省列表
const cacheList = ref<AreaList[]>([])
const cityList = ref<AreaList[]>([])
// https://yjy-oss-files.oss-cn-zhangjiakou.aliyuncs.com/tuxian/area.json 省市区
// 1. 取回所有的地区数据,显示弹层
const getCityList = async () => {
  const res = await request.get('https://yjy-oss-files.oss-cn-zhangjiakou.aliyuncs.com/tuxian/area.json')
  // console.log(res)
  cacheList.value = res.data
  cityList.value = res.data
}
// 选择城市
const changeResult = ref({
  provinceCode: '',
  provinceName: '',
  cityCode: '',
  cityName: '',
  countyCode: '',
  countyName: ''
})
const selectCity = (city: AreaList) => {
  console.log(city) // 所有的省直辖市列表
  // 如果选择了省
  if(city.level === 0) {
    changeResult.value.provinceName = city.name
    changeResult.value.provinceCode = city.code
    cityList.value = city.areaList
    console.log(changeResult.value.provinceName)
  }
  // 如果选择了市
  if(city.level === 1) {
    changeResult.value.cityName = city.name
    changeResult.value.cityCode = city.code
    cityList.value = city.areaList
  }
  // 如果选择了区
  if(city.level === 2) {
    changeResult.value.countyName = city.name
    changeResult.value.countyCode = city.code
    cityList.value = city.areaList
    console.log(changeResult.value.countyName)
    
  }
}
getCityList()
</script>
<template>
{{changeResult.provinceName}}
{{changeResult.cityName}}
{{changeResult.countyName}}
<div class="xtx-city" ref="target">
    <div class="option">
      <span class="ellipsis" @click="selectCity(item)" v-for="item in cityList" :key="item.code">
        {{ item.name }}
        </span>
    </div>
  </div>>
</template>

<style scoped lang="less">
.xtx-city {
  display: inline-block;
  position: relative;
  z-index: 400;
  .select {
    border: 1px solid #e4e4e4;
    height: 30px;
    padding: 0 5px;
    line-height: 28px;
    cursor: pointer;
    &.active {
      background: #fff;
    }
    .placeholder {
      color: #999;
    }
    .value {
      color: #666;
      font-size: 12px;
    }
    i {
      font-size: 12px;
      margin-left: 5px;
    }
  }
  .option {
    width: 542px;
    border: 1px solid #e4e4e4;
    position: absolute;
    left: 0;
    top: 29px;
    background: #fff;
    min-height: 30px;
    line-height: 30px;
    display: flex;
    flex-wrap: wrap;
    padding: 10px;
    > span {
      width: 130px;
      text-align: center;
      cursor: pointer;
      border-radius: 4px;
      padding: 0 3px;
      &:hover {
        background: #f5f5f5;
      }
    }
  }
}
</style>

使用

<City></City>