vue 造轮子 自定义组件 持续更新~~~~

如何把自定义组件,注册为全局组件,指令注册为全局指令

vue3.x版本注册全局组件和指令
一般项目的src/comonents/目录下,新建一个index.js

// 扩展vue原有的功能:全局组件,自定义指令,挂载原型方法,注意:没有全局过滤器。
// 这就是插件
// vue2.0插件写法要素:导出一个对象,有install函数,默认传入了Vue构造函数,Vue基础之上扩展
// vue3.0插件写法要素:导出一个对象,有install函数,默认传入了app应用实例,app基础之上扩展

import XtxSkeleton from './xtx-skeleton.vue'
import XtxCarousel from './xtx-carousel.vue'
import XtxMore from './xtx-more.vue'

export default {
  install(app) {
    // 在app上进行扩展,app提供 component directive 函数
    // 如果要挂载原型 app.config.globalProperties 方式
    app.component(XtxSkeleton.name, XtxSkeleton)
    app.component(XtxCarousel.name, XtxCarousel)
    app.component(XtxMore.name, XtxMore)

    // 把指令注册为全局指令的一个方法
    defineDirective(app)
  }

}

// 定义指令 
import defaultImg from '@/assets/images/200.png'
const defineDirective = (app) => {
  // 1. 图片懒加载 指令  v-lazy
  // 原理,先存储图片地址不能再 src 上,当图片进入可视区,将你存储图片的地址设置给图片元素即可
  app.directive('lazy', {
    // 操作 DOM 要在 mounted
    mounted(el, bingding) {
      // 观察 使用 指令的 元素,是否进入可视区
      // 创建观察对象
      const observer = new IntersectionObserver(([{ isIntersecting }]) => {
        if (isIntersecting) {
          // 停止 观察 
          observer.unobserve(el)

          // 处理图片加载失败
          el.onerror = () => {
            // 加载失败,设置默认图
            el.src = defaultImg
          }
          // onload 是加载成功

          // 将指令的值, 设置给 el 的 src 属性 ,bingding.value 就是指令的值
          el.src = bingding.value
          console.log(el, '图片进入可视区');
        }
      }, { threshold: 0 })
      // 观察 那个 DOM  开始观察
      observer.observe(el)
    }
  })
}

main.js中

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
// 引入  `src/components/index.js`
import ui from './components/index.js'

import 'normalize.css'
import '@/assets/styles/common.less'
// 插件的使用,在main.js使用app.use(插件)
createApp(App).use(store).use(router).use(ui).mount('#app')

vue2.x 版本
一般项目的src/comonents/目录下,新建一个index.js

// 该文件负责所有的公共的组件的全局注册   Vue.use
import PageTools from './PageTools'
import UploadExcel from './UploadExcel'
import ImageUpload from './ImageUpload'
import ScreenFull from './ScreenFull'
import ThemePicker from './ThemePicker'
import TagsView from './TagsView'
export default {
    install(Vue) {
        //  注册全局的通用栏组件对象
        Vue.component('PageTools', PageTools)
        Vue.component('UploadExcel', UploadExcel)
        Vue.component('ImageUpload', ImageUpload)
        Vue.component('ScreenFull', ScreenFull) // 注册全屏组件
        Vue.component('ThemePicker', ThemePicker)
        Vue.component('TagsView', TagsView)
    }
}
/*
    Vue.use({
        install(value){
            会自动执行这个方法,它里面的参数value 就是vue的实例
        }
    })

*/

在 main.js 中

// 导入 `src/components/index.js`
import Component from '@/components'
Vue.use(Component) // 注册自己的全局组件

vue3 动态注册 全局自定义组件

当自定义组件很多的时候,在一个个导入 注册太麻烦了

// 扩展vue原有的功能:全局组件,自定义指令,挂载原型方法,注意:没有全局过滤器。
// 这就是插件
// vue2.0插件写法要素:导出一个对象,有install函数,默认传入了Vue构造函数,Vue基础之上扩展
// vue3.0插件写法要素:导出一个对象,有install函数,默认传入了app应用实例,app基础之上扩展

// import XtxSkeleton from './xtx-skeleton.vue'
// import XtxCarousel from './xtx-carousel.vue'
// import XtxMore from './xtx-more.vue'
// import XtxBread from './xtx-bread.vue'
// import XtxBreadItem from './xtx-brand-item.vue'


// 1. 使用 require 提供的函数  context  加载某一个目录下的 所有  .vue 后缀的文件
// 2. 然后 context 会返回 一个导入 函数 importFn
// 3. importFn有一个 keys()  属性,获取所有文件的路径
// 4. 通过文件路径数组,遍历数组,在使用 importFn 根据路径导入组件对象
// 5. 导入的同时,全局注册

//require.context(目录位置,是否加载子目录,你所加载文件的匹配 这里可以是正则) 
const importFn = require.context('./', false, /\.vue$/)
// ./ 表示当前目录, 不加载子目录,加载 vue 文件

