带Tab的加载列表-状态保持改进方案.md 11.3 KB

带 Tab 的加载列表 - 状态保持改进方案

改进背景

当前实现在带 Tab 的加载列表(如搜索页面)中存在以下问题:

  1. 分页状态无法分别跟踪:使用单一的 currentPage ref,切换 tab 时会重置页码
  2. 滚动位置无法保持:使用页面级滚动,切换 tab 后滚动位置会丢失

用户期望的行为:

  • 在一个列表中滚动到某个位置(比如第 3 页)
  • 点击另一个 tab
  • 再切换回原来的 tab
  • 期望列表保持原来的滚动位置和分页状态,继续往下滚动时能从原来的位置加载

当前实现的问题

❌ 问题 1:分页状态无法分别跟踪

// src/pages/search/index.vue:168
const currentPage = ref(0)  // 单一页码,无法分别跟踪两个tab

// 切换 tab 时重置
const onTabClick = async (tabId) => {
  // ...
  currentPage.value = 0  // ❌ 重置为0,丢失了原来的页码
}

影响

  • 产品列表滚动到第 3 页
  • 切换到资料列表
  • 再切回产品列表
  • 页码变成 0,需要重新从第 1 页开始

❌ 问题 2:滚动位置无法保持

当前状态(2026-02-08)

  • LoadMoreList 组件已使用 scroll-view(第39-75行)
  • 尚未实现滚动位置的保存和恢复逻辑
  • 切换 tab 后,滚动位置仍然会重置

当前实现

<!-- src/components/LoadMoreList/index.vue:39-47 -->
<scroll-view
  v-if="enableScrollLoad && list.length > 0"
  class="load-more-content scrollable"
  :style="{ height: scrollHeight }"
  :scroll-y="true"
  lower-threshold="100"
  @scrolltolower="handleScrollToLower"
>
  <!-- 列表内容 -->
</scroll-view>

缺少的功能

  • ❌ 没有 scrollTop prop 控制滚动位置
  • ❌ 没有 @scroll 事件监听滚动
  • ❌ 没有滚动位置的保存和恢复机制

影响

  • 列表滚动到某个位置
  • 切换 tab(列表重新渲染)
  • 滚动位置会重置到顶部

✅ 问题 3:数据可以保持

列表数据本身会被保留:

// src/pages/search/index.vue:159-162
const products = ref([])  // 产品列表数据
const files = ref([])     // 资料列表数据

但这还不够,因为分页状态和滚动位置都丢失了。

改进方案

核心思路

  1. 为每个 tab 维护独立的分页状态(page、hasMore)
  2. 使用 scroll-view 替代页面级滚动,并保存每个 tab 的滚动位置
  3. 切换 tab 时保存和恢复滚动位置
  4. 切换 tab 时不重新请求已有数据

实现代码

1. 状态管理改进

// ❌ 原来的实现(单一状态)
// const currentPage = ref(0)
// const hasMore = ref(true)

// ✅ 改进方案:为每个 tab 维护独立的状态
const tabState = ref({
  product: {
    page: 0,
    hasMore: true,
    scrollTop: 0  // 保存滚动位置
  },
  file: {
    page: 0,
    hasMore: true,
    scrollTop: 0
  }
})

// 当前的 page 和 hasMore 从 tabState 中获取
const currentPage = computed(() => {
  return activeTab.value ? tabState.value[activeTab.value].page : 0
})

const hasMore = computed(() => {
  return activeTab.value ? tabState.value[activeTab.value].hasMore : true
})

2. LoadMoreList 组件改进

<template>
  <view class="load-more-list" :class="{ 'has-header': showHeader }">
    <!-- 可选固定头部 -->
    <view v-if="showHeader" class="load-more-header sticky top-0 z-10">
      <slot name="header"></slot>
    </view>

    <!-- 使用 scroll-view 替代普通 view -->
    <scroll-view
      class="load-more-content"
      :class="{ 'no-padding': noPadding }"
      :scroll-top="scrollTop"
      scroll-y
      @scroll="handleScroll"
      @scrolltolower="handleScrollToLower"
    >
      <!-- 列表内容(保持不变) -->
      <view v-if="loading && list.length === 0">
        <!-- loading -->
      </view>

      <view v-else class="list-container">
        <view
          v-for="(item, index) in displayList"
          :key="item[keyField] || index"
          class="list-item"
        >
          <slot name="item" :item="item" :index="index"></slot>
        </view>

        <!-- 空状态和加载更多提示 -->
      </view>
    </scroll-view>
  </view>
