带Tab的加载列表-状态保持改进方案.md 7.77 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:滚动位置无法保持

LoadMoreList 组件使用的是页面级滚动,而非 scroll-view 组件:

<!-- src/components/LoadMoreList/index.vue:39 -->
<view class="load-more-content">
  <!-- 列表内容 -->
</view>

影响

  • 列表滚动到某个位置
  • 切换 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 的分页列表

实施优先级

⚠️ 当前不实施:客户还未提出明确需求,暂时作为技术储备保存。

📝 何时需要实施

  • 用户反馈切换 tab 时状态丢失
  • 产品需求明确要求保持 tab 状态
  • 需要提升用户体验时

参考文件

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