微信小程序案例 - 本地生活(列表页面)

一、前言:列表页是本地生活类小程序的核心

如果说首页是"门面",那么列表页就是转化的关键入口

无论是美食、酒店还是团购服务,用户最终都要通过列表页找到心仪商品。

一个专业的列表页应具备:

  • ✅ 分类筛选(如"按销量"、"距离最近")
  • ✅ 下拉刷新 & 上拉加载更多
  • ✅ 空状态处理
  • ✅ 高性能滚动(避免卡顿)

本文将带你从零实现一个高仿美团/大众点评的商品列表页,涵盖数据请求、分页加载、交互优化等核心技能!

💡 本案例承接《微信小程序案例 - 本地生活(首页)》,建议先阅读首页开发。


二、最终效果预览

功能模块:

  1. 顶部筛选栏(综合、销量、价格)
  2. 商品卡片列表(带图片、标题、评分、价格)
  3. 下拉刷新最新数据
  4. 上拉自动加载下一页
  5. 无数据时显示空状态

三、项目结构准备

确保已有以下目录结构:

复制代码
local-life/
├── pages/
│   ├── index/          # 首页(已实现)
│   └── list/           # 新增列表页
│       ├── list.js
│       ├── list.json
│       ├── list.wxml
│       └── list.wxss
├── utils/
│   └── request.js      # 封装的网络请求工具
└── app.json            # 需注册新页面

app.json 中注册页面

javascript 复制代码
{
  "pages": [
    "pages/index/index",
    "pages/list/list"   // ← 新增
  ],
  "window": {
    "backgroundTextStyle": "light",
    "navigationBarBackgroundColor": "#fff",
    "navigationBarTitleText": "本地生活",
    "navigationBarTextStyle": "black"
  }
}

四、页面配置(list.json)

javascript 复制代码
{
  "navigationBarTitleText": "美食推荐",
  "enablePullDownRefresh": true,
  "onReachBottomDistance": 50
}

enablePullDownRefresh:开启下拉刷新

onReachBottomDistance:距底部 50px 触发上拉加载


五、核心逻辑:list.js(数据 + 分页)

javascript 复制代码
// pages/list/list.js
import { get } from '../../utils/request';

Page({
  data: {
    list: [],           // 商品列表
    page: 1,            // 当前页码
    pageSize: 10,       // 每页数量
    hasMore: true,      // 是否还有更多数据
    loading: false,     // 上拉加载中
    sortType: 'default', // 排序类型:default | sales | price
    categoryId: '',     // 分类ID(可扩展)
    isEmpty: false      // 是否空列表
  },

  // 页面加载(可接收分类参数)
  onLoad(options) {
    if (options.categoryId) {
      this.setData({ categoryId: options.categoryId });
    }
    this.loadData(true); // 首次加载
  },

  // 下拉刷新
  onPullDownRefresh() {
    this.setData({ page: 1, list: [] });
    this.loadData(true);
  },

  // 上拉加载更多
  onReachBottom() {
    if (!this.data.loading && this.data.hasMore) {
      this.setData({ loading: true });
      this.loadData(false);
    }
  },

  // 加载数据(refresh: 是否刷新)
  async loadData(refresh) {
    const { page, pageSize, sortType, categoryId } = this.data;
    
    try {
      // 模拟 API 请求(实际替换为你的接口)
      const res = await get('/api/products', {
        page,
        pageSize,
        sort: sortType,
        category: categoryId
      });

      const newList = refresh ? res.data : [...this.data.list, ...res.data];
      const hasMore = res.data.length === pageSize;

      this.setData({
        list: newList,
        page: refresh ? 1 : page + 1,
        hasMore,
        loading: false,
        isEmpty: newList.length === 0
      });
    } catch (err) {
      wx.showToast({ title: '加载失败', icon: 'none' });
      this.setData({ loading: false });
    } finally {
      // 停止下拉刷新动画
      if (refresh) wx.stopPullDownRefresh();
    }
  },

  // 切换排序
  handleSortChange(e) {
    const type = e.currentTarget.dataset.type;
    if (type === this.data.sortType) return; // 避免重复点击

    this.setData({ sortType: type, page: 1, list: [] });
    this.loadData(true);
  },

  // 跳转详情页
  goToDetail(e) {
    const id = e.currentTarget.dataset.id;
    wx.navigateTo({ url: `/pages/detail/detail?id=${id}` });
  }
});

💡 提示:get 方法来自之前封装的 request.js(支持 token、错误处理等)。


六、页面结构:list.wxml

