实现效果:

使用顶部搜索区域样式

使用固定搜索区域样式

代码实现:

<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、内存管理

  • 组件销毁时务必移除事件监听

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