组件背景
- 需要实现瀑布流效果、使用的前端框架是基于vant的。
- 瀑布流内容多种形式,除了图文还有标签形式。
- 项目需要SSR。
- 片寻网络,找到一个不错的瀑布流组件,基于它修改了部分内容(优化了数据获取方式,传递列表支持ssr,传递promise参数支持下拉刷新和上拉加载更多,修改内容部分,使用slot支持组件外部自定义内容格式)
实现效果
组件源码:
<template>
<div class="flow-box">
<div class="type-box" v-if="typeList && typeList.length">
<ul class="type-list">
<li
@click="changeType(index)"
v-for="(item, index) in typeList"
:class="['type-item', typeIndex == index ? 'type-item-on' : '']"
:key="index"
>
<div class="text">{{ item.name }}</div>
<div class="line"></div>
</li>
</ul>
</div>
<van-pull-refresh
v-model="isLoading"
@refresh="onRefresh"
style="min-height: 100vh"
:success-duration="800"
success-text="加载成功"
loading-text="加载中..."
>
<van-list
v-if="haveData == 2"
v-model="loading"
:finished="finished"
finished-text="没有更多了"
@load="onBottomLoad"
:offset="150"
:immediate-check="false"
>
<div class="data-list-box" id="data-list-box">
<div
class="data-item"
v-for="(item, index) in dataList"
:style="{ width: boxWidth + 'px' }"
:key="index"
>
<slot :item="item" :index="index"></slot>
</div>
</div>
</van-list>
</van-pull-refresh>
</div>
</template>
<script>
export default {
props: ["dataSource", "promisefunc"],
data() {
return {
typeList: [
//分类列表
// { name: "全部" },
// { name: "手机数码" },
// { name: "家用电器" },
// { name: "酒水饮料" },
// { name: "钟表珠宝" },
// { name: "美妆护肤" },
// { name: "运动户外" },
// { name: "汽车生活" },
],
typeIndex: 0, //分类索引
dataList: this.dataSource || [], //列表数据
haveData: 0, //是否有数据,1=无,2=有,0=页面还未初始化
pageIndex: 1, //页码
pageSize: 8, //每页加载数据数量
isLoading: false, //下拉刷新进行中,请求开始true, 请求完成false,用于下拉刷新组件van-pull-refresh
loading: false, //上拉加载更多中,上拉触底时自动变成true, 请求完成设置为false, 用于列表组件van-list
finished: false, //上拉加载是否加载完最后一页数,用于组件van-list
itemCount: 0, //上一次加载完成后的瀑布流item个数
lastRowHeights: [0, 0], //最后一行标签的顶部间距+高度,2列
boxMargin: 15, //每个item之间的边距
boxWidth: 165, //每个item宽度
};
},
created() {
if (process.server) {
return;
}
this.$toast.loading({
message: "加载中...",
duration: 0,
});
//当前瀑布流设置为两列,计算瀑布流每个item和图片的宽度
//console.log("load", window.innerWidth);
let screenWidth = window.innerWidth || 375; //屏幕宽度
this.boxWidth = (screenWidth - this.boxMargin * 3) / 2; //每个item的宽度
if (this.dataSource == null || !this.dataSource.length) {
this.onRefresh(); //刷新数据
} else {
this.pageIndex = 1; //重置第一页
this.finished = false; //上拉加载"所有数据已经完成"标识 重置为false
this.dataList = [];
this.loadImagesHeight(this.dataSource);
}
},
methods: {
changeType(index) {
//切换类型
if (this.typeIndex == index) return;
this.$toast.loading({
message: "加载中...",
duration: 0,
});
this.typeIndex = index;
this.onRefresh();
},
onRefresh() {
//下拉刷新
// if (this.isLoading) return; //还在请求中,返回
this.pageIndex = 1; //重置第一页
this.finished = false; //上拉加载"所有数据已经完成"标识 重置为false
//接口请求
this.getDataList();
},
onBottomLoad() {
//上拉加载更多
if (this.finished) return; //说明所有数据已经加载完毕,返回
this.getDataList(); //下一页数据请求中
},
//数据请求
getDataList() {
this.promisefunc({ page: this.pageIndex })
.then((res) => {
let list = res ? res : [];
if (list.length > 0) {
//从list中取pageSize条数据出来
var tempList = [];
for (let i = 0; i < this.pageSize; i++) {
if (list.length > 0) {
let tempIndex = parseInt(Math.random() * 1000) % list.length;
tempList.push(list[tempIndex]);
list.splice(tempIndex, 1);
}
}
this.loadImagesHeight(tempList); //模拟预加载图片,获取图片高度
} else {
this.loadImagesHeight(list); //处理数据
}
})
.catch((res) => {
//console.log("..fail: ", res);
this.$toast.clear();
this.isLoading = false; //下拉刷新请求完成
this.loading = false; //上拉加载更多请求完成
});
},
loadImagesHeight(list) {
var count = 0; //用来计数,表示是否所有图片高度已经获取
list.forEach((item, index) => {
//创建图片对象,加载图片,计算图片高度
var img = new Image();
img.src = item.cover;
img.onload = img.onerror = (e) => {
count++;
if (e.type == "load") {
//图片加载成功
//计算图片缩放后的高度:图片原高度/原宽度 = 缩放后高度/缩放后宽度
list[index].imgHeight = Math.round(
(img.height * this.boxWidth) / img.width
);
// console.log('index: ', index, ', load suc, imgHeiht: ', list[index].imgHeight);
} else {
//图片加载失败,给一个默认高度50
list[index].imgHeight = 50;
//console.log("index: ", index, ", 加载报错:", e);
}
//加载完成最后一个图片高度,开始下一步数据处理
if (count == list.length) {
this.resolveDataList(list);
}
};
});
},
resolveDataList(list) {
//处理数据
//下拉刷新,清空原数据
if (this.pageIndex <= 1) {
this.itemCount = 0;
this.dataList = [];
this.lastRowHeights = [0, 0]; //存储每列的最后一行高度清0
}
if (list.length >= this.pageSize) {
this.pageIndex++; //还有下一页
} else {
this.finished = true; //当前tab类型下所有数据已经加载完成
}
//合并新老两个数组数据
this.dataList = [...this.dataList, ...list];
//判断页面是否有数据
this.haveData = this.dataList.length > 0 ? 2 : 1;
// this.isLoading = false; //下拉刷新请求完成
// this.loading = false; //上拉加载更多请求完成
//console.log("...datalist: ", this.dataList);
//console.log("...this.isLoading: ", this.isLoading);
this.$nextTick(() => {
setTimeout(() => {
//渲染完成,计算每个item宽高,设置标签坐标定位
this.setItemElementPosition();
this.isLoading = false; //下拉刷新请求完成
this.loading = false; //上拉加载更多请求完成
}, 100);
});
},
//获取每个item标签高度,设置item的定位
setItemElementPosition() {
if (process.server) {
return;
}
let parentEle = document.getElementById("data-list-box");
let boxEles = parentEle.getElementsByClassName("data-item");
for (let i = this.itemCount; i < boxEles.length; i++) {
let tempEle = boxEles[i];
//上一个标签最小高度的列索引
let curColIndex = this.getMinHeightIndex(this.lastRowHeights);
let boxTop = this.lastRowHeights[curColIndex] + this.boxMargin;
let boxLeft =
curColIndex * (this.boxWidth + this.boxMargin) + this.boxMargin;
tempEle.style.left = boxLeft + "px";
tempEle.style.top = boxTop + "px";
this.lastRowHeights[curColIndex] = boxTop + tempEle.offsetHeight;
// console.log('i = ', i, ', boxTop: ', boxTop, ', eleHeight: ', tempEle.offsetHeight);
}
this.itemCount = boxEles.length;
//修改父级标签的高度
let maxHeight = Math.max.apply(null, this.lastRowHeights);
parentEle.style.height = maxHeight + "px";
this.$toast.clear();
//console.log("...boxEles: ", boxEles.length, ", maxH: ", maxHeight);
},
//获取数组中最小值的索引
getMinHeightIndex(arr) {
var minHeight = Math.min.apply(null, arr);
for (let i = 0; i < arr.length; i++) {
if (arr[i] == minHeight) {
return i;
}
}
},
},
};
</script>
<style lang="scss" scoped>
.flow-box {
background-color: #ffffff;
width: 100vw;
height: 100vh;
}
.flow-box .type-box {
width: 100%;
height: 40px;
position: fixed;
top: 0;
font-size: 14px;
background: #fff;
color: rgba(0, 0, 0, 0.4);
z-index: 99;
border-bottom: 0.5px solid #dddddd;
padding: 0 5px;
}
.type-box .type-list {
white-space: nowrap;
overflow-x: scroll;
}
.type-list .type-item {
display: inline-block;
padding: 0 12.5px;
height: 40px;
text-align: center;
}
.type-list .type-item .text {
line-height: 37.5px;
font-size: 14px;
color: rgba(0, 0, 0, 0.4);
margin: 0;
}
.type-list .type-item .line {
background-color: #ffffff;
}
.type-list .type-item-on .text {
font-size: 16px;
color: #f0142d;
}
.type-list .type-item-on .line {
width: 19px;
height: 2px;
margin: 0 auto;
background-color: #f0142d;
border-radius: 2px;
}
/* 隐藏滚动条 */
.type-list::webkit-scrollbar {
display: none;
}
/* 列表数据样式 */
@keyframes data-item-ani {
0% {
transform: scale(0.5);
}
100% {
transform: scale(1);
}
}
.flow-box .data-list-box {
position: relative;
height: 100vh;
/* margin-top: 40px; */
}
.data-list-box .data-item {
height: auto;
position: absolute;
background-color: #ffffff;
/* box-shadow: 0px 0px 5px 2px rgba(0, 0, 0, 0.5); */
left: -1000px;
animation: data-item-ani 0.4s;
transition: left 0.6s, top 0.6s;
transition-delay: 0.1s;
}
</style>
使用方法
<template>
<Waterfall
:dataSource="anlilist"
:promisefunc="this.requestFunc || this.$api.requestZhuangxiuCase"
>
<template slot-scope="child">
<div
class="hot-list"
v-if="child.index === 5 && hotlist && hotlist.length"
>
<lookall :list="hotlist" />
</div>
<div class="item-box">
<a href="/shop/" class="a">
<img
:src="child.item.cover"
class="data-cover"
:alt="child.item.title"
:style="{ width: '100%', height: child.item.imgHeight + 'px' }"
/>
<p class="p">{{ child.item.sign }}</p>
<h3 class="h3">{{ child.item.title }}</h3>
<i class="icon icon-an"></i>
</a>
<div class="news-list-info cf">
<a href="/shop/" class="source"
><img :src="child.item.iconimg" alt="" />{{
child.item.icontitle
}}</a
><a href="" class="btn-zan">{{ child.item.count }}赞</a>
</div>
</div>
</template>
</Waterfall>
</template>
<script>
import lookall from "~/components/jiajuhao/lookall.vue";
export default {
components: {
lookall,
},
props: ["dataSource", "requestFunc", "hotlist"],
data() {
return {
anlilist: this.dataSource || [],
};
},
};
</script>
<style lang="scss">
/* 全局样式 */
.hot-list {
border-radius: 0.1rem;
border: 1px solid #e5e5e5;
overflow: hidden;
background: #fff;
padding: 0.35rem 0.2rem;
margin-bottom: 0.25rem;
.news-block {
padding: 0;
border: 0;
h2 {
border: 0;
margin: 0;
}
ul {
li {
width: 100%;
}
}
}
}
</style>
<style lang="scss" scoped>
.item-box {
border-radius: 0.1rem;
border: 1px solid #e5e5e5;
overflow: hidden;
background: #fff;
.a {
position: relative;
display: block;
height: auto;
padding-bottom: 0.2rem;
}
.data-cover {
margin-bottom: 0.1rem;
display: block;
}
.p {
padding: 0.05rem 0.2rem 0.1rem;
font-size: 0.2rem;
color: #999;
}
.h3 {
padding: 0 0 0 0.2rem;
font-size: 0.26rem;
}
.icon-an {
width: 0.59rem;
height: 0.33rem;
background-position: 0 -2.5rem;
position: absolute;
top: 0;
left: 0;
}
.data-name {
font-size: 14px;
padding: 5px 10px;
text-align: left;
}
.news-list-info {
margin: 0 0.1rem 0.1rem;
}
}
</style>
组件原地址转至:https://www.cnblogs.com/tandaxia/p/12189301.html
评论