console.log(importFn.keys());

export default {
  install(app) {
    // 在app上进行扩展,app提供 component directive 函数
    // 如果要挂载原型 app.config.globalProperties 方式
    // 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)


    // 批量注册
    importFn.keys().forEach(path => {
      // 每个组件都有 export default  ,我们要拿到 default
      const component = importFn(path).default
      // 注册
      app.component(component.name, component)

    });


    // 把指令注册为全局指令的一个方法
    defineDirective(app)
  }

}

// 定义指令 
import defaultImg from '@/assets/images/200.png'
import load from '@/assets/images/load.gif'
const defineDirective = (app) => {
  // 1. 图片懒加载 指令  v-lazy
  // 原理,先存储图片地址不能再 src 上,当图片进入可视区,将你存储图片的地址设置给图片元素即可
  app.directive('lazy', {
    // 操作 DOM 要在 mounted
    mounted(el, bingding) {
      // 观察 使用 指令的 元素,是否进入可视区
      // 创建观察对象
      const observer = new IntersectionObserver(([{ isIntersecting }]) => {
        if (isIntersecting) {
          // 停止 观察 
          observer.unobserve(el)
          if (el.src === '') {
            el.src = defaultImg
            console.log('此时src 是空');
          }
          // 处理图片加载失败

          el.onerror = () => {
            // 加载失败,设置默认图
            el.src = defaultImg
            console.log('加载错误了', el);
          }
          // onload 是加载成功

          // 将指令的值, 设置给 el 的 src 属性 ,bingding.value 就是指令的值
          el.src = bingding.value
          // setTimeout(() => { el.src = bingding.value }, 400)

          // console.log(el, '图片进入可视区', bingding.value);
        }
      }, { threshold: 0 })
      // 观察 那个 DOM  开始观察
      observer.observe(el)
    }
  })
}

vue3 的轮子

at.alicdn.com/t/font_2143783_iq6z4ey5vu.css 这是项目用到的字体图标库,也可以自己弄~~~~

1.轮播图

  • sliders 表示原数组 存放数据的
  • autoPlay 表示 是否开启自动轮播,默认不开启
  • duration 表示间隔多久轮播,默认 3000
<template>
  <div class="xtx-carousel" @mouseenter="stop()" @mouseleave="start">
    <ul class="carousel-body">
      <!-- fade 谁加上 fade 就表示 谁显示 -->
      <li
        class="carousel-item"
        :class="{ fade: index === i }"
        v-for="(item, i) in sliders"
        :key="i"
      >
        <RouterLink to="/">
          <img :src="item.imgUrl" alt="" />
        </RouterLink>
      </li>
    </ul>
    <a @click="toggle(-1)" href="javascript:;" class="carousel-btn prev"
      ><i class="iconfont icon-angle-left"></i
    ></a>
    <a @click="toggle(1)" href="javascript:;" class="carousel-btn next"
      ><i class="iconfont icon-angle-right"></i
    ></a>
    <div class="carousel-indicator">
      <!-- active 表示 激活 -->
      <span
        @click="index = i"
        v-for="(item, i) in sliders"
        :key="i"
        :class="{ active: index === i }"
      ></span>
    </div>
  </div>
</template>

<script>
import { onUnmounted, ref, watch } from "vue";
export default {
  name: "XtxCarousel",
  props: {
    // 数据
    sliders: {
      type: Array,
      default: () => [],
    },
    // 是否自动轮播
    autoPlay: {
      type: Boolean,
      default: false,
    },
    // 间隔多久 轮播
    duration: {
      type: Number,
      default: 3000,
    },
  },
  setup(props) {
    // 默认显示的图片的索引
    const index = ref(0);

    // 自动轮播图的逻辑
    let timer = null;
    const autoPlayFn = () => {
      clearInterval(timer);
      // 自动播放的逻辑,每隔 多久切换索引
      timer = setInterval(() => {
        index.value++;
        console.log("执行了轮播,当前 index 值为", index.value);
        if (index.value >= props.sliders.length) {
          index.value = 0;
        }
      }, props.duration);
    };

    // 监听 slider 数据变化 ,并且 autoPlay 是 true
    watch(
      () => props.sliders,
      (newValue, oldValue) => {
        console.log("新的值", newValue);
        if (newValue.length && props.autoPlay) {
          autoPlayFn();
        }
      },
      // 默认触发
      { immediate: true }
    );

    // 2. 鼠标进入暂停 离开 开始
    const stop = () => {
      if (timer) {
        clearInterval(timer);
      }
    };
    const start = () => {
      if (props.sliders.length && props.autoPlay) {
        autoPlayFn();
      }
    };

    // 3 点击点 可以切换,上一张下一张
    const toggle = (step) => {
      const newIndex = index.value + step;

      if (newIndex > props.sliders.length - 1) {
        index.value = 0;
        return;
      }
      if (newIndex < 0) {
        index.value = props.sliders.length - 1;
        return;
      }
      //  正常
      index.value = newIndex;
    };

    // 组件卸载时,销毁定时器
    onUnmounted(() => {
      clearInterval(timer);
    });
    return { index, autoPlayFn, stop, start, toggle };
  },
};
</script>
<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>

