components in vue3

168 阅读1分钟

重置样式 .less

// 重置样式 .less
* {
  box-sizing: border-box;
 }
 
 html {
   height: 100%;
   font-size: 14px;
 }
 body {
   height: 100%;
   color: #333;
   min-width: 1240px;
   font: 1em/1.4 'Microsoft Yahei', 'PingFang SC', 'Avenir', 'Segoe UI', 'Hiragino Sans GB', 'STHeiti', 'Microsoft Sans Serif', 'WenQuanYi Micro Hei', sans-serif
 }
 
 ul,
 h1,
 h3,
 h4,
 p,
 dl,
 dd {
   padding: 0;
   margin: 0;
 }
 
 a {
   text-decoration: none;
   color: #333;
   outline: none;
 }
 
 i {
   font-style: normal;
 }
 
 input[type="text"],
 input[type="search"],
 input[type="password"], 
 input[type="checkbox"]{
   padding: 0;
   outline: none;
   border: none;
   -webkit-appearance: none;
   &::placeholder{
     color: #ccc;
   }
 }
 
 img {
   max-width: 100%;
   max-height: 100%;
   vertical-align: middle;
 }
 
 ul {
   list-style: none;
 }
 
 #app {
   background: #f5f5f5;
   user-select: none;
 }
 
 .container {
   width: 1240px;
   margin: 0 auto;
   position: relative;
 }
 
 .ellipsis {
   white-space: nowrap;
   text-overflow: ellipsis;
   overflow: hidden;
 }
 
 .ellipsis-2 {
   word-break: break-all;
   text-overflow: ellipsis;
   display: -webkit-box;
   -webkit-box-orient: vertical;
   -webkit-line-clamp: 2;
   overflow: hidden;
 }
 
 .fl {
   float: left;
 }
 
 .fr {
   float: right;
 }
 
 .clearfix:after {
   content: ".";
   display: block;
   visibility: hidden;
   height: 0;
   line-height: 0;
   clear: both;
 }

轮播图组件 vue3

<template>
  <!-- 轮播图封装 -->
  <div class="box" style="height: 500px">
    <div class="xtx-slider" @mouseenter="clearTimer" @mouseleave="startTimer">
      <!-- 图片列表 -->
      <ul class="slider-body">
        <!--
          fade: 当fade类名存在 当前图片就显示 不存在就不显示
         -->
        <li class="slider-item" v-for="(item, i) in sliders" :key="i" :class="{ fade: curIndex === i }" >
          <img :src="item.imgUrl" alt="" />
        </li>
      </ul>
      <!-- 圆圈切换按钮 -->
      <div class="slider-indicator">
        <span v-for="(item, index) in sliders" :key="index" @click="curIndex = index" :class="{ active: curIndex === index }"
        ></span>
      </div>
    </div>
  </div>
</template>

<script>
/**
   目标:点击圆圈按钮 实现对应图片的切换
   思路:
    1. 图片和圆圈按钮数量是一样的 下标值是对应的
    2. 记录一下当前点击的是哪一项
    3. 需要根据记录下来的下标值 去配合:class 控制fade这个类名是否应该显示
 */

/**
    目标:图片的自动轮播功能
    思路:哪个数据变化决定了图片切换? 从之前手动修改curIndex的值 变成一个自动修改 每隔几秒修改一下 计时器  setInterval
 */

/**
    目标:鼠标移入暂停播放 鼠标移除再次开启
    思路:暂停 - 清除定时器  定时器id  开启 - 再执行一次定时器
 */
import { onMounted, onUnmounted, ref } from 'vue'
export default {
  name: 'XtxSlider',
  props: {
    // 数据列表
    sliders: {
      type: Array,
      default: () => {
        return []
      }
    },
    autoPlay: {
      type: Boolean,
      default: true
    }
  },
  setup (props) {
    const curIndex = ref(0)
    // 声明一个存放定时器的数据
    const timer = ref(null)
    function clearTimer () {
      clearInterval(timer.value)
    }
    function startTimer () {
      // 开启定时器  每隔2s中修改一下curIndex的值
      initLoop()
    }

    function initLoop () {
      if (!props.autoPlay) {
        return false
      }
      // 开启定时器  每隔2s中修改一下curIndex的值
      timer.value = window.setInterval(() => {
        // 最大能到多大
        // 图片总数为4 length - 1为3 只要我发现你大于3了
        // 我就会重新赋值为 0 ,永远不能到达4 最大只能等于3
        curIndex.value++
        if (curIndex.value > props.sliders.length - 1) {
          curIndex.value = 0
        }
      }, 2000)
    }
    onMounted(() => {
      initLoop()
    })
    onUnmounted(() => {
      // 清理工作
      clearInterval(timer.value)
    })
    return {
      curIndex,
      clearTimer,
      startTimer
    }
  }
}
</script>

