Toggle navigation
Toggle navigation
This project
Loading...
Sign in
Hooke
/
manulife-weapp
Go to a project
Toggle navigation
Toggle navigation pinning
Projects
Groups
Snippets
Help
Project
Activity
Repository
Graphs
Network
Create a new issue
Commits
Issue Boards
Authored by
hookehuyr
2026-02-08 10:46:38 +0800
Browse Files
Options
Browse Files
Download
Email Patches
Plain Diff
Commit
b845b329ab5bbd0040e9d029a6c4f76bbb82c53b
b845b329
1 parent
5586d7b3
refactor(config): 统一使用 picsum.photos 占位图服务
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
323 additions
and
7 deletions
docs/improvements/带Tab的加载列表-状态保持改进方案.md
src/utils/mockData.js
docs/improvements/带Tab的加载列表-状态保持改进方案.md
0 → 100644
View file @
b845b32
# 带 Tab 的加载列表 - 状态保持改进方案
## 改进背景
当前实现在带 Tab 的加载列表(如搜索页面)中存在以下问题:
1.
**分页状态无法分别跟踪**
:使用单一的
`currentPage`
ref,切换 tab 时会重置页码
2.
**滚动位置无法保持**
:使用页面级滚动,切换 tab 后滚动位置会丢失
用户期望的行为:
-
在一个列表中滚动到某个位置(比如第 3 页)
-
点击另一个 tab
-
再切换回原来的 tab
-
期望列表保持原来的滚动位置和分页状态,继续往下滚动时能从原来的位置加载
## 当前实现的问题
### ❌ 问题 1:分页状态无法分别跟踪
```
javascript
// 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`
组件:
```
vue
<!-- src/components/LoadMoreList/index.vue:39 -->
<view class="load-more-content">
<!-- 列表内容 -->
</view>
```
**影响**
:
-
列表滚动到某个位置
-
切换 tab(列表重新渲染)
-
页面滚动位置会重置到顶部
### ✅ 问题 3:数据可以保持
列表数据本身会被保留:
```
javascript
// 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. 状态管理改进
```
javascript
// ❌ 原来的实现(单一状态)
// 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 组件改进
```
vue
<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. 搜索页面改进
```
javascript
// 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`
src/utils/mockData.js
View file @
b845b32
...
...
@@ -110,7 +110,7 @@ function generateWeekHotItem(id) {
return
{
meta_id
:
id
,
name
:
`
${
materialName
}
${
fileType
.
name
.
toUpperCase
()}
`
,
src
:
`https://p
lacehold.co/100x100/e2e8f0/475569?text=
${
fileType
.
extension
.
toUpperCase
()}
`
,
src
:
`https://p
icsum.photos/seed/material-
${
id
}
-
${
fileType
.
extension
}
/100/100
`
,
size
:
generateRandomSize
(),
read_people_count
:
generateRandomReadCount
(),
read_people_percent
:
generateRandomReadPercent
(),
...
...
@@ -188,10 +188,10 @@ function generateMaterialItem(id) {
size
:
generateRandomSize
(),
extension
:
fileType
.
extension
,
collected
:
generateRandomFavorite
()
===
'1'
,
src
:
`https://p
lacehold.co/100x100/e2e8f0/475569?text=
${
fileType
.
extension
.
toUpperCase
()}
`
,
downloadUrl
:
`https://p
lacehold.co/100x100/e2e8f0/475569?text=
${
fileType
.
extension
.
toUpperCase
()}
`
,
src
:
`https://p
icsum.photos/seed/file-
${
id
}
-
${
fileType
.
extension
}
/100/100
`
,
downloadUrl
:
`https://p
icsum.photos/seed/file-
${
id
}
-
${
fileType
.
extension
}
/100/100
`
,
post_date
:
new
Date
().
toISOString
(),
value
:
`https://p
lacehold.co/100x100/e2e8f0/475569?text=
${
fileType
.
extension
.
toUpperCase
()}
`
value
:
`https://p
icsum.photos/seed/file-
${
id
}
-
${
fileType
.
extension
}
/100/100
`
}
}
...
...
@@ -302,7 +302,7 @@ function generateProductItem(id) {
id
:
id
,
product_name
:
productName
,
name
:
productName
,
cover_image
:
`https://p
lacehold.co/400x300/4caf50/ffffff?text=
${
encodeURIComponent
(
productName
.
substring
(
0
,
6
))}
`
,
cover_image
:
`https://p
icsum.photos/seed/product-
${
id
}
/400/300
`
,
recommend
:
recommend
,
tags
:
tags
,
description
:
'这是一款优质的保险产品,为您的家庭提供全面保障...'
,
...
...
@@ -634,7 +634,7 @@ function generateFavoriteItem(id) {
meta_id
:
id
,
name
:
`
${
materialName
}
.
${
fileType
.
extension
}
`
,
size
:
generateRandomSize
(),
src
:
`https://p
lacehold.co/100x100/e2e8f0/475569?text=
${
fileType
.
extension
.
toUpperCase
()}
`
,
src
:
`https://p
icsum.photos/seed/favorite-
${
id
}
-
${
fileType
.
extension
}
/100/100
`
,
created_time
:
formatDate
(
createDate
)
}
}
...
...
@@ -723,7 +723,7 @@ function generateFeedbackItem(id) {
if
(
hasImages
)
{
const
imageCount
=
Math
.
floor
(
Math
.
random
()
*
3
)
+
1
for
(
let
i
=
0
;
i
<
imageCount
;
i
++
)
{
images
.
push
(
`https://p
lacehold.co/200x200/f3f4f6/9ca3af?text=截图
${
i
+
1
}
`
)
images
.
push
(
`https://p
icsum.photos/seed/feedback-
${
id
}
-
${
i
}
/200/200
`
)
}
}
...
...
Please
register
or
login
to post a comment