2. 骨架屏

一般数据都是请求的接口,有后端返回数据,这肯定需要事件请求,所以为了不让 显示白屏,需要一个东西来 代替 它,显示一个等待的效果,有数据以后再隐藏掉这个等待的效果

就是 一个 块块, 占位, 有一个 斜着的 亮的 动画

  • bg:设置背景颜色,16进制
  • width:设置宽度,默认 100px
  • height:设置高度,默认100px
  • animated : 设置动画,是否显示 斜着的 亮的动画

比如,再 index.vue 使用了 这个组件,一般还会在这个 index.vue 组件的 style 节点设置 这个css 样式

  .xtx-skeleton {
    animation: fade 1s linear infinite alternate;
  }
  @keyframes fade {
    from {
      opacity: 0.2;
    }
    to {
      opacity: 1;
    }
  }
<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: false,
    },
  },
};
</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>

3. 图片懒加载

import defaultImg from '@/assets/images/200.png'
const defineDirective = (app) => {
  // 1. 图片懒加载 指令  v-lazy
  // 原理,先存储图片地址不能再 src 上,当图片进入可视区,将你存储图片的地址设置给图片元素即可
  app.directive('lazy', {
    // 操作 DOM 要在 mounted
    mounted(el, bingding) {
      // 观察 使用 指令的 元素,是否进入可视区
      // 创建观察对象
      const observer = new IntersectionObserver(([{ isIntersecting }]) => {
        if (isIntersecting) {
          // 停止 观察 
          observer.unobserve(el)
		
		// 也可以 先判断 el.src 是否为空,如果是 给他一个默认图
		
          // 处理图片加载失败
          el.onerror = () => {
            // 加载失败,设置默认图
            el.src = defaultImg
          }
          // onload 是加载成功

          // 将指令的值, 设置给 el 的 src 属性 ,bingding.value 就是指令的值
          el.src = bingding.value
          console.log(el, '图片进入可视区');
        }
      }, { threshold: 0 })
      // 观察 那个 DOM  开始观察
      observer.observe(el)
    }
  })
}

4. 组件内异步请求的懒加载

只是组件里面的 异步请求,等到 这个 容器,在可视区显示的时候,在 发起 异步请求

我们可以使用 @vueuse/core 中的 useIntersectionObserver 来实现监听进入可视区域行为,但是必须配合vue3.0的组合API的方式才能实现。

注意:这个函数的懒加载,并不是 这个DOM 进入到 可视区,就开始加载,而是 DOM 和 可视区 相交的面积 达到一定比利时,才认为达到了 这个 门槛,才触发异步请求

npm i @vueuse/core
# 安装:@vueuse/core 包,它封装了常见的一些交互逻辑

官网useIntersectionObserver

官网代码分析

# 官网代码分析
// stop 是停止观察是否进入或移出可视区域的行为    
const { stop } = useIntersectionObserver(
  // target 检测,这个 DOM 容器,是否出现在了 可视区 范围以内,必须是dom容器,而且是vue3.0方式绑定的dom对象
  target,
  // isIntersecting 是否进入可视区域,true是进入 false是移出
  // observerElement 被观察的dom
  ([{ isIntersecting }], observerElement) => {
    // 在此处可根据isIntersecting来判断,然后做业务
  },
)

1)新建一个 js 文件

// hooks 封装逻辑,提供响应式数据
import { useIntersectionObserver } from '@vueuse/core'
import { ref } from 'vue'
// 数据懒加载函数
export const useLazyData = (target, apiFn) => {
  const result = ref([])
  const { stop } = useIntersectionObserver(
    target,
    ([{ isIntersecting }], observerElement) => {
      // isIntersecting 是否进入  可视区
      if (isIntersecting) {
        console.log('进去可视区', observerElement);
        // 停止 后续的 检测 是否进去 可视区
        stop()
        // 调用 api 函数
        apiFn().then(data => {
          result.value = data.result
        })
      }
    },
    // 配置选项
    {
      // threshold 表示 DOM 与 可视区 的 面积  > 0 的时候,就是 只要 显示 这个DOM 一丢丢 就 加载 异步数据
      threshold: 0
    }
  )
  return result
}

2)项目中测试代码