<style scoped lang='less'>
.xtx-slider {
  width: 100%;
  height: 100%;
  min-width: 300px;
  min-height: 150px;
  position: relative;
  .slider {
    &-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 {
    .slider-btn {
      opacity: 1;
    }
  }
}
</style>

骨架屏封装 vue3

<template>
  <!-- 骨架屏封装 -->
  <div class="xtx-skeleton" :style="{ width, height }" :class="{ shan: animated }">
    <!-- 1 盒子-->
    <div class="block" :style="{ backgroundColor: bg }"></div>
    <!-- 2 闪效果 xtx-skeleton 伪元素 --->
  </div>
</template>
<script>
export default {
  name: 'XtxSkeleton',
  // 使用的时候需要动态设置 高度,宽度,背景颜色,是否闪下
  props: {
    bg: {
      type: String,
      default: '#efefef'
    },
    width: {
      type: String,
      default: '100px'
    },
    height: {
      type: String,
      default: '100px'
    },
    animated: {
      type: Boolean,
      default: true // 控制动画开启
    }
  }
}
</script>
<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%;
  }
}
</style>

面包屑封装 vue3

<XtxBread separator=">">
    <XtxBreadItem to="/first">首页</XtxBreadItem>
    <XtxBreadItem to="/">分类页</XtxBreadItem>
    <XtxBreadItem>倒数页</XtxBreadItem>
    <XtxBreadItem to="/">最后页</XtxBreadItem>
</XtxBread>
<template>
  <div class="xtx-bread">
    <slot></slot>
  </div>
</template>

<script>
import { provide } from 'vue'
export default {
  name: 'XtxBread',
  props: {
    separator: {
      type: String
    }
  },
  setup (props) {
    provide('separator', props.separator)
  }
}
</script>
<style lang='less'  scoped>
.xtx-bread {
  display: flex;
  /deep/.xtx-bread-item:last-child {
    span {
      display: none;
    }
  }
}
</style>

<template>
  <div class="xtx-bread-item">
    <!-- 传了to,就用router-link包含,使其可以跳转 -->
    <router-link v-if="to" :to="to">
      <slot></slot>
    </router-link>

    <template v-else>
      <slot></slot>
    </template>

    <span>{{ separator }}</span>
  </div>
</template>

<script>
import { inject } from 'vue'
export default {
  name: 'XtxBreadItem',
  props: {
    to: {
      type: String
    }
  },
  setup () {
    const separator = inject('separator')
    return { separator }
  }
}
</script>
<style lang="less" scoped></style>

放大镜封装 vue3

Snipaste_2022-07-05_11-37-50.jpg

<template>
  <div class="goods-image">
    <!-- 定位的大图 -->
    <div
      class="large"
      :style="[
        {
          backgroundImage: `url(${imageList[curIndex]})`,
          backgroundPositionX: positionX + 'px',
          backgroundPositionY: positionY + 'px',
        },
      ]"
      v-show="showFlag"
    ></div>
    <div class="middle" ref="target">
      <img :src="imageList[curIndex]" alt="" />
      <!-- 蒙层容器 -->
      <div
        class="layer"
        :style="{ left: left + 'px', top: top + 'px' }"
        v-show="showFlag"
      ></div>
    </div>
    <!-- 小图 -->
    <ul class="small">
      <li
        v-for="(img, i) in imageList"
        :key="i"
        @mouseenter="mouseEnterFn(i)"
        :class="{ active: i === curIndex }"
      >
        <img :src="img" alt="" />
      </li>
    </ul>
  </div>
</template>
<script>
/**
 * 交互思路分析:
 *   1. 基于鼠标移入事件  mouseenter
 *   2. 鼠标移入哪个就把哪个的下标值记录一下  然后通过下标值去imageList中去取值 把取到的值放到src渲染即可
 */
