HarmonyOS 购物商城:商品列表页的设计
排序、筛选、商品卡片——电商列表页的三板斧。
前言
首页是"逛"——用户漫无目的地看,被 Banner 和推荐吸引。列表页是"找"——用户带着明确目的来,需要快速筛选出想要的商品。
商品列表页的设计目标:让用户用最少的操作找到目标商品。排序、筛选、列表三个模块各司其职。
完整效果
页面结构
从截图可以看到,页面从上到下:
- 顶部导航栏:返回按钮 + 标题"全部分类" + 商品数量"16件"
- 排序栏:综合 | 价格↑ | 价格↓ | 销量 + 筛选按钮
- 商品列表:每个商品一行,左侧图片 + 中间信息 + 右侧加购按钮

路由入口
从首页进入
// 搜索
router.pushUrl({ url: 'pages/ProductList',
params: { 'search': this.searchText } as Record<string,Object> })
// 分类
router.pushUrl({ url: 'pages/ProductList',
params: { 'category': item.name } as Record<string,Object> })

列表页接收两种参数:
search:搜索关键词category:分类名称
从分类导航进入
// 底部导航的"分类"按钮
.onClick(() => { router.pushUrl({ url: 'pages/ProductList',
params: { 'category': '全部分类' } as Record<string,Object> }) })

"全部分类"是一个特殊值——显示所有商品。
排序栏
从截图可以看到四个排序选项:综合、价格↑、价格↓、销量。
// 排序栏的实现思路
Row() {
Text('综合').fontSize(13).fontColor(T1) // 当前选中
Text('价格↑').fontSize(13).fontColor(T2)
Text('价格↓').fontSize(13).fontColor(T2)
Text('销量').fontSize(13).fontColor(T2)
Blank()
Text('筛选').fontSize(13).fontColor(T2)
}

排序逻辑:
@State sortBy: string = 'default' // default | priceAsc | priceDesc | sales
private getSortedProducts(): Product[] {
let list = this.filteredProducts
if (this.sortBy === 'priceAsc') {
list = [...list].sort((a, b) => a.price - b.price)
} else if (this.sortBy === 'priceDesc') {
list = [...list].sort((a, b) => b.price - a.price)
} else if (this.sortBy === 'sales') {
list = [...list].sort((a, b) => b.sales - a.sales)
}
return list
}
为什么要 [...list] 复制数组?
list = [...list].sort(...)
直接 list.sort() 会修改原数组——ArkTS 的 @State 不会检测到原数组内部的变化,UI 不会更新。复制一个新数组再排序,@State 检测到引用变化,才会触发重新渲染。
这个问题和 3D 拆解分析里的 this.pc 刷新是同一个原理。
筛选功能
从截图右上角可以看到"筛选"按钮。点击后应该弹出筛选面板(价格区间、品牌、评分等)。
@State showFilter: boolean = false
// 筛选按钮
Text('筛选').fontSize(13).fontColor(T2)
.onClick(() => { this.showFilter = true })
// 筛选面板(条件渲染)
if (this.showFilter) {
// 筛选弹窗内容...
}
筛选是列表页最复杂的模块——需要支持多条件组合(价格区间 + 品牌 + 评分),还要支持重置和应用。
商品卡片
从截图可以看到每个商品的布局:
┌─────────────────────────────────────────┐
│ 📱 iPhone 15 Pro Max [+] │
│ 深空灰·黑色·全色全覆盖 │
│ ¥8999 ★ 15天 │
└─────────────────────────────────────────┘
三个区域:左侧图片、中间信息、右侧加购按钮。
加购按钮
从截图可以看到每个商品右侧有一个 [+] 按钮。这个按钮把商品加入购物车。
// 加购按钮的实现
Row() {
Text('+').fontSize(18).fontColor(Color.White)
}.width(28).height(28).borderRadius(14).backgroundColor(A)
.onClick(() => {
addToCart(p)
this.cartCount = getCartCount() // 更新徽章
})
加购后刷新首页徽章:
用户在列表页加购后,回到首页时购物车徽章需要更新。首页的 onPageShow 会重新调用 getCartCount(),所以只要数据源(CartService)更新了,首页徽章会自动刷新。
商品信息的密度
从截图看,每个商品卡片包含:
- 图标(emoji)
- 名称(加粗)
- 描述(灰色小字,比如"深空灰·黑色·全色全覆盖")
- 价格(红色加粗)
- 其他信息(评分、天数等)
信息密度很高——每个卡片要在有限空间内展示尽可能多的决策信息。名称告诉用户"这是什么",描述告诉用户"有什么特点",价格告诉用户"多少钱",评分告诉用户"别人觉得怎么样"。
跳转到商品详情
.onClick(() => { router.pushUrl({ url: 'pages/ProductDetail',
params: { 'id': p.id } as Record<string,Object> }) })
点击商品行跳转到详情页,传递商品 ID。详情页根据 ID 加载完整的商品信息。
踩坑记录
1. 筛选和排序的状态管理
列表页有多个状态需要管理:
sortBy:排序方式searchText:搜索关键词category:分类showFilter:是否显示筛选面板filteredProducts:筛选后的商品列表
如果把这些状态都放在 @State 里,代码会很乱。建议用一个 @State 对象统一管理:
@State filterState: FilterState = {
sortBy: 'default',
category: '',
priceRange: [0, Infinity],
brands: []
}
2. 空状态处理
当搜索/筛选没有结果时,需要显示空状态:
if (this.getSortedProducts().length === 0) {
Column() {
Text('🔍').fontSize(48).opacity(0.25)
Text('没有找到相关商品').fontSize(16).fontColor(T1)
Text('换个关键词试试').fontSize(12).fontColor(T3)
}
}
3. 列表性能
如果商品数量很多(比如 100+),ForEach 一次性渲染所有商品会影响性能。建议用懒加载:
// 只渲染可见的商品
private visibleProducts: Product[] = []
private pageSize: number = 20
4. 路由参数的类型安全
params: { 'category': item.name } as Record<string,Object>
每次路由跳转都要 as Record<string,Object> 断言,很啰嗦。建议封装一个路由工具函数:
function navigateTo(page: string, params: Record<string, string>): void {
router.pushUrl({ url: page, params: params as Record<string,Object> })
}
写在最后
商品列表页是电商 App 中使用频率最高的页面——用户每次搜索、每次点分类、每次点"更多"都会进来。它的设计直接影响用户的购物效率。
排序和筛选是列表页的核心能力。排序让用户按价格或销量快速定位,筛选让用户按品牌或价格区间缩小范围。两者配合,用户能在几秒内从 100 个商品中找到想要的那个。
加购按钮的即时反馈很重要——用户点一下就能看到购物车徽章更新。这种"操作→反馈"的闭环让用户知道"我的操作成功了"。
更多推荐

所有评论(0)