<div ref="box" style="position: relative;height: 406px;">
// 省略。。。
<script>
import HomePanel from './home-panel'
import HomeSkeleton from './home-skeleton'
import { findNew } from '@/api/home'
import { ref } from 'vue'
import { useIntersectionObserver } from '@vueuse/core'
export default {
  name: 'HomeNew',
  components: { HomePanel, HomeSkeleton },
  setup () {
    const goods = ref([])
    const box = ref(null)
    const { stop } = useIntersectionObserver(
      box,
      ([{ isIntersecting }]) => {
        if (isIntersecting) {
          stop()
          findNew().then(data => {
            goods.value = data.result
          })
        }
      }
    )
    return { goods, box }
  }
}
</script>

5.面包屑

xtx-bread

<script>
import { h } from "vue";
export default {
  name: "XtxBread",
  //  解决  最后的 > 符号 ,先要 把 template 模板  删除  掉  一定要删除
  render() {
    // 返回值就是组件要显示的内容
    /* 
      1.创建 xtx-bread 父容器
      2.获取默认插槽内容
      3.去除 xtx-bread-item 组件 的 i 标签,应该有 render 函数来创建
      4.遍历插槽的 item,得到一个动态创建的节点,最后一个 item 不加 i 标签
      5.把动态创建的节点,渲染在 xtx-bread 标签中
      */
    // 第一个参数是 标签名,第二个是属性对象,第三个是子节点
    // 获取 xtx-bread 下所有的默认插槽
    const items = this.$slots.default();
    let dynaincItems = [];
    items.forEach((item, index) => {
      console.log("执行了");
      dynaincItems.push(item);
      if (index < items.length - 1) {
        dynaincItems.push(h("i", { class: "iconfont icon-angle-right" }));
      }
      console.log(dynaincItems);
    });

    // console.log("结果", dynaincItems);
    return h(
      "div",
      {
        class: "xtx-bread",
      },
      dynaincItems
    );
  },
};
</script>

<style  lang='less'>
// 去除 scoped 属性,目的是 让样式作用到,  xtx-bread-item 组件
.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;
  }

  // 这个方法也能去掉最后的 > 符号
  // .xtx-bread-item:last-of-type {
  //   > i:last-of-type {
  //     display: none;
  //   }
  // }
}
</style>

xtx-bread-item

<template>
  <div class="xtx-bread-item">
    <RouterLink v-if="to" :to="to">
      <slot></slot>
    </RouterLink>
    <span v-else><slot></slot></span>

    <!-- <i class="iconfont icon-angle-right"></i> -->
  </div>
</template>

<script>
export default {
  name: "XtxBreadItem",
  props: {
    to: {
      type: [String, Object],
      default: "",
    },
  },
};
</script>

<style scoped lang='less'>
</style>

测试

      <!-- 面包屑 -->
      <XtxBread>
        <XtxBreadItem to="/">首页</XtxBreadItem>
        <XtxBreadItem to="/category/109243029">电器</XtxBreadItem>
        <XtxBreadItem>空调</XtxBreadItem>
      </XtxBread>

再给面包屑加上动画

<template>
  <div class="top-category">
    <div class="container">
      <!-- 面包屑 -->
      <XtxBread>
        <XtxBreadItem to="/">首页</XtxBreadItem>
        <transition name="fade-right" mode="out-in">
          <!-- 当 key 发生变化了,就证明这个 节点需要重新创建和移除 -->
          <!-- 移除元素有动画,创建元素有动画现在是多个 元素 过度
          ,容易出现问题, ,
          mode="out-in" 可以设置为当前元素先进行过度,完成之后新元素过度进入 -->
          <XtxBreadItem :key="topCategory.id">{{
            topCategory.name
          }}</XtxBreadItem>
        </transition>
      </XtxBread>
    </div>
  </div>
</template>

<style scoped lang="less">
.fade-right-enter-to,
.fade-right-leave-from {
  opacity: 1;
  transform: none;
}
.fade-right-enter-active,
.fade-right-leave-active {
  transition: all 0.5s;
}
.fade-right-enter-from,
.fade-right-leave-to {
  opacity: 0;
  transform: translate3d(20px, 0, 0);
}
</style>

6 无限加载组件 / 触底加载

<template>
  <div class="xtx-infinite-loading" ref="container">
    <div class="loading" v-if="loading">
      <span class="img"></span>
      <span class="text">正在加载...</span>
    </div>
    <div class="none" v-if="finished">
      <span class="img"></span>
      <span class="text">亲,没有更多了</span>
    </div>
  </div>
</template>

<script>
import { ref } from "vue";
import { useIntersectionObserver } from "@vueuse/core";
export default {
  name: "XtxInfiniteLoading",
  props: {
    // 是否正在加载
    loading: {
      type: Boolean,
      default: false,
    },
    // 加载是否完成
    finished: {
      type: Boolean,
      default: false,
    },
  },
  setup(props, { emit }) {
    // 监听 这个 DOM  是否进入 可视区
    const container = ref(null);
    useIntersectionObserver(
      container,
      ([{ isIntersecting }], dom) => {
        if (isIntersecting) {
          if (props.loading === false && props.finished === false) {
            // 向外 暴露 一个 事件,让外面继续加载数据
            // 1. 请求加载完成  2. 数据加载完毕,没有剩余的数据了
            emit("infinite");
          }
        }
      },
      {
        threshold: 0,
      }
    );
    return { container };
  },
};
</script>