javascript 复制代码
<!-- pages/list/list.wxml -->
<view class="container">
  <!-- 筛选栏 -->
  <view class="filter-bar">
    <view 
      class="filter-item {{sortType === 'default' ? 'active' : ''}}"
      data-type="default"
      bindtap="handleSortChange">
      综合
    </view>
    <view 
      class="filter-item {{sortType === 'sales' ? 'active' : ''}}"
      data-type="sales"
      bindtap="handleSortChange">
      销量
    </view>
    <view 
      class="filter-item {{sortType === 'price' ? 'active' : ''}}"
      data-type="price"
      bindtap="handleSortChange">
      价格
    </view>
  </view>

  <!-- 列表内容 -->
  <block wx:if="{{!isEmpty}}">
    <view 
      class="product-item" 
      wx:for="{{list}}" 
      wx:key="id"
      bindtap="goToDetail"
      data-id="{{item.id}}">
      <image src="{{item.image}}" class="product-img" mode="aspectFill" />
      <view class="product-info">
        <view class="title">{{item.title}}</view>
        <view class="meta">
          <text class="rating">⭐ {{item.rating}}</text>
          <text class="sales">{{item.sales}}人购买</text>
        </view>
        <view class="price-row">
          <text class="price">¥{{item.price}}</text>
          <text wx:if="{{item.originalPrice}}" class="origin-price">¥{{item.originalPrice}}</text>
        </view>
      </view>
    </view>

    <!-- 上拉加载提示 -->
    <view wx:if="{{loading}}" class="load-more">正在加载...</view>
    <view wx:elif="{{!hasMore}}" class="load-more">没有更多了</view>
  </block>

  <!-- 空状态 -->
  <view wx:else class="empty-state">
    <image src="/images/empty.png" class="empty-img" />
    <text class="empty-text">暂无相关商品</text>
  </view>
</view>

七、样式美化:list.wxss

javascript 复制代码
/* pages/list/list.wxss */
.container {
  padding: 0 20rpx;
  background-color: #f8f8f8;
}

/* 筛选栏 */
.filter-bar {
  display: flex;
  background: white;
  padding: 20rpx 0;
  border-radius: 16rpx;
  margin-bottom: 20rpx;
  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
}
.filter-item {
  flex: 1;
  text-align: center;
  font-size: 28rpx;
  color: #666;
}
.filter-item.active {
  color: #e74c3c;
  font-weight: bold;
}

/* 商品项 */
.product-item {
  display: flex;
  background: white;
  border-radius: 16rpx;
  padding: 20rpx;
  margin-bottom: 20rpx;
  box-shadow: 0 2rpx 6rpx rgba(0,0,0,0.08);
}
.product-img {
  width: 200rpx;
  height: 200rpx;
  border-radius: 12rpx;
  margin-right: 20rpx;
}
.product-info {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}
.title {
  font-size: 28rpx;
  font-weight: bold;
  color: #333;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.meta {
  display: flex;
  justify-content: space-between;
  color: #999;
  font-size: 24rpx;
  margin: 10rpx 0;
}
.rating {
  color: #ff9900;
}
.sales {
  color: #666;
}
.price-row {
  display: flex;
  align-items: center;
}
.price {
  font-size: 32rpx;
  color: #e74c3c;
  font-weight: bold;
}
.origin-price {
  font-size: 24rpx;
  color: #999;
  text-decoration: line-through;
  margin-left: 10rpx;
}

/* 加载提示 */
.load-more {
  text-align: center;
  padding: 30rpx 0;
  color: #999;
  font-size: 26rpx;
}

/* 空状态 */
.empty-state {
  text-align: center;
  padding: 200rpx 0;
}
.empty-img {
  width: 200rpx;
  height: 200rpx;
  opacity: 0.6;
}
.empty-text {
  font-size: 28rpx;
  color: #999;
  margin-top: 20rpx;
}

八、关键知识点总结

功能 实现方式
下拉刷新 onPullDownRefresh + wx.stopPullDownRefresh()
上拉加载 onReachBottom + 分页参数控制
排序切换 data-type 传参 + 重置分页
空状态 wx:if 判断列表长度
性能优化 使用 wx:key、避免深层嵌套

九、扩展建议(进阶方向)

  1. 添加搜索框:支持关键词过滤
  2. 地图模式切换:列表 ↔ 地图双视图
  3. 骨架屏加载:提升等待体验
  4. 缓存分页数据:避免重复请求
  5. 接入真实后端:替换 mock 数据

十、结语

感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!

相关推荐
咖啡の猫3 小时前
微信小程序案例 - 本地生活(首页)
微信小程序·生活·notepad++
咸虾米_4 小时前
uniapp引入iconfont字体图标在微信小程序中适用
微信小程序·小程序·uni-app
咖啡の猫4 小时前
微信小程序页面导航
微信小程序·小程序
小咕聊编程4 小时前
【含文档+PPT+源码】基于微信小程序的点餐系统的设计与实现
微信小程序·小程序
StarChainTech1 天前
一站式租车平台革新:从信用免押到全流程可视化管理的技术实践
大数据·人工智能·微信小程序·小程序·软件需求
换日线°1 天前
微信小程序对接位置服务(腾讯、高德)完成路径规划
前端·微信小程序·vue
程序员白彬1 天前
再见,2025:职业有波澜,生活向前看
面试·生活·裁员
晨港飞燕1 天前
Notepad++使用技巧
notepad++
苏苏哇哈哈1 天前
微信小程序实现仿腾讯视频小程序首页圆角扩散轮播组件
微信小程序·小程序·轮播图