</template>

<script setup>
import { computed } from 'vue'
import { usePullDownRefresh, stopPullDownRefresh } from '@tarojs/taro'

const props = defineProps({
  // ... 原有 props
  scrollTop: {
    type: Number,
    default: 0
  }
})

const emit = defineEmits(['load-more', 'refresh', 'scroll'])

/**
 * scroll-view 滚动事件
 * @param {Object} e - 滚动事件对象
 */
const handleScroll = (e) => {
  emit('scroll', e)
}

/**
 * scroll-view 触底事件
 */
const handleScrollToLower = () => {
  // 如果正在加载或没有更多数据,不执行
  if (props.loadingMore || props.loading || !props.hasMore) {
    return
  }

  console.log('[LoadMoreList] 触底加载更多,当前页:', props.page, '下一页:', props.page + 1)
  const nextPage = props.page + 1
  emit('load-more', nextPage)
}

// ... 其他代码保持不变
</script>

<style lang="less">
.load-more-content {
  // 设置固定高度,使 scroll-view 正常工作
  height: calc(100vh - 200rpx);
  padding: 32rpx;

  &.no-padding {
    padding: 0;
  }
}

// ... 其他样式保持不变
</style>

3. 搜索页面改进

// scroll-view 的滚动位置
const scrollViewScrollTop = ref(0)

/**
 * scroll-view 滚动事件
 * @param {Object} e - 滚动事件对象
 */
const handleScroll = (e) => {
  if (!activeTab.value) return

  // 保存当前 tab 的滚动位置
  tabState.value[activeTab.value].scrollTop = e.detail.scrollTop
}

/**
 * Tab 点击处理(改进版)
 * @param {string} tabId - Tab ID
 */
const onTabClick = async (tabId) => {
  if (activeTab.value === tabId) return

  // 保存当前 tab 的滚动位置(在切换前)
  if (activeTab.value) {
    tabState.value[activeTab.value].scrollTop = scrollViewScrollTop.value
  }

  // 切换 tab
  activeTab.value = tabId

  // 恢复新 tab 的滚动位置
  scrollViewScrollTop.value = tabState.value[tabId].scrollTop

  // 如果没有搜索过,不执行
  if (!hasSearched.value || !searchKeyword.value.trim()) {
    return
  }

  // 检查当前 tab 是否已有数据,如果有则不重新请求
  const currentData = activeTab.value === 'product' ? products.value : files.value
  if (currentData.length > 0) {
    console.log('[Search] Tab 已有数据,不重新请求')
    return
  }

  // 如果没有数据,则请求
  console.log('[Search] Tab 无数据,发起请求:', tabId, '页码:', tabState.value[tabId].page)
  await performSearch(
    searchKeyword.value.trim(),
    tabId,
    tabState.value[tabId].page,
    pageSize,
    false
  )
}

/**
 * 处理加载更多事件
 * @param {number} page - 下一页页码
 */
const handleLoadMore = async (page) => {
  console.log('[Search] 加载更多,tab:', activeTab.value, '页码:', page)

  if (!activeTab.value) return

  // 更新当前 tab 的 page
  tabState.value[activeTab.value].page = page

  // 如果没有搜索过或没有选中 tab,不执行
  if (!hasSearched.value || !activeTab.value || !searchKeyword.value.trim()) {
    return
  }

  // 加载下一页数据
  await performSearch(
    searchKeyword.value.trim(),
    activeTab.value,
    page,
    pageSize,
    true  // 标记为加载更多
  )
}

改进效果