<style scoped lang='less'>
.xtx-infinite-loading {
  .loading {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 200px;
    .img {
      width: 50px;
      height: 50px;
      background: url(../../assets/images/load.gif) no-repeat center / contain;
    }
    .text {
      color: #999;
      font-size: 16px;
    }
  }
  .none {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 200px;
    .img {
      width: 200px;
      height: 134px;
      background: url(../../assets/images/none.png) no-repeat center / contain;
    }
    .text {
      color: #999;
      font-size: 16px;
    }
  }
}
</style>

测试
效果图
在这里插入图片描述
测试代码

<template>
        <ul>
          <li v-for="goods in goodsList" :key="goods.id">
            <GoodsItem :goods="goods" />
          </li>
        </ul>
        <!-- 无线加载组件 -->
        <XtxInfiniteLoading
          :loading="loading"
          :finished="finished"
          @infinite="getData"
        ></XtxInfiniteLoading>
</template>

<script>
import SubBread from "./components/sub-bread";
import SubFilter from "./components/sub-filter";
import SubSort from "./components/sub-sort";
import GoodsItem from "./components/goods-item.vue";
import { ref, watch } from "vue";
import { findSubCategoryGoods } from "@/api/category";
import { useRoute } from "vue-router";
export default {
  name: "SubCategory",
  components: { SubBread, SubFilter, SubSort, GoodsItem },
  setup() {
    const route = useRoute();
    const loading = ref(false);
    const finished = ref(false);
    // 商品列表数据
    const goodsList = ref([]);
    // 查询参数
    let reqParams = {
      page: 1,
      pageSize: 20,
    };
    const getData = () => {
      loading.value = true;
      // 发请求的时候要设置二级分类的 ID
      reqParams.categoryId = route.params.id;
      findSubCategoryGoods(reqParams).then(({ result }) => {
        // 获取数据成功~~
        // 判断数据有没有  没有 就是 没有数据了
        if (result.items.length) {
          goodsList.value.push(...result.items);
          loading.value = false;
        } else {
          // 没有数据
          finished.value = true;
        }
        loading.value = false;
      });
      console.log("继续加载");
    };
    return { loading, finished, getData, goodsList };
  },
};
</script>
<style scoped lang='less'>
.goods-list {
  background: #fff;
  padding: 0 25px;
  margin-top: 25px;
  ul {
    display: flex;
    flex-wrap: wrap;
    padding: 0 5px;
    li {
      margin-right: 20px;
      margin-bottom: 20px;
      &:nth-child(5n) {
        margin-right: 0;
      }
    }
  }
}
</style>

7. 放大镜

在这里插入图片描述

<template>
  <div class="goods-image">
    <!-- 大图 -->
    <div
      v-show="show"
      class="large"
      :style="[{ backgroundImage: `url(${images[currIndex]})` }, largePosition]"
    ></div>
    <!-- 中图 -->
    <div class="middle" ref="target">
      <img :src="images[currIndex]" alt="" />
      <!-- 遮罩色块 -->
      <div v-show="show" class="layer" :style="layerPosition"></div>
    </div>
    <!-- 小图 -->
    <ul class="small">
      <li
        v-for="(img, i) in images"
        :key="img"
        :class="{ active: i === currIndex }"
      >
        <img @mouseenter="currIndex = i" :src="img" alt="" />
      </li>
    </ul>
  </div>
</template>
<script>
import { reactive, ref, watch } from "vue";
import { useMouseInElement } from "@vueuse/core";
export default {
  name: "GoodsImage",
  props: {
    images: {
      type: Array,
      default: () => [],
    },
  },
  setup(props) {
    // 当前预览图的索引
    const currIndex = ref(0);

    // 1. 是否显示遮罩和大图
    const show = ref(false);
    // 2. 遮罩的坐标(样式)
    const layerPosition = reactive({
      left: 0,
      top: 0,
    });
    // 3. 大图背景定位(样式)
    const largePosition = reactive({
      backgroundPositionX: 0,
      backgroundPositionY: 0,
    });
    // 4. 使用useMouseInElement得到基于元素左上角的坐标和是否离开元素数据
    const target = ref(null);
    const { elementX, elementY, isOutside } = useMouseInElement(target);
    watch([elementX, elementY, isOutside], () => {
      // 5. 根据得到数据设置样式数据和是否显示数据
      show.value = !isOutside.value;

      // 大图 400X400 遮罩层 200X200
      // 计算坐标
      const position = { x: 0, y: 0 };

      if (elementX.value < 100) position.x = 0;
      else if (elementX.value > 300) position.x = 200;
      else position.x = elementX.value - 100;

      if (elementY.value < 100) position.y = 0;
      else if (elementY.value > 300) position.y = 200;
      else position.y = elementY.value - 100;
      // 给样式赋值
      layerPosition.left = position.x + "px";
      layerPosition.top = position.y + "px";
      largePosition.backgroundPositionX = -2 * position.x + "px";
      largePosition.backgroundPositionY = -2 * position.y + "px";
    });

    return { currIndex, show, layerPosition, largePosition, target };
  },
};
</script>
<style scoped lang="less">
.goods-image {
  width: 480px;
  height: 400px;
  position: relative;
  display: flex;
  z-index: 500;
  .large {
    position: absolute;
    top: 0;
    left: 412px;
    width: 400px;
    height: 400px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
    background-repeat: no-repeat;
    background-size: 800px 800px;
    background-color: #f8f8f8;
  }
  .middle {
    width: 400px;
    height: 400px;
    background: #f5f5f5;
    position: relative;
    cursor: move;
    .layer {
      width: 200px;
      height: 200px;
      background: rgba(0, 0, 0, 0.2);
      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 @xtxColor;
      }
    }
  }
}
</style>

