HarmonyOS7网购订单中心实战:订单状态展示与操作按钮映射
文章目录
前言
订单列表页看上去只是把几张卡片排出来,真正难的地方其实在于:同一份订单数据要同时服务筛选栏、状态标签、商品列表、金额汇总和底部操作按钮。只要这几个区域没有围绕同一个状态模型来组织,页面就会很快变成一堆条件分支的拼接。
这个 HarmonyOS 示例比较适合拿来讲“电商业务页怎么把展示逻辑和业务语义绑在一起”。它没有引入复杂网络层,也没有拆出一堆组件,但已经把订单页最核心的几件事做全了:订单状态切换、列表筛选、商品明细展示,以及不同状态下的动作映射。
这个页面真正要解决的,不是“把订单显示出来”
很多人第一次写订单页,会把注意力放在卡片布局上,比如店铺名放哪、价格放哪、按钮摆哪。布局当然重要,但这不是业务页最容易出问题的地方。订单页的核心其实是:一条订单记录进入页面后,应该被解释成什么样的用户可见信息。
以这个案例为例,一条订单至少要回答下面几件事:
- 当前处于什么状态。
- 应该出现在哪个筛选标签下。
- 底部应该给用户什么操作。
- 有没有额外的补充动作,比如查看物流。
- 商品数量、总价和创建时间怎么组合成一张完整卡片。
这也是为什么示例里虽然只有 orders 和 activeTab 两个核心状态,但页面并不单薄。它把复杂度放在了“解释数据”这件事上,而不是盲目扩状态。
先看状态模型:为什么这页只需要两个核心状态
这个页面最值得学的地方之一,就是状态数量很克制。
@State orders: OrderItem_ksld[] = [ ... ]
@State activeTab: string = 'all'

orders 是唯一主数据源,里面不仅有订单 id、店铺名和总价,还顺手把商品明细、状态、创建时间这些展示层必需的信息全部带上。这样做的好处是,列表渲染时不用再到别处查数据,也不需要临时拼接多个对象。
activeTab 则承担了当前筛选上下文。它的意义并不只是“高亮哪个 tab”,而是告诉整个页面:现在用户想看哪一类订单。只要这个值变化,列表内容和 tab 视觉状态都会一起变化。
这种设计非常像真实业务系统里常说的“单一主数据源 + 少量视图状态”。它的优点很直接:
- 容易排查问题,列表不对就先看
orders和activeTab。 - 扩展成本低,后面要加搜索、售后过滤或者按时间排序,都能在现有结构上叠加。
- 不容易出现多份状态彼此不同步的情况。
statusConfig 不是装饰,它是状态翻译层
订单状态通常不会直接拿原始字段给用户看。源码里用一张 statusConfig 表,把内部状态值翻译成可见文案和颜色:
private statusConfig: Record<string, StatusConfig_oskv> = {
'pending_pay': { label: '待付款', color: '#FF3B30' },
'pending_ship': { label: '待发货', color: '#FF9500' },
'shipped': { label: '运输中', color: '#007AFF' },
'delivered': { label: '待评价', color: '#34C759' },
'reviewed': { label: '已完成', color: '#888888' },
'refund': { label: '退款中', color: '#FF6B35' },
}
这类映射表在业务代码里很常见,但很多教程不会特意强调它的重要性。实际上,它解决的是“同一状态要在多个地方保持一致”的问题。
如果没有这层配置,你很可能会在不同卡片、不同组件里手写很多类似的判断:
if status === 'shipped' 就显示运输中if status === 'shipped' 就用蓝色if status === 'refund' 就用橙色