✅ 改进后

  1. 分页状态独立:每个 tab 都有自己的 page 和 hasMore
  2. 滚动位置保持:切换 tab 后再切回,滚动位置会恢复到原来的位置
  3. 不重复请求:如果 tab 已经有数据,切换时不会重新请求
  4. 无缝体验:用户可以在不同 tab 之间自由切换,状态完全保持

使用场景

这个改进方案适用于所有带 Tab 的加载列表场景:

  • 搜索页面(产品/资料)
  • 订单列表(待付款/待发货/已完成)
  • 消息列表(系统消息/订单消息)
  • 任何带 Tab 的分页列表

实施优先级

⚠️ 当前不实施(2026-02-08 状态)

不实施的原因

  1. 需求不明确

    • 客户/产品经理未提出明确需求
    • 尚未收到用户反馈或投诉
    • 业务价值不明确
  2. 技术储备充足

    • 方案设计已完成
    • 实施步骤清晰
    • 可随时在需求明确后快速实施
  3. 优先级评估

    • 当前有更高优先级的任务
    • 此功能属于体验优化,非核心功能
    • 建议等待用户反馈后再决定

何时需要实施

建议在以下情况下启动实施

  1. 用户明确反馈

    • 用户投诉"切换 tab 后要重新滚动"
    • 用户反馈"能不能记住我之前的浏览位置"
    • 用户满意度调查显示此问题影响体验
  2. 产品需求明确

    • 产品经理提出"tab 状态保持"需求
    • 产品文档中明确要求此功能
    • 竞品已实现类似功能
  3. 体验优化阶段

    • 核心功能已完成,进入体验优化阶段
    • 需要提升用户留存和使用时长
    • 需要在竞品对比中脱颖而出
  4. 技术债务清理

    • 定期技术债务审查时发现此问题
    • 代码重构时顺便优化此功能

实施工作量评估

如果确定要实施,预计工作量:

任务 预计时间 难度
LoadMoreList 组件改造 1-2 小时 🟡 中
搜索页面状态管理改造 2-3 小时 🟠 较高
滚动位置保存/恢复逻辑 1-2 小时 🟡 中
测试和调试 1-2 小时 🟢 低
文档更新 0.5 小时 🟢 低
总计 5.5-9.5 小时

风险评估

风险项 风险等级 缓解措施
scroll-view scrollTop 属性不生效 🟡 中 使用 ref + nextTick 技巧确保值变化
滚动位置精度问题 🟢 低 使用 Math.floor() 取整
首次进入 tab 跳动问题 🟡 中 检查数据是否为空,初始化 scrollTop
下拉刷新时重置问题 🟢 低 刷新时显式重置滚动位置和页码
兼容性问题(iOS/Android) 🟡 中 在真机上充分测试

实施建议

如果确定要实施,建议遵循以下步骤:

  1. 先做技术验证(0.5-1 小时):

    • 创建一个简单的 demo 验证 scroll-view 的 scrollTop 功能
    • 测试在不同设备(iOS/Android)上的表现
    • 确认技术可行性
  2. 分阶段实施

    • 阶段 1:先实现分页状态独立跟踪(不涉及滚动位置)
    • 阶段 2:再实现滚动位置保存和恢复
    • 阶段 3:优化体验(添加过渡动画等)
  3. 充分测试

    • 在真机上测试不同场景
    • 测试边界情况(快速切换 tab、下拉刷新、触底加载)
    • 性能测试(大量数据时的表现)

技术储备说明

本文档已作为技术储备保存,包含:

  • ✅ 完整的问题分析
  • ✅ 详细的改进方案
  • ✅ 可直接使用的代码示例
  • ✅ 工作量评估和风险分析

当需求明确时,可以直接参考本文档快速实施。

参考文件

  • 搜索页面:src/pages/search/index.vue
  • LoadMoreList 组件:src/components/LoadMoreList/index.vue

文档维护记录

  • 创建日期:2026-02-08
  • 最后更新:2026-02-08
  • 维护人:Claude Code
  • 状态:需求不明确,暂不实施
  • 下次审查:收到用户反馈或产品需求时