加载更多组件
定义组件:src/components/library/xtx-infinite-loading.vue
<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 }) {
const container = ref(null)
useIntersectionObserver(
container,
([{ isIntersecting }], dom) => {
if (isIntersecting) {
if (props.loading === false && props.finished === false) {
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>
注册组件:src/components/library/index.js
import XtxInfiniteLoading from './xtx-InfiniteLoading.vue'
export default {
install(app) {
app.component(XtxInfiniteLoading.name, XtxInfiniteLoading)
}
}
引用组件:src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
// 导入自定插件
import ui from './components/library'
// 创建一个vue应用实例
createApp(App).use(store).use(router).use(ui).mount('#app')
使用组件: .vue文件
<XtxInfiniteLoading :loading="loading" :finished="finished"
@infinite="getData" />
<script>
import { ref} from 'vue'
import { useRoute } from 'vue-router'
import { findSubCategoryGoods } from '@/api/category'
export default {
name: 'SubCategory',
setup () {
const loading = ref(false)
const finished = ref(false)
const route = useRoute()
const goodsList = ref([])
// 查询参数
let reqParams = {
page: 1,
pageSize: 20
}
// 获取数据函数
const getData = () => {
loading.value = true
reqParams.categoryId = route.params.id
findSubCategoryGoods(reqParams).then(({ result }) => {
if (result.items.length) {
goodsList.value.push(...result.items)
reqParams.page++
} else {
// 加载完毕
finished.value = true
}
// 请求结束
loading.value = false
})
}
return { loading, finished, getData }
}
}
</script>
轮播图组件
首先是轮播图的样式:src/components/library/xtx-carousel.vue
<template>
</template>
<script>
</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%;
}
// 轮播商品
.slider {
display: flex;
justify-content: space-around;
padding: 0 40px;
>a {
width: 240px;
text-align: center;
img {
padding: 20px;
width: 230px !important;
height: 230px !important;
}
.name {
font-size: 16px;
color: #666;
padding: 0 40px;
}
.price {
font-size: 16px;
color: @priceColor;
margin-top: 15px;
}
}
}
}
&-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, .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>
然后是轮播图的结构与逻辑封装:src/components/library/xtx-carousel.vue
<template>
<div class='xtx-carousel' @mouseenter="stop()" @mouseleave="start()">
<ul class="carousel-body">
<!-- fade是控制显示那的那张图片 需要一个默认索引数据,
渲染第一张图和激活第一个点 -->
<li class="carousel-item" v-for="(item,i) in sliders" :key="i"
:class="{fade:index===i}">
<RouterLink to="/">
<img :src="item.imgUrl" alt="">
</RouterLink>
</li>
</ul>
<!-- 左右点击按钮 -->
<a @click="toogle(-1)" href="javascript:;" class="carousel-btn prev">
<i class="iconfont icon-angle-left"></i>
</a>
<a @click="toogle(1)" href="javascript:;" class="carousel-btn next">
<i class="iconfont icon-angle-right"></i>
</a>
<!-- 跟随的小点 -->
<div class="carousel-indicator">
<span 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: {
silders: {
type: Array,
default: () => []
},
//自动轮播的间隙
duration: {
type: Number,
default: 3000
},
//是否自动轮播
antoPlay: {
type: Boolean,
default: true
}
},
setup(props) {
// 设置默认索引
const index = ref(0)
// 自动播放
let timer = null
const autoPlayFn = () => {
clearInterval(timer)
timer = setTimeout(() => {
index.value++
if (index.value >= props.silders.length) {
index.value = 0
}
}, props.duration)
}
watch(() => index.value, () => {
// 有数据 并且开启了自动播放,才调用自动播放
if (props.sliders.length && props.antoPlay) {
autoPlayFn()
}
}, { immediate: true })
// 鼠标进入轮播 计时停止
const stop = () => {
if (timer) clearInterval(timer)
}
// 鼠标离开轮播 计时开始
const start = () => {
if (props.silders.length && props.antoPlay) {
autoPlayFn()
}
}
// 左右按钮切换
const toogle = (step) => {
const newIndex = index.value + step
if (newIndex >= props.silders.length) {
index.value = 0
} else if (newIndex < 0) {
index.value = props.silders.length - 1
} else {
index.value = newIndex
}
}
// 销毁计时器
onUnmounted(() => {
clearInterval(timer)
})
return {
index,
stop,
start,
timer,
toogle
}
}
}
</script>
插件注册:src/components/library/index.js
+import XtxCarousel from './xtx-carousel.vue'
export default {
install (app) {
+app.component(XtxCarousel.name, XtxCarousel)
}
}
插件使用:src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import 'normalize.css'
+import ui from './components/library'
+createApp(App).use(store).use(router).use(ui).mount('#app')
在.vue文件中使用
<XtxCarousel :sliders="sliders" />
消息提示组件
封装组件: src/components/library/xtx-message.vue
<template>
<div class="xtx-message" :style="style[type]">
<!-- 上面绑定的是样式 -->
<!-- 不同提示图标会变 -->
<i class="iconfont" :class="[style[type].icon]"></i>
<span class="text">{
{text}}</span>
</div>
</template>
<script>
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)'
}
}
return { style }
}
}
</script>
<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>
插件定义:src/componets/library/index.js
import XtxMessage from './xtx-message.vue'
export default {
install (app) {
// 在app上进行扩展,app提供 component directive函数
// component是用来进行全局组件注册的
// directive主要是用来自定义指令的
// 如果要挂载原型 app.config.globalProperties 方式
app.component(XtxMessage.name, XtxMessage)
}
}
使用插件:src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import 'normalize.css'
+import ui from './components/library'
+// 插件的使用,在main.js使用app.use(插件)
+createApp(App).use(store).use(router).use(ui).mount('#app')
在.vue文件中使用:
<XtxMessage text="账号密码错误" type="error"/>
骨架屏组件
封装组件:src/components/library/xtx-skeleton.vue
<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>
插件定义: src/componets/library/index.js
import XtxSkeleton from './xtx-skeleton.vue'
export default {
install (app) {
// 在app上进行扩展,app提供 component directive 函数
// 如果要挂载原型 app.config.globalProperties 方式
// component是用来进行全局组件注册的
// directive主要是用来自定义指令的
app.component(XtxSkeleton.name, XtxSkeleton)
}
}
使用插件: src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import 'normalize.css'
+import ui from './components/library'
+// 插件的使用,在main.js使用app.use(插件)
+createApp(App).use(store).use(router).use(ui).mount('#app')
在.vue文件中使用:
<XtxSkeleton bg="#e4e4e4" width="306px" height="306px" animated />
<XtxSkeleton width="50px" height="18px" bg="rgba(255,255,255,0.2)" />