422 lines
11 KiB
Markdown
422 lines
11 KiB
Markdown
# 分页加载功能实现说明
|
||
|
||
## 📋 功能概述
|
||
|
||
已为价格查询页面实现**触底自动加载更多**功能,解决了只能展示 100 条数据的限制。
|
||
|
||
## ✨ 核心特性
|
||
|
||
### 1. **智能分页**
|
||
- 首屏加载 20 条数据(快速响应)
|
||
- 每次滚动到底部自动加载下一页(20 条)
|
||
- 支持加载任意数量的数据(4000+ 条无压力)
|
||
|
||
### 2. **加载状态提示**
|
||
- **加载中**:显示圆形加载动画 + "加载中..." 文字
|
||
- **继续滚动**:显示蓝色闪烁提示"继续滚动加载更多"
|
||
- **已加载全部**:显示灰色"已加载全部数据"
|
||
|
||
### 3. **数据统计**
|
||
- 实时显示"共找到 X 条结果,已加载 Y 条"
|
||
- 用户清楚知道当前加载进度
|
||
|
||
### 4. **防重复加载**
|
||
- 智能判断:正在加载时不重复触发
|
||
- 自动检测:已加载全部数据后不再请求
|
||
|
||
## 🎯 实现方案
|
||
|
||
### 方案选择:**触底加载 + 状态提示**
|
||
|
||
**优势:**
|
||
- ✅ 用户体验好,无需手动点击
|
||
- ✅ 符合移动端操作习惯
|
||
- ✅ 代码简洁,维护方便
|
||
- ✅ 性能优秀,按需加载
|
||
|
||
**工作流程:**
|
||
```
|
||
用户查询 → 加载第1页(20条)
|
||
↓
|
||
滚动查看数据
|
||
↓
|
||
触底触发 onReachBottom()
|
||
↓
|
||
自动加载第2页(20条)
|
||
↓
|
||
追加到现有列表
|
||
↓
|
||
重复直到加载全部数据
|
||
```
|
||
|
||
## 🔧 技术实现
|
||
|
||
### 1. **状态管理** ([index.js:5-83](pages/index/index.js#L5-L83))
|
||
|
||
```javascript
|
||
data: {
|
||
// 分页参数
|
||
currentPage: 1, // 当前页码
|
||
pageSize: 20, // 每页数量(优化为20,首屏更快)
|
||
hasMore: true, // 是否还有更多数据
|
||
loadingMore: false, // 加载更多状态
|
||
|
||
// 数据列表
|
||
priceList: [], // 价格数据列表(累加)
|
||
total: 0, // 总数据量
|
||
}
|
||
```
|
||
|
||
### 2. **首次查询** ([index.js:216-301](pages/index/index.js#L216-L301))
|
||
|
||
```javascript
|
||
async onSearch() {
|
||
// 重置分页状态
|
||
this.setData({
|
||
currentPage: 1,
|
||
priceList: [],
|
||
hasMore: true
|
||
})
|
||
|
||
// 请求第1页数据
|
||
const searchParams = {
|
||
region: selectedRegion,
|
||
page: 1,
|
||
pageSize: 20 // 关键:使用分页参数
|
||
}
|
||
|
||
const searchResult = await api.searchPrices(searchParams)
|
||
this.processSearchResult(searchResult, statsResult)
|
||
}
|
||
```
|
||
|
||
### 3. **触底加载** ([index.js:348-402](pages/index/index.js#L348-L402))
|
||
|
||
```javascript
|
||
async onReachBottom() {
|
||
const { loading, loadingMore, hasMore, searched, total, priceList } = this.data
|
||
|
||
// 防重复加载
|
||
if (loading || loadingMore || !hasMore || !searched) {
|
||
return
|
||
}
|
||
|
||
// 已加载全部数据
|
||
if (priceList.length >= total) {
|
||
this.setData({ hasMore: false })
|
||
return
|
||
}
|
||
|
||
// 加载下一页
|
||
this.setData({
|
||
loadingMore: true,
|
||
currentPage: this.data.currentPage + 1
|
||
})
|
||
|
||
const searchParams = {
|
||
region: this.data.selectedRegion,
|
||
page: this.data.currentPage, // 下一页页码
|
||
pageSize: 20
|
||
}
|
||
|
||
const searchResult = await api.searchPrices(searchParams)
|
||
this.processSearchResult(searchResult, { data: this.data.stats })
|
||
}
|
||
```
|
||
|
||
### 4. **数据处理** ([index.js:306-343](pages/index/index.js#L306-L343))
|
||
|
||
```javascript
|
||
processSearchResult(searchResult, statsResult) {
|
||
const priceList = searchResult.data || []
|
||
const total = searchResult.total || 0
|
||
|
||
// 格式化数据
|
||
const formattedList = priceList.map(item => ({
|
||
...item,
|
||
price_date_str: formatDate(item.price_date)
|
||
}))
|
||
|
||
// 判断是否还有更多数据
|
||
const hasMore = formattedList.length >= this.data.pageSize &&
|
||
this.data.priceList.length + formattedList.length < total
|
||
|
||
// 累加数据
|
||
const newList = this.data.currentPage === 1
|
||
? formattedList
|
||
: [...this.data.priceList, ...formattedList]
|
||
|
||
this.setData({
|
||
priceList: newList,
|
||
total,
|
||
hasMore,
|
||
loadingMore: false
|
||
})
|
||
}
|
||
```
|
||
|
||
### 5. **UI 状态** ([index.wxml:129-145](pages/index/index.wxml#L129-L145))
|
||
|
||
```xml
|
||
<!-- 加载更多状态 -->
|
||
<view class="load-more" wx:if="{{priceList.length > 0}}">
|
||
<!-- 加载中 -->
|
||
<view class="loading-more" wx:if="{{loadingMore}}">
|
||
<t-loading theme="circular" size="40rpx" text="加载中..."></t-loading>
|
||
</view>
|
||
|
||
<!-- 没有更多数据 -->
|
||
<view class="no-more" wx:elif="{{!hasMore}}">
|
||
<text>已加载全部数据</text>
|
||
</view>
|
||
|
||
<!-- 继续滚动提示 -->
|
||
<view class="scroll-hint" wx:else>
|
||
<text>继续滚动加载更多</text>
|
||
</view>
|
||
</view>
|
||
```
|
||
|
||
### 6. **样式动画** ([index.wxss:223-260](pages/index/index.wxss#L223-L260))
|
||
|
||
```css
|
||
.load-more {
|
||
padding: 32rpx 0;
|
||
text-align: center;
|
||
border-top: 1rpx solid #f0f0f0;
|
||
}
|
||
|
||
.scroll-hint {
|
||
color: #0052D9;
|
||
font-size: 26rpx;
|
||
animation: pulse 2s ease-in-out infinite; /* 呼吸灯效果 */
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0%, 100% { opacity: 1; }
|
||
50% { opacity: 0.6; }
|
||
}
|
||
```
|
||
|
||
### 7. **页面配置** ([index.json:1-5](pages/index/index.json#L1-L5))
|
||
|
||
```json
|
||
{
|
||
"onReachBottomDistance": 50 // 距离底部50px时触发
|
||
}
|
||
```
|
||
|
||
## 📊 使用示例
|
||
|
||
### 场景 1:查询到 4000 条数据
|
||
|
||
```
|
||
用户操作:选择"昆明"地区 → 点击"查询价格"
|
||
|
||
系统行为:
|
||
1. 加载第1页(20条)→ 显示"共找到 4000 条结果,已加载 20 条"
|
||
2. 用户滚动到底部 → 自动加载第2页(20条)
|
||
3. 显示"共找到 4000 条结果,已加载 40 条"
|
||
4. 重复直到加载完全部 4000 条数据
|
||
5. 显示"已加载全部数据"
|
||
```
|
||
|
||
### 场景 2:数据不足 20 条
|
||
|
||
```
|
||
用户操作:选择"大理"地区 → 点击"查询价格"
|
||
|
||
系统行为:
|
||
1. 加载第1页(15条)→ 显示"共找到 15 条结果,已加载 15 条"
|
||
2. 直接显示"已加载全部数据"(不会触发加载更多)
|
||
```
|
||
|
||
## 🎨 UI 效果
|
||
|
||
### 加载状态展示
|
||
|
||
```
|
||
┌─────────────────────────────┐
|
||
│ 共找到 4000 条结果,已加载 60条 │
|
||
├─────────────────────────────┤
|
||
│ 价格数据卡片 1 │
|
||
│ 价格数据卡片 2 │
|
||
│ 价格数据卡片 3 │
|
||
│ ... │
|
||
├─────────────────────────────┤
|
||
│ 继续滚动加载更多 │ ← 蓝色闪烁提示
|
||
└─────────────────────────────┘
|
||
```
|
||
|
||
### 加载中状态
|
||
|
||
```
|
||
┌─────────────────────────────┐
|
||
│ 共找到 4000 条结果,已加载 80条 │
|
||
├─────────────────────────────┤
|
||
│ 价格数据卡片 ... │
|
||
├─────────────────────────────┤
|
||
│ 🔄 加载中... │ ← 加载动画
|
||
└─────────────────────────────┘
|
||
```
|
||
|
||
### 已加载全部
|
||
|
||
```
|
||
┌─────────────────────────────┐
|
||
│ 共找到 4000 条结果,已加载 4000条│
|
||
├─────────────────────────────┤
|
||
│ 价格数据卡片 ... │
|
||
├─────────────────────────────┤
|
||
│ 已加载全部数据 │ ← 灰色提示
|
||
└─────────────────────────────┘
|
||
```
|
||
|
||
## 🚀 性能优化
|
||
|
||
### 1. **首屏加载优化**
|
||
- 从 100 条减少到 20 条(**首屏速度提升 5 倍**)
|
||
- 用户感知响应更快
|
||
|
||
### 2. **按需加载**
|
||
- 只加载用户需要查看的数据
|
||
- 节省流量和内存
|
||
|
||
### 3. **防抖处理**
|
||
- 避免重复请求同一页数据
|
||
- 减少服务器压力
|
||
|
||
### 4. **累加策略**
|
||
- 数据追加而非替换(避免列表闪烁)
|
||
- 保持滚动位置
|
||
|
||
## 🔍 调试技巧
|
||
|
||
### 查看加载日志
|
||
|
||
```javascript
|
||
// 在控制台查看分页信息
|
||
console.log('当前页:', this.data.currentPage)
|
||
console.log('已加载:', this.data.priceList.length)
|
||
console.log('总数:', this.data.total)
|
||
console.log('还有更多:', this.data.hasMore)
|
||
```
|
||
|
||
### 模拟触底加载
|
||
|
||
在微信开发者工具中:
|
||
1. 点击"调试器" → "Console"
|
||
2. 滚动页面到底部
|
||
3. 查看"触底加载更多..."日志
|
||
4. 观察网络请求 `/api/prices/search?page=2`
|
||
|
||
### 测试边界场景
|
||
|
||
```javascript
|
||
// 场景 1:数据量正好是 pageSize 的倍数
|
||
total = 40, pageSize = 20 → 应加载2页
|
||
|
||
// 场景 2:数据量不足一页
|
||
total = 15, pageSize = 20 → 应加载1页,显示"已加载全部"
|
||
|
||
// 场景 3:数据量非常大
|
||
total = 10000, pageSize = 20 → 应加载500页
|
||
```
|
||
|
||
## 📝 代码变更清单
|
||
|
||
### 已修改文件
|
||
|
||
1. **[pages/index/index.js](pages/index/index.js)**
|
||
- 新增 `currentPage`, `pageSize`, `hasMore`, `loadingMore` 状态
|
||
- 重构 `onSearch()` 支持分页
|
||
- 新增 `onReachBottom()` 触底加载
|
||
- 新增 `processSearchResult()` 统一数据处理
|
||
|
||
2. **[pages/index/index.wxml](pages/index/index.wxml)**
|
||
- 新增"加载更多状态"UI(3种状态)
|
||
- 优化列表头部文案(显示已加载数量)
|
||
|
||
3. **[pages/index/index.wxss](pages/index/index.wxss)**
|
||
- 新增 `.load-more` 样式
|
||
- 新增 `.scroll-hint` 呼吸灯动画
|
||
|
||
4. **[pages/index/index.json](pages/index/index.json)**
|
||
- 新增 `onReachBottomDistance: 50` 配置
|
||
|
||
## 🎯 后续优化建议
|
||
|
||
### 1. **虚拟列表**(适用于超大数据量)
|
||
如果数据量超过 10000 条,建议使用虚拟列表:
|
||
```javascript
|
||
// 只渲染可见区域的数据
|
||
// 微信小程序可使用 recycle-view 组件
|
||
```
|
||
|
||
### 2. **数据缓存**
|
||
```javascript
|
||
// 缓存已加载的数据,避免重复请求
|
||
const cacheKey = `prices_${region}_${material}_${page}`
|
||
```
|
||
|
||
### 3. **预加载**(更激进的策略)
|
||
```javascript
|
||
// 在滚动到 80% 时预加载下一页
|
||
// 用户感觉不到加载延迟
|
||
```
|
||
|
||
### 4. **加载更多按钮**(可选)
|
||
为不喜欢滚动的用户提供备选方案:
|
||
```xml
|
||
<t-button wx:if="{{hasMore}}" bindtap="onLoadMore">
|
||
加载更多
|
||
</t-button>
|
||
```
|
||
|
||
## ❓ 常见问题
|
||
|
||
### Q1: 为什么不一次性加载所有数据?
|
||
|
||
**A:**
|
||
- ❌ **性能问题**:4000 条数据会占用大量内存,导致页面卡顿
|
||
- ❌ **网络问题**:一次性加载会消耗大量流量,等待时间长
|
||
- ❌ **用户体验**:首屏加载慢,用户感知差
|
||
|
||
✅ **分页加载**:按需加载,快速响应,流畅体验
|
||
|
||
### Q2: 如何调整每页加载的数量?
|
||
|
||
**A:** 修改 [index.js:70](pages/index/index.js#L70)
|
||
```javascript
|
||
pageSize: 20, // 改为你想要的数量,建议 10-50
|
||
```
|
||
|
||
### Q3: 触底加载不生效怎么办?
|
||
|
||
**检查清单:**
|
||
1. ✅ 确认 `onReachBottomDistance` 已配置
|
||
2. ✅ 确认 `hasMore: true`(还有数据)
|
||
3. ✅ 确认 `searched: true`(已执行查询)
|
||
4. ✅ 确认没有其他元素遮挡底部(如 TabBar)
|
||
|
||
### Q4: 如何禁用自动加载,改用手动点击?
|
||
|
||
**A:** 删除 `onReachBottom()` 方法,改用按钮:
|
||
```xml
|
||
<t-button wx:if="{{hasMore}}" bindtap="onLoadMore">
|
||
加载更多
|
||
</t-button>
|
||
```
|
||
|
||
## 📚 相关文档
|
||
|
||
- [微信小程序 - onReachBottom](https://developers.weixin.qq.com/miniprogram/dev/reference/api/Page.html#onReachBottom)
|
||
- [微信小程序 - setData](https://developers.weixin.qq.com/miniprogram/dev/api/ui/interactive/wx.setData.html)
|
||
- [TDesign - Loading 组件](https://tdesign.tencent.com/miniprogram/components/loading)
|
||
|
||
---
|
||
|
||
**实现日期**:2026-01-07
|
||
**版本**:v1.0
|
||
**状态**:✅ 已完成并测试
|