瀑布流demo的实现效果:
效果说明:
1.使用vue3实现瀑布流效果;
2.瀑布流横向设置5等分,可根据个人需求调整;
3.左侧菜单可根据右侧滚动条滑动时进行固定和取消固定,实现更优的展示效果;
4.瀑布流中的图片可使用img标签,也可使用背景图片(代码中注释的部分已标名)
实现方式:
一、创建瀑布流子组件
1.新建瀑布流组件的vue文件,如命名为WaterFall.vue
2.构建瀑布流中展示的内容,可根据不同需求进行自定义
<template>
<div class="list">
<div
class="item"
v-for="(item, index) in waterList"
:key="index"
:style="{
width: width + 'px',
height: item.height + 'px',
left: item.left + 'px',
top: item.top + 'px',
// 'background-image': 'url(' + item.image + ')', // 假设 item 对象中有 image 属性
// 'background-size': 'cover', // 如果需要的话,设置背景图片大小
// 'background-position': 'center', // 如果需要的话,设置背景图片位置
// 'background-repeat': 'no-repeat', // 禁止背景图片重复
}"
>
<img :src="item.image" :alt="item.text" />
<p class="text-box">{{ item.text }}</p>
</div>
</div>
</template>
3.构建瀑布流展示的相关方法
这里需要说明的是:为增加该瀑布流的可复用性,瀑布流数据通过父组件的list进行传递,boxWidth是指盛放瀑布流的盒子宽度,也是通过父组件进行传递,也可个人自己定义。父组件在下面也会给出详细代码。
这里将每个瀑布流里面的盒子设置的间距为右边20,下边20,第一行的数据进行了特殊处理,可根据个人需求调整。
<script setup>
import { ref, reactive, onMounted } from 'vue';
const props = defineProps({
list: {
type: Array,
default: () => {
return [];
},
},
boxWidth: {
type: String,
default: () => {
return '';
},
},
});
const width = 226; // 图片宽度
const gap = 20; // 图片上下间距
const rightGap = 20; // 图片右间距
const waterList = ref([]); // 瀑布流数组
const heightList = reactive([]); // 列高度数组
// 屏幕宽度需要在 mounted 之后拿到
onBeforeUpdate(() => {
// 计算列数
console.log('组件里获取的宽度', props.boxWidth);
const column = Math.floor(props.boxWidth / width); // 根据盒子的宽度确定列数
console.log('计算的列数', column);
// 核心内容就是维护每个图片的 left、top
for (let i = 0; i < props.list.length; i++) {
// 先铺上第一行(i < column 则表示是第一行)
if (i < column) {
props.list[i].top = 0;
props.list[i].left = i * (width + (i > 0 ? rightGap : 0));
// 塞进瀑布流
waterList.value?.push(props.list[i]);
// 高度数据更新
heightList[i] = props.list[i].height;
}
// 后面的就要一张张塞进去,每次找出最低的列往里塞
else {
// 最低的高度,先默认为第一列高度
let current = heightList[0];
// 最低的列,先默认为第一个
let col = 0;
// 循环每一列进行比较
heightList.forEach((h, i) => {
if (h < current) {
current = h;
col = i;
}
});
console.log('最低的列', col, '高度为', current);
// 由此计算出该图片的 left、top
props.list[i].left = col * (width + rightGap);
props.list[i].top = current + gap;
// 塞进瀑布流
waterList.value.push(props.list[i]);
// 更新列高度数组
heightList[col] = current + gap + props.list[i].height;
}
}
console.log('waterList', waterList.value);
console.log('heightList', heightList);
});
</script>
4.设置瀑布流的相关样式
可根据个人需求进行调整,这里展示的是上方效果图的样式
<style lang="scss" scoped>
.list {
position: relative;
height: 100%;
width: 100%;
.item {
position: absolute;
.text-box {
font-weight: 500;
font-size: 18px;
color: #000000;
}
img {
width: 100%;
height: 90%;
object-fit: cover;
}
}
}
</style>
二、创建使用瀑布流的父组件
1.新建父组件,如命名为index.vue
2.创建父组件的展示内容,分为左侧的菜单和右侧的瀑布流内容
isMenuBarFixed为控制左侧菜单根据右侧滚动条进行滑动的属性,menu-bar里面的内容为左侧菜单的内容,works-box为右侧瀑布流的内容,中间的<div style="width: 160px" v-if="isMenuBarFixed"></div>为当左侧菜单脱离文档流的占位内容,关于脱离文档流,想要学习的同学可参考这篇博客:CSS标准文档流和脱离文档流_css脱离文档流-CSDN博客
<template>
<div class="max-content">
<div class="main-content">
<div class="works-publicity">
<nav class="menu-bar" ref="menuBar" :class="{ fixed: isMenuBarFixed }">
<ul style="padding: 0">
<li
v-for="(item, index) in menuItems"
:key="index"
class="menu-item"
@click="setActiveItem(index)"
:class="{ active: activeIndex === index }"
>
<div class="menu-item-content">
<div
class="menu-item-border"
:class="{ blue: activeIndex === index }"
></div>
<img
:src="item.isActive ? item.activeImage : item.image"
class="menu-icon"
alt=""
/>
<div class="menu-text" :class="{ blue: activeIndex === index }">
{{ item.text }}
</div>
</div>
</li>
</ul>
</nav>
<div style="width: 160px" v-if="isMenuBarFixed"></div>
<div class="works-box" ref="myDiv">
<water-fall :list="list" :boxWidth="boxWidth" class="water-fall" />
</div>
</div>
</div>
</div>
</template>
3.父组件的相关方法
控制左侧瀑布流菜单根据右侧滚动条固定的相关关键代码可参考本篇博客的缩略版使用vue3实现右侧瀑布流滑动时左侧菜单的固定与取消固定-CSDN博客
<script setup>
import { cloneDeep } from 'lodash-es';
const config = useRuntimeConfig();
const myDiv = ref(null);
const menuBar = ref(null);
const isMenuBarFixed = ref(false);
const boxWidth = ref(''); // 展示瀑布流的盒子的宽度
const menuItems = ref([
{
text: '第一个',
image: '/images/works-publicity/industrial-design.png',
activeImage: '/images/works-publicity/industrial-design-active.png',
isActive: true,
},
{
text: '第二个',
image: '/images/works-publicity/handicrafts.png',
activeImage: '/images/works-publicity/handicrafts-active.png',
isActive: false,
},
{
text: '第三个',
image: '/images/works-publicity/painting.png',
activeImage: '/images/works-publicity/painting-active.png',
isActive: false,
},
{
text: '第四个',
image: '/images/works-publicity/photograph.png',
activeImage: '/images/works-publicity/photograph-active.png',
isActive: false,
},
{
text: '第五个',
image: '/images/works-publicity/geography.png',
activeImage: '/images/works-publicity/geography-active.png',
isActive: false,
},
{
text: '第六个',
image: '/images/works-publicity/tradition.png',
activeImage: '/images/works-publicity/tradition-active.png',
isActive: false,
},
]);
const list = [
{
height: 450,
background: 'red',
image: '/images/works-publicity/test1.png',
text: '平面作品+李宇轩',
},
{
height: 600,
background: 'pink',
text: '美术作品+沈佳宜',
image: '/images/works-publicity/test2.png',
},
{
height: 450,
background: 'blue',
image: '/images/works-publicity/test3.png',
text: '平面作品+李宇轩',
},
{
height: 350,
background: 'green',
image: '/images/works-publicity/test4.png',
text: '平面作品+李宇轩',
},
{
height: 500,
background: 'gray',
image: '/images/works-publicity/test5.png',
text: '平面作品+李宇轩',
},
{
height: 400,
background: '#CC00FF',
image: '/images/works-publicity/test6.png',
text: '平面作品+李宇轩',
},
{
height: 300,
background: 'pink',
image: '/images/works-publicity/test7.png',
text: '平面作品+李宇轩',
},
{
height: 600,
background: '#996666',
image: '/images/works-publicity/test8.png',
text: '平面作品+李宇轩',
},
{
height: 400,
background: 'gray',
image: '/images/works-publicity/test9.png',
text: '平面作品+李宇轩',
},
{
height: 400,
background: '#CC00FF',
image: '/images/works-publicity/test10.png',
text: '平面作品+李宇轩',
},
{
height: 500,
background: 'gray',
image: '/images/works-publicity/test11.png',
text: '平面作品+李宇轩',
},
{
height: 300,
background: '#996666',
image: '/images/works-publicity/test12.png',
text: '平面作品+李宇轩',
},
{
height: 600,
background: 'gray',
image: '/images/works-publicity/test13.png',
text: '平面作品+李宇轩',
},
{
height: 400,
background: '#CC00FF',
image: '/images/works-publicity/test1.png',
text: '平面作品+李宇轩',
},
{
height: 500,
background: 'gray',
image: '/images/works-publicity/test2.png',
text: '平面作品+李宇轩',
},
{
height: 300,
background: '#996666',
image: '/images/works-publicity/test3.png',
text: '平面作品+李宇轩',
},
{
height: 300,
background: 'gray',
image: '/images/works-publicity/test4.png',
text: '平面作品+李宇轩',
},
{
height: 400,
background: '#CC00FF',
image: '/images/works-publicity/test5.png',
text: '平面作品+李宇轩',
},
{
height: 500,
background: 'gray',
image: '/images/works-publicity/test6.png',
text: '平面作品+李宇轩',
},
{
height: 400,
background: '#996666',
image: '/images/works-publicity/test7.png',
text: '平面作品+李宇轩',
},
{
height: 350,
background: 'gray',
image: '/images/works-publicity/test8.png',
text: '平面作品+李宇轩',
},
{
height: 450,
background: '#CC00FF',
image: '/images/works-publicity/test9.png',
text: '平面作品+李宇轩',
},
{
height: 430,
background: 'gray',
image: '/images/works-publicity/test10.png',
text: '平面作品+李宇轩',
},
{
height: 600,
background: '#996666',
image: '/images/works-publicity/test11.png',
text: '平面作品+李宇轩',
},
];
const activeIndex = ref(0);
// 点击左侧菜单栏切换内容
const setActiveItem = (num) => {
menuItems.value.forEach((item, idx) => {
item.isActive = num === idx;
});
activeIndex.value = num;
};
// 监听滚动事件
const handleScroll = () => {
const scrollTopThreshold = 428;
const scrollTop = document.body.scrollTop; // 获取滚动位置
if (scrollTop >= scrollTopThreshold) {
isMenuBarFixed.value = true;
} else {
isMenuBarFixed.value = false;
}
};
onMounted(async () => {
await nextTick(); // 等待 DOM 更新
boxWidth.value = myDiv.value.offsetWidth; // 确保 myDiv 已经被定义并且 DOM 已经渲染
document.body.addEventListener('scroll', handleScroll); // 添加滚动事件监听器
});
// 组件卸载前
onUnmounted(() => {
document.body.removeEventListener('scroll', handleScroll()); // 移除滚动事件监听器
});
</script>
4.父组件内容的相关样式
<style lang="scss" scoped>
.works-publicity {
display: flex;
width: 100%;
padding: 20px 0;
box-sizing: border-box;
.menu-bar,
.fixed {
position: relative; /* 或者 static,取决于你的布局 */
top: 0; /* 确保它在页面顶部开始 */
transition: top 0.3s ease; /* 可选的过渡效果 */
display: flex;
justify-content: space-around; /* 根据需要调整菜单项间距 */
list-style: none;
padding: 0;
.menu-item {
display: flex;
position: relative;
align-items: center; /* 垂直居中图片和文字 */
justify-content: center;
cursor: pointer; /* 鼠标悬停时改变样式(可选) */
width: 160px;
height: 136px;
text-align: center;
border-left: 1px solid #d3dde9;
}
.menu-icon {
width: 32px; /* 根据图标大小调整 */
height: 32px; /* 根据图标大小调整 */
margin-bottom: 20px; /* 图片和文字之间的间距 */
}
.menu-item-content {
display: flex;
align-items: center;
flex-direction: column;
}
.menu-item-border {
position: absolute;
left: 0; /* 左边框位置 */
top: 0; /* 可以根据需要调整 */
bottom: 0; /* 可以根据需要调整 */
width: 3px; /* 边框宽度 */
background-color: transparent; /* 默认透明 */
}
.menu-item-border.blue {
background-color: var(--el-color-primary); /* 激活时变为蓝色 */
}
.menu-text.blue {
color: var(--el-color-primary); /* 激活时文字变为蓝色 */
}
}
.fixed {
/* 当固定时 */
position: fixed;
top: 0;
width: 160px; /* 或者你需要的宽度 */
z-index: 100; /* 确保它在其他内容之上 */
/* 其他样式,可能需要调整以适应固定定位 */
}
.works-box {
display: flex;
flex-direction: column;
width: 100%;
.water-fall {
margin-top: 30px;
}
}
}
</style>
这样,关于瀑布流的展示及滚动条滑动时,左侧菜单的灵活固定就完成啦!