8 按钮

<template>
  <button class="xtx-button ellipsis" :class="[size,type]">
    <slot />
  </button>
</template>
<script>
export default {
  name: 'XtxButton',
  props: {
    size: {
      type: String,
      default: 'middle'
    },
    type: {
      type: String,
      default: 'default'
    }
  }
}
</script>
<style scoped lang="less">
.xtx-button {
  appearance: none;
  border: none;
  outline: none;
  background: #fff;
  text-align: center;
  border: 1px solid transparent;
  border-radius: 4px;
  cursor: pointer;
}
.large {
  width: 240px;
  height: 50px;
  font-size: 16px;
}
.middle {
  width: 180px;
  height: 50px;
  font-size: 16px;
}
.small {
  width: 100px;
  height: 32px;
  font-size: 14px;  
}
.mini {
  width: 60px;
  height: 32px;
  font-size: 14px;  
}
.default {
  border-color: #e4e4e4;
  color: #666;
}
.primary {
  border-color: @xtxColor;
  background: @xtxColor;
  color: #fff;
}
.plain {
  border-color: @xtxColor;
  color: @xtxColor;
  background: lighten(@xtxColor,50%);
}
.gray {
  border-color: #ccc;
  background: #ccc;;
  color: #fff;
}
</style>

9. 分页

<template>
  <div class="xtx-pagination">
    <a
      @click="changePager(myCurrentPage - 1)"
      v-if="myCurrentPage > 1"
      href="javascript:;"
      >上一页</a
    >
    <a v-else href="javascript:;" class="disabled">上一页</a>
    <span v-if="pager.start > 1">...</span>
    <a
      @click="changePager(i)"
      href="javascript:;"
      v-for="i in pager.btnArr"
      :key="i"
      :class="{ active: i === myCurrentPage }"
      >{{ i }}</a
    >
    <span v-if="pager.end < pager.pageCount">...</span>
    <a
      @click="changePager(myCurrentPage + 1)"
      v-if="myCurrentPage < pager.pageCount"
      href="javascript:;"
      >下一页</a
    >
    <a v-else href="javascript:;" class="disabled">下一页</a>
  </div>
</template>
<script>
import { computed, ref, watch } from "vue";
export default {
  name: "XtxPagination",
  props: {
    total: {
      type: Number,
      default: 100,
    },
    pageSize: {
      type: Number,
      default: 10,
    },
    currentPage: {
      type: Number,
      default: 1,
    },
  },
  setup(props, { emit }) {
    // 需要数据:
    // 1. 约定按钮的个数 5 个,如果成为动态的需要设置响应式数据
    const count = 5;
    // 2. 当前显示的页码
    const myCurrentPage = ref(1);
    // 3. 总页数 = 总条数 / 每一页条数  向上取整
    const myTotal = ref(100);
    const myPageSize = ref(10);
    // 其他数据(总页数,起始按钮,结束按钮,按钮数组)依赖上面数据得到
    const pager = computed(() => {
      // 总页数
      const pageCount = Math.ceil(myTotal.value / myPageSize.value);
      // 按钮个和当前页码 ====> 起始按钮,结束按钮,按钮数组
      // 1. 理想情况下:
      const offset = Math.floor(count / 2);
      let start = myCurrentPage.value - offset;
      let end = start + count - 1;
      // 2. 如果起始页码小于1需要处理
      if (start < 1) {
        start = 1;
        end = start + count - 1 > pageCount ? pageCount : start + count - 1;
      }
      // 3. 如果结束页码大于总页数需要处理
      if (end > pageCount) {
        end = pageCount;
        start = end - count + 1 < 1 ? 1 : end - count + 1;
      }
      const btnArr = [];
      for (let i = start; i <= end; i++) {
        btnArr.push(i);
      }
      // 提供计算属性数据
      return {
        pageCount,
        btnArr,
        start,
        end,
      };
    });

    // 监听props的变化,更新组件内部数据
    watch(
      props,
      () => {
        myTotal.value = props.total;
        myPageSize.value = props.pageSize;
        myCurrentPage.value = props.currentPage;
      },
      { immediate: true }
    );

    // 切换分页函数
    const changePager = (page) => {
      // 页码相同不作为
      if (myCurrentPage.value !== page) {
        myCurrentPage.value = page;
        // 通知父组件
        emit("current-change", page);
      }
    };

    return { myCurrentPage, pager, changePager };
  },
};
</script>
<style scoped lang="less">
.xtx-pagination {
  display: flex;
  justify-content: center;
  padding: 30px;
  > a {
    display: inline-block;
    padding: 5px 10px;
    border: 1px solid #e4e4e4;
    border-radius: 4px;
    margin-right: 10px;
    &:hover {
      color: @xtxColor;
    }
    &.active {
      background: @xtxColor;
      color: #fff;
      border-color: @xtxColor;
    }
    &.disabled {
      cursor: not-allowed;
      opacity: 0.4;
      &:hover {
        color: #333;
      }
    }
  }
  > span {
    margin-right: 10px;
  }
}
</style>