import { ref, watch } from 'vue'
import { useMouseInElement } from '@vueuse/core'
export default {
  name: 'XtxImageView',
  props: {
    imageList: {
      type: Array,
      // 给复杂类型 对象/数组 给默认值,需要通过工厂函数提供
      default: () => {
        return []
      }
    }
  },
  setup () {
    // 实现鼠标移入交互
    const curIndex = ref(0)
    function mouseEnterFn (i) {
      curIndex.value = i
    }

    // 实现放大镜效果
    const target = ref(null)
    // 控制是否显示 false代表不显示 (直接使用isOutside 会有闪动bug)
    const showFlag = ref(false)
    // elementX:相较于我们盒子左侧的距离  refObj
    // elementY:相较于盒子顶部的距离 refObj
    // isOutSide: 鼠标是否在盒子外部  true代表在外部  refObj
    const { elementX, elementY, isOutside } = useMouseInElement(target)

    // 实现我们滑块跟随鼠标移动的交互效果
    const left = ref(0)
    const top = ref(0)
    const positionX = ref(0)
    const positionY = ref(0)
    watch([elementX, elementY, isOutside], () => {
      showFlag.value = !isOutside.value

      // 只有进入到容器中才开始做移动判断
      if (isOutside.value) {
        return false
      }
      // 根据鼠标的坐标变化控制我们滑块的位移 left top值
      // 1. 控制滑块最大的可移动范围
      if (elementX.value > 300) {
        left.value = 200
      }
      if (elementX.value < 100) {
        left.value = 0
      }
      // 2. 横向有效移动范围内的逻辑
      if (elementX.value < 300 && elementX.value > 100) {
        left.value = elementX.value - 100
      }

      if (elementY.value > 300) {
        top.value = 200
      }
      if (elementY.value < 100) {
        top.value = 0
      }
      // 2. 横向有效移动范围内的逻辑
      if (elementY.value < 300 && elementY.value > 100) {
        top.value = elementY.value - 100
      }

      // 控制背景大图的移动 (背景图的移动 是跟着 滑块的移动走的)
      // 1.鼠标的移动的方向和大图的方向是相反的 (正负)
      // 2.鼠标每移动一个像素 大图背景移动俩个像素 (x2)
      positionX.value = -left.value * 2
      positionY.value = -top.value * 2
    })
    /**
     * 1. 换算关系 难点
     * 2. 使用工具函数的时候 返回的数据的类型  ref类型  refObj.value
     * 3. 在实现一些和样式有关的交互 一定要保证css单位值是有效的
     */
    return {
      mouseEnterFn,
      curIndex,
      target,
      elementX,
      elementY,
      left,
      top,
      positionX,
      positionY,
      showFlag
    }
  }
}
</script>
<style scoped lang="less">
.goods-image {
  width: 480px;
  height: 400px;
  position: relative;
  display: flex;
  .middle {
    width: 400px;
    height: 400px;
    background: #f5f5f5;
  }
  .large {
    position: absolute;
    top: 0;
    left: 412px;
    width: 400px;
    height: 400px;
    z-index: 500;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
    background-repeat: no-repeat;
    // 背景图:盒子的大小 = 2:1  将来控制背景图的移动来实现放大的效果查看 background-position
    background-size: 800px 800px;
    background-color: #f8f8f8;
  }
  .layer {
    width: 200px;
    height: 200px;
    background: rgba(0, 0, 0, 0.2);
    // 绝对定位 然后跟随咱们鼠标控制lefttop属性就可以让滑块移动起来
    left: 0;
    top: 0;
    position: absolute;
  }
  .small {
    width: 80px;
    li {
      width: 68px;
      height: 68px;
      margin-left: 12px;
      margin-bottom: 15px;
      cursor: pointer;
      &:hover,
      &.active {
        border: 2px solid skyblue;
      }
    }
  }
}
</style>

城市选择组件 vue3

Snipaste_2022-07-06_19-28-33.jpg

<template>
  <div class="xtx-city">
    <div class="select" @click="activeCity = !activeCity">
      <span class="placeholder">{{
        changeResult.resultAddress
          ? changeResult.resultAddress
          : '请选择配送地址'
      }}</span>
      <span class="value"></span>
      <i class="iconfont icon-angle-down"></i>
    </div>
    <div class="option" v-if="activeCity">
      <span
        class="ellipsis"
        @click="itemHandle(i)"
        v-for="i in dataList"
        :key="i"
      >
        {{ i.name }}
      </span>
    </div>
  </div>
</template>

<script>
/*
开始点击显示对话框
点击第三层关闭对话框
展示选中的数据
*/
import axios from 'axios'
import { ref, reactive } from 'vue'
export default {
  name: 'City',
  setup () {
    const dataList = ref([])
    const dataListCopy = ref([])
    const activeCity = ref(false)
    // ajax获取城市数据列表
    async function loadCityList () {
      const res = await axios({
        method: 'get',
        // 城市接口
        url: 'https://.....'
      })
      dataList.value = res.data
      // 拷贝一份
      dataListCopy.value = res.data
      // console.log(dataList)
    }
    loadCityList()

    // 3. 交互选择
    const changeResult = reactive({
      provinceCode: '', // 省code
      provinceName: '', // 省名称
      cityCode: '', // 城市code
      cityName: '', // 城市名称
      countyCode: '', // 地区code
      countyName: '', // 地区名
      resultAddress: '' // 最终结果地址
    })

    // 点击选项
    const itemHandle = (item) => {
      dataList.value = item.areaList
      // 点击第三层(区)关闭对话框
      if (item.level === 0) {
        changeResult.provinceCode = item.code
        changeResult.provinceName = item.name
      }
      if (item.level === 1) {
        changeResult.cityCode = item.code
        changeResult.cityName = item.name
      }
      if (item.level === 2) {
        changeResult.countyCode = item.code
        changeResult.countyName = item.name
        changeResult.resultAddress =
          changeResult.provinceName +
          changeResult.cityName +
          changeResult.countyName
        activeCity.value = false
        // 解决二次点击对话框选项为空的问题
        dataList.value = dataListCopy.value
      }
    }
    return { dataList, activeCity, itemHandle, changeResult }
  }
}
</script>

<style scoped lang="less">
.xtx-city {
  display: inline-block;
  position: relative;
  z-index: 400;
  margin-left: 10px;
  .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>