判断一多,后期一改状态名或者视觉规范,就会变成全文件找分支。把这些东西集中进配置表,本质上是在给展示层做解耦。后面哪怕接入后端枚举、国际化文本,改动面也会小很多。
getFilteredOrders() 负责列表可见范围,而不是改订单数据
订单页里最容易写混的一个点,是“筛选”到底是在改数据,还是在决定显示哪些数据。这个示例处理得很清楚:
getFilteredOrders(): OrderItem_ksld[] {
if (this.activeTab === 'all') return this.orders
return this.orders.filter((o: OrderItem_ksld) => o.status === this.activeTab)
}
这个函数没有去修改 orders,而是根据 activeTab 返回一个当前应该展示的结果集。这个思路非常重要,因为筛选本质上是“视图投影”,不是“重写原始数据”。
这样做有三个直接好处:
- 切换 tab 不会破坏原始订单数据。
- 同一个
orders可以被不同筛选条件重复使用。 - 后面叠加搜索、排序、售后筛选时,逻辑可以自然继续往这个函数里加,而不是到处改
orders。
如果你以后做的是更复杂的电商页,这个函数通常还会继续演化成组合筛选入口,比如状态 + 时间范围 + 关键词的联合过滤。示例虽然简单,但骨架是对的。
操作按钮映射,是这页最像真实业务的地方
订单页真正体现“业务味”的地方,不是商品卡片,而是底部操作区。因为不同状态下用户能做的事不一样,这意味着按钮文案、按钮数量、按钮样式都可能变化。
源码里先把主按钮文案抽成函数:
getActionButton(status: string): string {
const btnMap: Record<string, string> = {
'pending_pay': '立即付款',
'pending_ship': '取消订单',
'shipped': '确认收货',
'delivered': '去评价',
'reviewed': '再次购买',
'refund': '查看详情',
}
return btnMap[status] ?? '查看详情'
}