10. 选择按钮

在这里插入图片描述

<template>
  <div class="xtx-checkbox" @click="changeChecked()">
    <i v-if="checked" class="iconfont icon-checked"></i>
    <i v-else class="iconfont icon-unchecked"></i>
    <span v-if="$slots.default"><slot /></span>
  </div>
</template>
<script>
import { ref, watch } from "vue";
// v-model ===> :modelValue + @update:modelValue
export default {
  name: "XtxCheckbox",
  props: {
    modelValue: {
      type: Boolean,
      default: false,
    },
  },
  setup(props, { emit }) {
    const checked = ref(false);
    const changeChecked = () => {
      checked.value = !checked.value;
      emit("update:modelValue", checked.value);
      emit("change", checked.value);
    };
    watch(
      () => props.modelValue,
      () => {
        checked.value = props.modelValue;
      },
      { immediate: true }
    );
    return { checked, changeChecked };
  },
};
</script>
<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>

11. 消息提示框组件 (组件式调用)

<template>
  <Transition name="down">
    <div class="xtx-message" :style="style" v-show="visible">
      <!-- 上面绑定的是样式 -->
      <!-- 不同提示图标会变 -->
      <i class="iconfont" :class="[style[type].icon]"></i>
      <span class="text">{{ text }}</span>
    </div>
  </Transition>
</template>
<script>
import { onMounted, ref } from "vue";
export default {
  name: "XtxMessage",
  props: {
    text: {
      type: String,
      default: "",
    },
    type: {
      type: String,
      // warn 警告  error 错误  success 成功
      default: "warn",
    },
  },
  setup() {
    // 定义一个对象,包含三种情况的样式,对象key就是类型字符串
    const style = {
      warn: {
        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)",
      },
    };
    // 定义一个数据控制显示隐藏,默认是隐藏,组件挂载完毕显示
    const visible = ref(false);
    onMounted(() => {
      visible.value = true;
    });
    return { style, visible };
  },
};
</script>
<style scoped lang="less">
.down {
  &-enter {
    &-from {
      transform: translate3d(0, -75px, 0);
      opacity: 0;
    }
    &-active {
      transition: all 0.5s;
    }
    &-to {
      transform: none;
      opacity: 1;
    }
  }
}

.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>

12.消息提示框组件(函数式调用)

Message.js

// 提供一个 能够显示 xtx-message 组件的函数
// 这个函数,将来可以导入直接使用,也可以挂载再 vue 实力原型上
// import Message from 'Message.js' 使用 Message({type:'error},text:'提示文字')
// this.$Message({type:'error},text:'提示文字')
import { createVNode, render } from 'vue'
import XtxMessage from './xtx-message.vue'

// 准备dom容器
const div = document.createElement('div')
div.setAttribute('class', 'xtx-message-container')
document.body.appendChild(div)
// 定时器标识
let timer = null

export default ({ type, text }) => {
  console.log(type, text);
  // 实现:根据xtx-message.vue渲染消息提示
  // 1. 导入组件
  // 2. 根据组件创建虚拟节点
  // createVNode(那个组件 , 组件的参数)
  const vnode = createVNode(XtxMessage, { text, type })
  // 3. 准备一个DOM容器
  // 4. 把虚拟节点渲染DOM容器中
  // render(虚拟节点 , 将虚拟节点渲染到那个容器)
  render(vnode, div)
  // 5. 开启定时,移出DOM容器内容
  clearTimeout(timer)
  timer = setTimeout(() => {
    // render 第一个参数是 null 就表示 销毁这个 div
    render(null, div)
  }, 3000)
}

