Vue实现图文瀑布流布局模式,图片预加载显示占位区域阴影,加载完成后向上浮动动画出现
sgWaterfall.vue源码
<template>
<ul :class="$options.name" :style="waterfallStyle">
<li v-for="(a, i) in items " :key="i" :style="a.style" :imgLoaded="a.imgLoaded">
<template>
<div class="cover">
<img :src="a.imgURL" @load="a.imgLoaded = true">
</div>
<label>{{ a.label }}</label>
</template>
</li>
</ul>
</template>
<script>
export default {
name: 'sgWaterfall',
data() {
return {
imgLoadedStatus: [],
items: [],
waterfallStyle: {},
__var_itemTextHeight: 0,
}
},
props: [
"data",
"itemWidth",
"itemMarginRight",
"itemMarginBottom",
"itemTextHeight",
"webpageMargin",
],
watch: {
itemTextHeight: {
handler(d) {
this.__var_itemTextHeight = parseFloat(d) || 50;
this.setProperty();
}, deep: true, immediate: true,
},
data: {
handler(d) {
d && (this.items = this.calcAnyPhotosPosition(d));
}, deep: true, immediate: true,
},
},
destroyed() {
removeEventListener('resize', this.resize);
},
mounted() {
this.setProperty();
addEventListener('resize', this.resize);
},
methods: {
resize(e) {
this.items = this.calcAnyPhotosPosition(this.items)
},
setProperty() {
this.$el && this.$el.style.setProperty("--itemTextHeight", this.__var_itemTextHeight + 'px');
},
// BEGIN 计算图片的坐标位置_________________________________________________________
// 计算能显示下多少列
calcColsCount({ itemWidth, webpageMargin, itemMarginRight }) {
return Math.floor((window.innerWidth - webpageMargin * 2 + itemMarginRight) / (itemWidth + itemMarginRight));
},
// 计算图片的坐标位置
calcPhotoScaleWidthHeight(d, { itemWidth, itemTextHeight, defaultItemHeight, }) {
// index是图片索引
if (d.width && d.height) {
let originWidth = d.width;//图片原始宽度
let originHeight = d.height;//图片原始高度
let proportion = originWidth / originHeight;//图片宽高比
let scaleWidth = itemWidth;//缩放后的宽度
let scaleHeight = scaleWidth / proportion;//缩放后的高度
return {
width: scaleWidth,
height: scaleHeight + itemTextHeight,
}
} else {
// 后台不提供高度数据的情况下(默认宽度高度,是个正方形)
return {
width: itemWidth,
height: defaultItemHeight + itemTextHeight,
}
}
},
// 批量计算图片的坐标位置
calcAnyPhotosPosition(arr) {
arr = JSON.parse(JSON.stringify(arr));
let itemWidth = parseFloat(this.itemWidth) || 250;//固定宽度
let defaultItemHeight = itemWidth;//默认高度
let itemMarginRight = parseFloat(this.itemMarginRight) || 20;//右侧间距
let itemMarginBottom = parseFloat(this.itemMarginBottom) || 20;//底部间距
let itemTextHeight = this.__var_itemTextHeight;//文本显示区域高度
let webpageMargin = parseFloat(this.webpageMargin) || 0;//网页左右两侧间距
let colsCount = this.calcColsCount({ itemWidth, webpageMargin, itemMarginRight });//一共有多少列
let colHeights = [...Array(colsCount)].map(v => 0);//列的高度数组
arr.forEach(v => {
let minHeight = Math.min(...colHeights);//找到高度最小的值
let colIndex = colHeights.indexOf(minHeight);//找到高度最小的那一列
let { width, height } = this.calcPhotoScaleWidthHeight(v, { itemWidth, itemTextHeight, defaultItemHeight, });
let left = colIndex * (itemWidth + itemMarginRight);
let top = minHeight + itemMarginBottom;
colHeights[colIndex] += (height + itemMarginBottom);
v.imgLoaded || (v.imgLoaded = false);//预加载图片使用响应式属性(这里不提前声明初始化,动态渲染数据的时候不会生效)
v.style = {
left: left + 'px',
top: top + 'px',
width: width + 'px',
height: height + 'px',
}
});
let waterfallWidth = (itemWidth + itemMarginRight) * colsCount - itemMarginRight;//计算容器宽度
let waterfallHeight = this.scrollContainerHeight = Math.max(...colHeights);//计算容器高度
this.waterfallStyle = {
width: waterfallWidth + 'px',
height: waterfallHeight + 'px',
}
return arr;
},
// END _________________________________________________________
}
};
</script>
<style lang="scss" scoped>
.sgWaterfall {
position: relative;
overflow: hidden;
margin: 0 auto;
li {
position: absolute;
display: flex;
flex-direction: column;
transition: .382s;
.cover {
height: calc(100% - var(--itemTextHeight));
background-color: #f0f0f0;
border-radius: 16px;
overflow: hidden;
transition: .382s;
img {
border-radius: 16px;
opacity: 0;
width: 100%;
height: 100%;
transform: translateY(100%);
object-position: center;
object-fit: cover;
transition: .8s cubic-bezier(.075, .82, .165, 1); //快速+柔和结束
}
}
label {
height: var(--itemTextHeight);
display: block;
box-sizing: border-box;
padding: 10px 0;
/*多行省略号*/
overflow: hidden;
word-break: break-all;
/*单词分割换行*/
display: -webkit-box;
-webkit-box-orient: vertical;
max-height: min-content;
-webkit-line-clamp: 2;
line-height: 1.6;
font-size: 14px;
}
&[imgLoaded] {
.cover {
background-color: transparent;
img {
opacity: 1;
transform: translateY(0);
}
}
}
}
}
</style>
应用组件
<template>
<div class="sg-body">
<sgWaterfall :data="data" itemWidth="200" />
</div>
</template>
<script>
import sgWaterfall from "@/vue/components/admin/sgWaterfall";
export default {
components: { sgWaterfall },
data: () => ({
labels: [
'在新的历史起点上继续推动文化繁荣',
'江苏一飞机坠落 坠落地有飞机残骸',
'河南暴雨 宾客坐积水中吃席',
'多地细化举措护航暑期安全见闻',
'男子炫耀钓到大鱼挂后备箱示众',
'男子自称上山捡菌遇老虎 警方:行拘',
'高温天妈妈将电扇让给孩子被热死',
'狗狗被电动车踏板打了一路屁股',
'重度自闭症男孩靠吹气球成网红',
'男子疑在售水机打水时触电身亡',
'广州平均月薪10883元',
'邓伦再成被执行人',
'9岁男孩被跳楼者砸中昏迷不醒',
'女子买69平公寓公摊37平',
'“银行高管”娶四个老婆获刑',
'封神首映费翔黄渤河南话引爆笑',
'张一山靠在关晓彤后背哭',
'马丽的孩子确实叫沈腾舅舅',
'鹿晗现身医院做康复治疗',
'男子高空抛砖砸死人 称想跳楼但不敢',
'一名中国侨胞在巴西遭枪击身亡',
'山西杀害队长的环卫工已被捕',
'猴子4小时去超市连偷3次',
'韩安冉吐槽环球影城3小时花8000',
'九旬老人逗鹦鹉得了鹦鹉热',
'二十大以来首个正部级落马',
'试管失败医院拒绝返还夫妻胚胎',
'老人怕味大影响乘客高铁上干吃泡面',
'重庆一消防员考上清华大学研究生',
'猪场因断电死亡462头猪损失近百万',
'消防喷水20分钟解救3000只中暑鸭',
],
data: [],
}),
created() {
this.data = this.createData();
},
methods: {
createData(e) {
return [...Array(50)].map((v, i) => {
let width = Math.round(Math.random() * (1000 - 500) + 500);
let height = Math.round(Math.random() * (1000 - 500) + 500);
return {
label: this.labels[Math.round(Math.random() * (this.labels.length - 1))],
// imgURL: `static/files/picture/${(Math.random() * (50 - 1) + 1).toFixed(0)}.jpg`,
imgURL: `http://via.placeholder.com/${width}x${height}`,
width,
height,
}
}
)
},
},
};
</script>
<style lang="scss" scoped>
.sg-body {
width: 100vw;
height: 100vh;
overflow: hidden;
overflow-y: auto;
}
</style>