vue2+element 实现无限滚动及搜索栏吸顶
使用element的 Infinite Scroll 无限滚动实现触底加载。滚动容器:集成无限滚动指令,统一处理内容区域与搜索栏的滚动事件。粘性定位实现,粘性定位是当元素达到指定的偏移位置时,它就会像。滚动时根据滚动位置计算两个搜索栏的透明度,实现渐变切换效果。支持重置加载(搜索触发)和增量加载(滚动触发)双模式。紧凑式搜索栏:滚动时动态显示的固定定位搜索组件。自定义滚动监听器实现搜索栏动态过渡效
实现效果:
使用顶部搜索区域样式
使用固定搜索区域样式
代码实现:
<template>
<!-- 主容器:用于素材展示和滚动 -->
<div ref="contentRef" class="content-box">
<!-- 无限滚动容器:当滚动到底部时触发加载更多 -->
<div v-infinite-scroll="handleSearch" :infinite-scroll-disabled="disabled">
<!-- 顶部搜索区域:带背景图的大搜索框 -->
<div ref="searchHeader" class="search-box-top">
<h1 class="search-title">
搜索素材
</h1>
<el-input
v-model="query.name"
class="material-top-input"
placeholder="请输入素材名称"
>
<template #suffix>
<i
class="el-icon-search search-icon"
@click="handleSearch('reset')"
/>
</template>
</el-input>
</div>
<!-- 固定搜索区域:滚动时显示在顶部的小搜索框 -->
<div ref="searchBoxFull" class="search-box-full">
<el-input
v-model="query.name"
class="search-full-input"
placeholder="请输入素材名称"
>
<template #suffix>
<i
class="el-icon-search search-icon"
@click="handleSearch('reset')"
/>
</template>
</el-input>
</div>
<!-- 素材卡片列表:网格布局展示素材 -->
<div class="material-list">
<div
v-for="item in cardList"
:key="item.id"
class="material-card"
>
<div class="card-content">
<div class="img-box">
<img :ref="`img${item.id}`" :src="item.imgUrl" alt="素材图片">
</div>
</div>
</div>
<p
v-if="noMore"
class="no-more-text"
>
没有更多了
</p>
</div>
</div>
</div>
</template>
<script>
// 引入素材列表分页查询接口
import { materialChnPageList } from '@/api/material'
export default {
name: 'MaterialShow',
data() {
return {
loading: false, // 数据加载状态标识
noMore: false, // 是否已加载全部数据标识
pagination: {
currentPage: 1, // 当前页码
pageSize: 20 // 每页显示条数
},
cardList: [], // 素材卡片数据列表
query: {
name: '' // 搜索关键词
},
offsetTop: 0 // 顶部搜索框的高度,用于计算滚动动画
}
},
computed: {
// 控制无限滚动是否禁用:加载中或无更多数据时禁用
disabled() {
return this.loading || this.noMore
}
},
mounted() {
// 初始化加载数据
this.handleSearch('reset')
this.$nextTick(() => {
// 获取顶部搜索框高度并添加滚动监听
this.offsetTop = this.$refs.searchHeader.offsetHeight
this.$refs.contentRef.addEventListener('scroll', this.handleScroll, true)
})
},
beforeDestroy() {
// 组件销毁前移除滚动监听,防止内存泄漏
this.$refs.contentRef.removeEventListener('scroll', this.handleScroll, true)
},
methods: {
/**
* 处理页面滚动事件
* 根据滚动位置计算两个搜索框的透明度,实现渐变切换效果
*/
handleScroll() {
const scrollTop = this.$refs.contentRef.scrollTop
let opacity = 1
let otherOpacity = 0
// 根据滚动位置计算透明度
if (scrollTop > this.offsetTop) {
opacity = 0
otherOpacity = 1
} else {
// 当滚动到searchHeader最后120px时开始渐变
const fadeStartPoint = this.offsetTop - 120
// 当滚动到searchHeader最后64px时完成渐变
const fadeEndPoint = this.offsetTop - 64
if (scrollTop > fadeStartPoint) {
if (scrollTop > fadeEndPoint) {
// 已完成渐变
opacity = 0
otherOpacity = 1
} else {
// 计算渐变进度 (0-1)
const progress = (scrollTop - fadeStartPoint) / (fadeEndPoint - fadeStartPoint)
opacity = 1 - progress
otherOpacity = progress
}
} else {
opacity = 1
otherOpacity = 0
}
}
// 应用透明度效果
this.$refs.searchHeader.style = `opacity:${opacity}`
this.$refs.searchBoxFull.style.opacity = otherOpacity
this.$refs.searchBoxFull.style['z-index'] = otherOpacity <= 0 ? -1 : 2
},
/**
* 处理搜索和加载更多数据
* @param {string} operateType - 操作类型:'reset'重置列表,'append'追加数据
*/
async handleSearch(operateType = 'append') {
this.loading = true
// 重置搜索时重置页码
if (operateType === 'reset') {
this.pagination.currentPage = 1
}
try {
// 调用接口获取数据
const { data } = await materialChnPageList({ ...this.pagination, ...this.query })
// 重置搜索时滚动到顶部
if (operateType === 'reset') {
this.$refs.contentRef.scrollTo(0, 0)
}
// 处理返回的数据列表
const arr = data.list || []
this.cardList = operateType === 'reset' ? [...arr] : [...this.cardList, ...arr]
// 判断是否还有更多数据
this.noMore = Number(this.pagination.pageSize) * Number(this.pagination.currentPage || 1) >= Number(data.total || 0)
this.pagination.currentPage += 1
} catch (error) {
if (operateType === 'reset') {
this.noMore = true
}
} finally {
this.loading = false
}
}
}
}
</script>
<style lang="scss" scoped>
/* Element UI 组件样式重写 */
::v-deep {
.el-input__inner {
padding: 0;
border: none;
&::placeholder {
color: rgba(0, 0, 0, 0.25);
}
}
.el-input__suffix {
right: 12px;
display: flex;
align-items: center;
}
}
/* 页面主要布局样式 */
.content-box {
position: relative;
height: 600px;
display: flex;
flex-direction: column;
overflow: auto;
background-color: #f0f1f4;
}
/* 搜索框相关样式 */
.search-box-top {
border-radius: 8px;
flex-shrink: 0;
background-image: url("../../../assets/material-bg.png");
width: 100%;
height: 272px;
background-size: cover;
background-position: right;
background-repeat: no-repeat;
padding: 52px 0 0 54px;
.search-title {
color: #000;
line-height: 32px;
font-size: 24px;
margin-bottom: 16px;
}
.material-top-input {
width: 537px;
height: 56px;
line-height: 56px;
padding: 0 36px 0 24px;
border-radius: 12px;
background: #fff;
font-size: 16px;
}
}
.search-box-full {
opacity: 0;
position: fixed;
box-sizing: border-box;
z-index: 2;
display: flex;
top: 56px;
right: 0;
transition: width 0.28s;
width: calc(100% - 54px);
background: #fff;
border: 1px solid #fff;
padding: 16px 24px;
align-items: center;
.search-full-input {
height: 32px;
background: #fff;
border-radius: 136px;
border: 1px solid rgba(0, 5, 27, 0.15);
overflow: hidden;
padding: 0 36px 0 12px;
}
}
.search-icon {
font-size: 20px;
cursor: pointer;
color: #00051b;
opacity: 0.65;
}
/* 素材卡片网格布局 */
.material-list {
display: flex;
padding: 16px;
flex-wrap: wrap;
}
.material-card {
overflow: hidden;
margin-right: 12px;
margin-bottom: 14px;
border-radius: 10px;
padding: 10px;
font-weight: 600;
font-size: 16px;
color: rgba(0, 5, 27, 0.65);
line-height: 24px;
background: #fff;
box-shadow: 0 4px 10px 0 rgba(0, 0, 0, 0.1);
.img-box {
background-color: #fff;
width: 100%;
position: relative;
overflow: hidden;
border-radius: 8px;
&::after {
content: "";
display: block;
padding-bottom: 125%;
}
img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: auto;
}
}
}
.no-more-text {
width: 100%;
line-height: 20px;
color: rgba(0, 5, 27, 0.45);
margin: 0 0 12px 0;
text-align: center;
font-size: 14px;
}
/* 响应式布局:根据不同屏幕宽度调整卡片列数 */
@media (max-width: 1440px) {
.material-card {
width: calc(20% - 12px * 4 / 5);
&:nth-child(5n) {
margin-right: 0;
}
}
}
@media (min-width: 1440px) and (max-width: 1920px) {
.material-card {
width: calc(100% / 6 - 12px * 5 / 6);
&:nth-child(6n) {
margin-right: 0;
}
}
}
@media (min-width: 1920px) {
.material-card {
width: calc(100% / 7 - 12px * 6 / 7);
&:nth-child(7n) {
margin-right: 0;
}
}
}
</style>
实现说明:
一、架构设计
1、组件分层结构
-
滚动容器:集成无限滚动指令,统一处理内容区域与搜索栏的滚动事件
-
双模式搜索栏:
-
沉浸式搜索区:初始状态展示,含背景图与大尺寸输入框
-
紧凑式搜索栏:滚动时动态显示的固定定位搜索组件
-
-
响应式网格:采用CSS媒体查询实现跨分辨率适配布局
-
三阶段自适应布局:
-
≤1440px:5列网格
-
1440-1920px:6列网格
-
≥1920px:7列网格
-
-
通过nth-child选择器精准控制换行间距
-
2、滚动控制机制
-
使用element的 Infinite Scroll 无限滚动实现触底加载
-
通过
infinite-scroll-disabled
属性智能控制请求节流 -
自定义滚动监听器实现搜索栏动态过渡效果
二、核心交互逻辑
1、双搜索栏渐变过渡
-
滚动时根据滚动位置计算两个搜索栏的透明度,实现渐变切换效果
-
动态计算z-index保证元素堆叠顺序
2、数据加载控制
-
支持重置加载(搜索触发)和增量加载(滚动触发)双模式
-
重置加载时需要让滚动条回到顶部
-
内置请求锁机制防止重复调用
三、注意事项
1、定位方案选择
-
简单场景推荐使用
position: sticky
粘性定位实现,粘性定位是当元素达到指定的偏移位置时,它就会像position: fixed
固定定位那样固定在页面上,不用监听滚动 -
复杂交互动画需配合动态样式计算(如本实现)
2、滚动边界处理
-
避免使用负margin值影响滚动计算
-
需要时可配置
infinite-scroll-distance
补偿偏移量
3、内存管理
-
组件销毁时务必移除事件监听
更多推荐
所有评论(0)