vue 常见组件封装
实际开发的时候,为了减少重复造轮子,提高工作效率,节省开发时间成本, 免不了会使用ui组件库,比如在web前端很受欢迎的element-ui。 但有的时候,我们需要在原组件的基础上做些改造,比如一个image组件, 我们需要统一在图片加载失败的时候展示的特定图,每次使用组件都加一遍, 麻烦耗时,关键是维护成本高,当需要更新这个加载出错的图片时, 得再次一个个去找到使用该组件的地方修改。
再例如自定义分页组件也很常见,组件的样式,默认支持的每页数目, 封装之后再用,在可维护性和开发效率上都很有好处。
常用组件:在项目多处重复,在多处场景调用的组件,可以封装为公共组件 我们在项目开发过程中,某个界面的详情,可以在系统各个地方调用,这样的组件需要我们封装为公共的业务组件供系统使用
vue 的组件封装,有两种方式:
- 普通公共组件封装:父子通信,v-model
- 通过渲染函数封装组件:
h函数与render函数运用
共性:
vue 的插槽使用:
- 普通插槽
- 具名插槽
- 作用域插槽
前置工作:创建组件 + 全局注册
例:
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>