如何把自定义组件,注册为全局组件,指令注册为全局指令
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 包,它封装了常见的一些交互逻辑
官网代码分析
# 官网代码分析
// 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)
})
}