1.导入式调用

import Message from "../../../components/library/Message";

      Message({ type: "error", text: "用户名或者密码错误" });

2.函数式调用

// 扩展vue原有的功能:全局组件,自定义指令,挂载原型方法,注意:没有全局过滤器。
// 这就是插件
// vue2.0插件写法要素:导出一个对象,有install函数,默认传入了Vue构造函数,Vue基础之上扩展
// vue3.0插件写法要素:导出一个对象,有install函数,默认传入了app应用实例,app基础之上扩展

console.log(importFn.keys());
import Message from './Message';
export default {
  install(app) {
    // 挂在 message 函数
    app.config.globalProperties.$message = Message
  }
}
}
# 使用
  created() {
    this.$message({ text: "错误" });
  },
# 使用:第二种方式
import {  getCurrentInstance } from "vue";
 setup() {
    // 用 getCurrentInstance  再 setup 中调用 message 方法
    const { proxy } = getCurrentInstance();
    // proxy 就是当前组件的实例
    proxy.$message({ text: "11111" });
 }

13. 确认消息的弹框

<template>
  <div class="xtx-confirm" :class="{ fade }">
    <div class="wrapper" :class="{ fade }">
      <div class="header">
        <h3>{{ title }}</h3>
        <a
          @click="cancel"
          href="JavaScript:;"
          class="iconfont icon-close-new"
        ></a>
      </div>
      <div class="body">
        <i class="iconfont icon-warning"></i>
        <span>{{ text }}</span>
      </div>
      <div class="footer">
        <XtxButton @click="cancel" size="mini" type="gray">取消</XtxButton>
        <XtxButton @click="submit" size="mini" type="primary">确认</XtxButton>
      </div>
    </div>
  </div>
</template>
<script>
import { onMounted, ref } from "vue";
import XtxButton from "./xtx-button";
export default {
  name: "XtxConfirm",
  components: { XtxButton },
  props: {
    title: {
      type: String,
      default: "温馨提示",
    },
    text: {
      type: String,
      default: "",
    },
    cancelCallback: {
      type: Function,
    },
    submitCallback: {
      type: Function,
    },
  },
  setup(props) {
    // 对话框默认隐藏
    const fade = ref(false);
    // 组件渲染完毕后
    onMounted(() => {
      // 过渡效果需要在元素创建完毕后延时一会加上才会触发
      setTimeout(() => {
        fade.value = true;
      }, 0);
    });
    // 取消
    const cancel = () => {
      // 其他事情
      props.cancelCallback();
    };
    // 确认
    const submit = () => {
      // 其他事情
      props.submitCallback();
    };
    return { cancel, submit, fade };
  },
};
</script>
<style scoped lang="less">
.xtx-confirm {
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  z-index: 8888;
  background: rgba(0, 0, 0, 0);
  &.fade {
    transition: all 0.4s;
    background: rgba(0, 0, 0, 0.5);
  }
  .wrapper {
    width: 400px;
    background: #fff;
    border-radius: 4px;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -60%);
    opacity: 0;
    &.fade {
      transition: all 0.4s;
      transform: translate(-50%, -50%);
      opacity: 1;
    }
    .header,
    .footer {
      height: 50px;
      line-height: 50px;
      padding: 0 20px;
    }
    .body {
      padding: 20px 40px;
      font-size: 16px;
      .icon-warning {
        color: @priceColor;
        margin-right: 3px;
        font-size: 16px;
      }
    }
    .footer {
      text-align: right;
      .xtx-button {
        margin-left: 20px;
      }
    }
    .header {
      position: relative;
      h3 {
        font-weight: normal;
        font-size: 18px;
      }
      a {
        position: absolute;
        right: 15px;
        top: 15px;
        font-size: 20px;
        width: 20px;
        height: 20px;
        line-height: 20px;
        text-align: center;
        color: #999;
        &:hover {
          color: #666;
        }
      }
    }
  }
}
</style>

Confirm.js

import { createVNode, render } from 'vue'
import XtxConfirm from './xtx-confirm'

// 准备div
const div = document.createElement('div')
div.setAttribute('class', 'xtx-confirm-container')
document.body.appendChild(div)

// 该函数渲染XtxConfirm组件,标题和文本
// 函数的返回值是promise对象
export default ({ title, text }) => {
  return new Promise((resolve, reject) => {
    const submitCallback = () => {
      render(null, div)
      resolve()
    }
    const cancelCallback = () => {
      render(null, div)
      reject(new Error('点击取消'))
    }
    // 1. 渲染组件
    // 2. 点击确认按钮,触发resolve同时销毁组件
    // 3. 点击取消按钮,触发reject同时销毁组件
    const vnode = createVNode(XtxConfirm, { title, text, submitCallback, cancelCallback })
    render(vnode, div)
  })
}
  • 6
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值