app页面-锚点滚动 和 滚动自动激活菜单

页面

锚点滚动 和 滚动自动激活菜单视频案例

javascript 复制代码
<template>
   <view class="container">
      <!-- 锚点导航 -->
      <view class="anchor-tabs">
         <view
            v-for="tab in subjectTabs" 
            :key="tab.id"
            class="tab"
            :class="[currentSubject === tab.id ? 'activeTab': '']"
            @click="switchSubjectTab(tab.id)"
         >{{ tab.subjectName }}</view>
      </view>

    <!-- 滚动内容区域 -->
   <scroll-view 
      class="scroll-view"
      :scroll-into-view="currentSubject"
      scroll-y 
      :scroll-with-animation="true"
      @scroll="onScroll"
   >
      <view 
         v-for="section in subjectRightSections" 
         :id="section.id"
         :key="section.id"
         class="content-section"
      ></view>
   </scroll-view>
  </view>
</template>
<script>
export default {
 data() {
    return {
      currentSubject: 'sec_0', // 当前滚动到的学科元素id
      distances: null, // scrollVie里面所有区块距离信息对象
      subjectTabs: [], // 学科tabs(根据状态和 学段 查出 所有学科数据,并对所有学科进行集合)
    }
 },
 computed: {
    // 查询出的所有学科数据id集合
    contentSectionsIds() {
      const subjectRightSectionsIds = this.subjectRightSections.map(element => element.id);
      return subjectRightSectionsIds
    },
  },
  onShow() {
    this.getRightsCardList()
  },
  methods: {
    // 获取所有学科的权益数据
    async getRightsCardList() {
      // 获取正确的购买信息(学段,年级)
      const { data } = await rightsQA.getRightsCardList({
        status: this.currentStatus,
        studyStageType: this.currentStudyStage + 1
      })
      const { rightsInfoList, rightsStatusTotalList, studyStageType, subjectList } = data
      // 3: 处理状态 + 学段 下的学科list
      this.subjectTabs = subjectList.filter(item => {
        // 给学科数据添加id
        item['id'] = `sec_${item.subjectType}`
        return item
      })

      // 4: 权益数据list
      this.subjectTabs.forEach(item => {
        this.subjectRightSections.push({
          id: item.id,
          subjectType: item.subjectType, // 学科枚举,多学科有唯一id
          subject: item.subject, // 多学科使用逗号分隔,学科枚举集合
          subjectName: item.subjectName,
          subjectRightList: this.rightsInfoListFun(item.subjectType,rightsInfoList)
        })
      })

      // 5: 处理默认激活学科
      this.currentSubject = this.subjectTabs[0]?.id

     // 6: 获取滚动区域中所有内容区块的距离信息
      if (rightsInfoList && rightsInfoList.length > 0) {
        this.getAllSectionsDistance()
      } else {
        this.noRightShow = true // 么有数据展示无数据背景图
      }
    },
    // 整理各个学科权益list数据
    rightsInfoListFun(subjectType, rightsInfoList) {
      // 筛选某个学科的所有权益list
      let subjectRightList = rightsInfoList.filter(rightsItem => rightsItem.subjectType === subjectType)
      /*
        1: rightType: 权益类型(付费权益卡:【0.付费】; 图书权益卡:【1.体验 2.正式 3.预售】)
        2: 付费购买的答疑卡放在图书免费配赠权益的上面【升序排序】
        3: 付费购买的答疑卡按照购买答疑卡的时间倒序排列
        4: 图书免费配赠的答疑权益按照图书激活的时间倒序排列。
      */
      const subjectPayRightList = subjectRightList.filter(rightsItem => rightsItem.rightType === 0).sort((a, b) => {
        return new Date(b.rightsPayTime) - new Date(a.rightsPayTime);
      });
      const subjectFreeRightList = subjectRightList.filter(rightsItem => rightsItem.rightType !== 0).sort((a, b) => {
        return new Date(b.bookUserCreateTime) - new Date(a.bookUserCreateTime);
      });
      return [...subjectPayRightList, ...subjectFreeRightList]
    },
    // 学科tab切换,自动滚动到对应学科部分
    switchSubjectTab(sectionId) {
      this.currentSubject = sectionId;
    },
    // 步骤1: 获取滚动区域中所有内容区块的距离信息
    async getAllSectionsDistance() {
      this.$nextTick(async() => {
        try {
          // 使用 Promise 和 await 让异步代码更清晰
          const [scrollViewRect, ...targetRects] = await Promise.all([
            this.getBoundingClientRect('.scroll-view'),
            ...this.contentSectionsIds.map(id =>
              this.getBoundingClientRect(`#${id}`)
            )
          ]);

          if (scrollViewRect) {
            // 计算每个元素距离 scroll-view 顶部的距离
            const distances = this.contentSectionsIds.map((id, index) => {
              const targetRect = targetRects[index];
              let distanceObj = {
                id: id,
                distance: null,
                error: '元素未找到'
              }
              if (targetRect) {
                distanceObj = {
                  id: id,
                  distance: targetRect.top - scrollViewRect.top,
                  elementTop: targetRect.top, // 当前元素节点的上边界坐标
                  scrollViewTop: scrollViewRect.top // scrollView元素节点的上边界坐标
                };
              }
              return distanceObj;
            });
            console.log('所有区块距离信息:', distances);
            this.distances = distances
          }

          // if (targetRect && scrollViewRect) {
          //   // 计算目标元素顶部相对于 scroll-view 顶部的距离
          //   // 因为 boundingClientRect 返回的是相对于视口顶部的距离,所以做减法
          //   this.distance = targetRect.top - scrollViewRect.top;
          //   console.log('距离:', this.distance);
          // }
        } catch (error) {
          console.error('获取节点信息失败:', error);
        }
      });
    },
    // 封装获取节点边界信息的方法
    getBoundingClientRect(selector) {
      return new Promise((resolve) => {
        // 创建查询请求
        uni.createSelectorQuery()
          .in(this) // 限定在当前组件实例中查找
          .select(selector)
          .boundingClientRect((rect) => {
            // 如果节点存在,rect 是一个对象,包含位置和尺寸信息;否则为 null
            if (rect) {
              resolve(rect);
            } else {
              resolve(null);
            }
          })
          .exec(); // 执行查询
      });
    },
    // 步骤2: scroll滚动事件回调,自动激活对应区域学科
    onScroll(e) {
      const { scrollTop, scrollHeight } = e.detail;
      // 常用的属性包括:
      // scrollTop: 竖向滚动条位置
      // scrollLeft: 横向滚动条位置
      // scrollHeight: 滚动内容的高度
      // scrollWidth: 滚动内容的宽度
      // deltaX, deltaY: 前一次滚动事件的偏移量
      // 例如:计算已滚动比例
      // const scrollRatio = scrollTop / scrollHeight;
      // if (!this.distances) {
      //   this.getAllSectionsDistance()
      //   return
      // }
      console.log('竖向滚动距离顶部距离:', scrollTop);
      const currentSection = this.findInterval(scrollTop, this.distances)
      if (currentSection) {
        console.log(`当前在区块 ${JSON.stringify(currentSection)}`);
        console.log(`当前在区块序号 ${currentSection.index + 1}`);
        this.currentSubject = currentSection.id;
      }
    },
    // 步骤3: 计算滚动距离 和 每个科学id元素的坐标 直接的差值,并确定当前滚动在那个科学id区域内
    findInterval(number1, intervals) {
      let number = number1 + 5 // 给一个误差范围5
      // let number = number1
      // 确保区间数组是升序排列的
      const sortedIntervals = [...intervals].sort((a, b) => a.distance - b.distance);
      for (let i = 0; i < sortedIntervals.length - 1; i++) {
        if (number >= sortedIntervals[i].distance && number < sortedIntervals[i + 1].distance) {
          return {
            interval: [sortedIntervals[i].distance, sortedIntervals[i + 1].distance],
            index: i,
            id: sortedIntervals[i].id,
            description: `在区间 [${sortedIntervals[i].distance}, ${sortedIntervals[i + 1].distance}) 内`
          };
        }
      }
       
        // 如果数字小于第一个区间点
        if (number < sortedIntervals[0].distance) {
          return {
            interval: [-Infinity, sortedIntervals[0].distance],
            index: -1,
            id: sortedIntervals[0].id,
            description: `小于 ${sortedIntervals[0].distance}`
          };
        }
        
        // 如果数字大于最后一个区间点
        if (number >= sortedIntervals[sortedIntervals.length - 1].distance) {
          return {
            interval: [sortedIntervals[sortedIntervals.length - 1].distance, Infinity],
            index: sortedIntervals.length - 1,
            id: sortedIntervals[sortedIntervals.length - 1].id,
            description: `大于等于 ${sortedIntervals[sortedIntervals.length - 1].distance}`
          };
        }

      return null;
    },

  }
}
</script>
<style lang="scss" scoped>
.scroll-view {
  flex: 1; /* 撑满剩余空间 */
  // height: 100%; /* 重要:设置高度 */
  // height: calc(100vh - 400rpx);
  height: calc(100vh - 470rpx);
}
.content-section:last-child {
  height: 100% !important;
}
</style>
相关推荐
AAA阿giao1 小时前
在你的网页中嵌入 Coze 智能客服:一步步打造专属 AI Agent
前端·javascript·人工智能
AAA阿giao1 小时前
深入解析 OOP 考题之 EditInPlace 类:从零开始掌握面向对象编程实战
前端·javascript·dom
时71 小时前
利用requestIdleCallback优化Dom的更新性能
前端·性能优化·typescript
西西学代码1 小时前
flutter---进度条(2)
前端·javascript·flutter
Apeng_09191 小时前
vue+canvas实现按下鼠标绘制箭头
前端·javascript·vue.js
QuantumLeap丶1 小时前
《Flutter全栈开发实战指南:从零到高级》- 21 -响应式设计与适配
android·javascript·flutter·ios·前端框架
wordbaby1 小时前
组件与外部世界的桥梁:一文读懂 useEffect 的核心机制
前端·react.js
wordbaby1 小时前
永远不要欺骗 React:详解 useEffect 依赖规则与“闭包陷阱”
前端·react.js
火星数据-Tina1 小时前
体彩数据API
前端·websocket