这个函数的价值不只是省代码,而是把“订单状态决定用户动作”明确建模出来。你看这几组映射就会发现,它们其实对应的是一条订单生命周期:
- 下单未支付时,引导完成付款。
- 已支付待发货时,常见动作变成取消订单。
- 已发货时,主动作转向确认收货。
- 已收货未评价时,再把用户导向评价闭环。
这比单纯说“根据状态显示不同按钮”更重要,因为它体现的是产品流程。教程如果只停留在 UI 层,很难帮读者建立这种业务意识。
另外,源码还额外处理了一个很真实的细节:只有 shipped 状态时,才会额外展示“查看物流”按钮。说明这页不是简单的一主按钮模型,而是开始区分“主动作”和“补充动作”。这在正式项目里经常会继续演化成操作集合配置。
商品明细区为什么要放在同一张订单卡里
在视觉上,这页最占空间的部分是商品列表。但从数据组织角度看,更关键的是:products 被嵌进单条订单里,而不是单独平铺。这样一来,订单和商品天然形成了父子关系。
好处很明显:
- 渲染时可以直接在订单卡内部
ForEach(order.products)。 - 汇总区可以直接基于当前订单的商品数量做
reduce。 - 一张卡片内部就能同时表达店铺、商品、金额和操作,不需要跨区域拼装。
这也解释了为什么订单卡片读起来很完整。它不是“先渲染订单,再到别处找商品”,而是围绕一条订单,把用户关心的信息一次性交代完。
从交互链路看,这页其实是一条很完整的闭环
把源码顺一遍,你会发现这页虽然没有写点击后的复杂事件,但交互闭环已经很完整了:
- 顶部 tab 先确定用户当前看的订单范围。
getFilteredOrders()根据 tab 产出列表内容。- 每张订单卡再根据状态展示文案颜色和按钮动作。
- 特定状态追加补充动作,比如查看物流。
- 用户从列表中继续进入付款、收货、评价或售后后续流程。
这类页面的价值就在这里。它不是做一个“静态设计稿还原”,而是在有限代码里把状态、列表、动作三者串起来了。
如果把它扩成正式项目,优先改哪些地方
这个案例已经适合教学和原型演示,但如果你要把它往真实项目推,我建议优先做下面几件事:
- 把状态配置和按钮映射抽成独立常量,避免页面文件继续变重。
- 给订单项补充更多业务字段,比如支付方式、物流单号、退款进度。
- 把订单卡、商品行、操作区拆成子组件,降低
build()的阅读负担。 - 给按钮点击补上真实行为,比如确认收货后的状态推进、取消订单后的列表刷新。
- 增加空列表、加载中、接口失败这几类正式项目必备状态。
尤其是最后一点,很多教程文章都容易忽略。但订单页一旦接远程接口,空态和错误态往往比正常态更需要提前设计。
完整代码
下面保留案例完整代码,方便你直接对照学习、复制和重构。
// 网购订单中心: 网购订单列表 - 订单状态流转、商品卡片、评价入口
interface OrderProduct {
name: string
spec: string
price: number
quantity: number
imageDesc: string
}
interface OrderItem_ksld {
id: string
shopName: string
status: string // 'pending_pay' | 'pending_ship' | 'shipped' | 'delivered' | 'reviewed' | 'refund'
products: OrderProduct[]
totalPrice: number
freight: number
createTime: string
estimatedTime: string
}
interface TabOption {
key: string
label: string
}
interface StatusConfig_oskv {
label: string
color: string
}
@Entry
@Component
struct EcommerceOrderCenter {
@State orders: OrderItem_ksld[] = [
{
id: 'ORD20260601001',
shopName: '苹果官方旗舰店',
status: 'shipped',
products: [
{ name: 'iPhone 16 Pro', spec: '黑色 256G', price: 8999, quantity: 1, imageDesc: '📱' },
{ name: 'MagSafe 保护壳', spec: '透明', price: 199, quantity: 1, imageDesc: '🔲' },
],
totalPrice: 9198,
freight: 0,
createTime: '2026-06-01 14:30',
estimatedTime: '2026-06-07 预计送达'
},
{
id: 'ORD20260602002',
shopName: '耐克官方旗舰店',
status: 'delivered',
products: [
{ name: 'Air Max 2026', spec: '白色 42码', price: 1299, quantity: 1, imageDesc: '👟' },
],
totalPrice: 1299,
freight: 0,
createTime: '2026-05-28 10:15',
estimatedTime: '已送达'
},
{
id: 'ORD20260603003',
shopName: '京东自营',
status: 'pending_pay',
products: [
{ name: 'MacBook Pro 14', spec: 'M4 512G', price: 14999, quantity: 1, imageDesc: '💻' },
],
totalPrice: 14999,
freight: 0,
createTime: '2026-06-06 09:00',
estimatedTime: '待付款'
},
{
id: 'ORD20260604004',
shopName: '小米官方',
status: 'reviewed',
products: [
{ name: '小米手环 9', spec: '黑色', price: 299, quantity: 1, imageDesc: '⌚' },
{ name: '充电器 67W', spec: '白色', price: 99, quantity: 2, imageDesc: '🔌' },
],
totalPrice: 497,
freight: 0,
createTime: '2026-05-20 16:45',
estimatedTime: '已评价'
},
{
id: 'ORD20260605005',
shopName: '网易严选',
status: 'refund',
products: [
{ name: '纯棉T恤', spec: '白色 L码', price: 129, quantity: 2, imageDesc: '👕' },
],
totalPrice: 258,
freight: 0,
createTime: '2026-05-15 11:20',
estimatedTime: '退款中'
},
]
@State activeTab: string = 'all'
private tabs: TabOption[] = [
{ key: 'all', label: '全部' },
{ key: 'pending_pay', label: '待付款' },
{ key: 'pending_ship', label: '待发货' },
{ key: 'shipped', label: '待收货' },
{ key: 'delivered', label: '待评价' },
{ key: 'refund', label: '退款' },
]
private statusConfig: Record<string, StatusConfig_oskv> = {
'pending_pay': { label: '待付款', color: '#FF3B30' },
'pending_ship': { label: '待发货', color: '#FF9500' },
'shipped': { label: '运输中', color: '#007AFF' },
'delivered': { label: '待评价', color: '#34C759' },
'reviewed': { label: '已完成', color: '#888888' },
'refund': { label: '退款中', color: '#FF6B35' },
}
getFilteredOrders(): OrderItem_ksld[] {
if (this.activeTab === 'all') return this.orders
return this.orders.filter((o: OrderItem_ksld) => o.status === this.activeTab)
}
getActionButton(status: string): string {
const btnMap: Record<string, string> = {
'pending_pay': '立即付款',
'pending_ship': '取消订单',
'shipped': '确认收货',
'delivered': '去评价',
'reviewed': '再次购买',
'refund': '查看详情',
}
return btnMap[status] ?? '查看详情'
}
build() {
Column() {
// 顶部
Row() {
Text('我的订单').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#1A1A1A')
Blank()
Text('🔍').fontSize(20)
}
.padding({ left: 20, right: 20, top: 20, bottom: 0 })
// 状态筛选Tab
Scroll() {
Row({ space: 0 }) {
ForEach(this.tabs, (tab: TabOption) => {
Column() {
Text(tab.label)
.fontSize(13)
.fontColor(this.activeTab === tab.key ? '#FF6B35' : '#666666')
.fontWeight(this.activeTab === tab.key ? FontWeight.Bold : FontWeight.Normal)
.padding({ top: 14, bottom: 14, left: 12, right: 12 })
Divider()
.strokeWidth(2)
.color(this.activeTab === tab.key ? '#FF6B35' : 'transparent')
}
.onClick(() => { this.activeTab = tab.key })
})
}
.padding({ left: 8, right: 8 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.backgroundColor('#FFFFFF')
.margin({ bottom: 12 })
// 订单列表
List({ space: 12 }) {
ForEach(this.getFilteredOrders(), (order: OrderItem_ksld) => {
ListItem() {
Column() {
// 店铺行
Row() {
Text('🏪').fontSize(14)
Text(order.shopName)
.fontSize(14).fontWeight(FontWeight.Medium).fontColor('#1A1A1A').margin({ left: 6 })
Blank()
Text(this.statusConfig[order.status]?.label ?? order.status)
.fontSize(13)
.fontColor(this.statusConfig[order.status]?.color ?? '#888888')
.fontWeight(FontWeight.Medium)
}
.width('100%')
.padding({ bottom: 10 })
.border({ width: { bottom: 0.5 }, color: { bottom: '#F5F5F5' } })
// 商品列表
ForEach(order.products, (p: OrderProduct) => {
Row() {
Text(p.imageDesc)
.fontSize(36)
.width(70).height(70)
.textAlign(TextAlign.Center)
.backgroundColor('#F8F8F8')
.borderRadius(8)
Column() {
Text(p.name)
.fontSize(13).fontColor('#333333').maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(p.spec)
.fontSize(11).fontColor('#999999')
.backgroundColor('#F5F5F5').borderRadius(4)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.margin({ top: 4 })
Row() {
Text(`¥${p.price.toFixed(2)}`)
.fontSize(14).fontWeight(FontWeight.Bold).fontColor('#FF3B30')
Blank()
Text(`x${p.quantity}`).fontSize(12).fontColor('#999999')
}
.width('100%')
.margin({ top: 6 })
}
.layoutWeight(1)
.margin({ left: 12 })
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.padding({ top: 10, bottom: 10 })
.border({ width: { bottom: 0.5 }, color: { bottom: '#F5F5F5' } })
})
// 合计
Row() {
Text(`共 ${order.products.reduce((s: number, p: OrderProduct) => s + p.quantity, 0)} 件`)
.fontSize(12).fontColor('#888888')
Blank()
Text('实付:')
.fontSize(12).fontColor('#888888')
Text(`¥${order.totalPrice.toFixed(2)}`)
.fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FF3B30')
}
.width('100%')
.padding({ top: 10 })
// 操作按钮
Row() {
Text(order.createTime).fontSize(11).fontColor('#BBBBBB')
Blank()
if (order.status === 'shipped') {
Button('查看物流')
.fontSize(12).fontColor('#555555')
.backgroundColor('#F5F5F5').borderRadius(16).height(32)
.padding({ left: 12, right: 12 })
.margin({ right: 8 })
}
Button(this.getActionButton(order.status))
.fontSize(12).fontColor('#FF6B35')
.backgroundColor('transparent')
.border({ width: 1, color: '#FF6B35' })
.borderRadius(16).height(32)
.padding({ left: 12, right: 12 })
}
.width('100%')
.margin({ top: 10 })
}
.width('100%')
.padding(14)
.backgroundColor('#FFFFFF')
.borderRadius(14)
}
})
}
.layoutWeight(1)
.padding({ left: 16, right: 16 })
.scrollBar(BarState.Off)
}
.width('100%').height('100%')
.backgroundColor('#F8F8F8')
}
}
最后总结
这篇订单页案例最值得带走的,不是某个按钮怎么画,而是它把订单状态当成页面的中心语义来处理。只要你把“状态翻译”“列表筛选”“动作映射”这三层关系吃透,后面再写售后页、物流页、交易记录页,思路都会顺很多。
更多推荐

所有评论(0)