跳转到内容

编程实验:开发订单创建与列表页

本实验将基于 09_order_create_list 模板工程,在已经完成商品列表页和购物车功能的基础上,继续开发点餐 APP 的订单创建与订单列表页。最终效果是:用户在商品列表页通过购物车发起结算,创建订单成功后回到上一页;进入订单 Tab 后,可以按订单状态查看所有订单、待支付订单、待上菜订单和已完成订单。

本实验主要完成以下任务:

  • 在购物车底部栏中实现订单创建确认、Loading 等待反馈和创建订单接口调用
  • 创建订单成功后清空购物车,并通知订单页有新订单产生
  • ServerApi 中封装创建订单和获取订单列表接口
  • 搭建订单管理页,完成 Loading、异常、内容状态流转
  • 使用 Tabs 实现订单状态分类导航
  • 使用多个 BasicDataSource 管理不同状态的订单列表
  • 开发订单列表组件和订单小项组件
  • 使用 emitter 监听新订单创建事件,让订单列表及时显示新订单

最终效果

购物车结算入口
订单列表页最终效果

本实验涉及的知识点

知识点使用场景
AlertDialog点击“去结算”后弹出创建订单确认框
CustomDialogController创建订单时显示 Loading 弹窗
@Consume在购物车底部栏中读取商品列表页提供的商铺信息
Tabs / TabContent实现订单状态分类导航
BasicDataSource管理全部、待支付、待上菜、已完成四类订单数据
LazyForEach渲染订单列表和订单内商品列表
emitter商品列表页创建订单后通知订单管理页更新数据
class-transformer将接口返回的订单 JSON 转换为 OrderData 对象

开发思路

本实验按照“先打通业务链路,再完善列表页面,再处理跨页面刷新”的顺序推进:

  1. 先在购物车底部栏完成“创建订单”流程,让用户可以从购物车提交订单。
  2. 再搭建订单管理页,完成订单接口请求和页面状态流转。
  3. 接着实现订单分类 Tab 和分类数据源,把订单按状态分开管理。
  4. 然后开发订单列表和订单小项 UI,展示订单内商品、价格、状态、订单号和创建时间。
  5. 最后使用 emitter 处理新订单通知,解决创建订单后订单列表不及时更新的问题。

目标:导入并运行模板工程,确认当前工程状态,明确本实验需要修改的文件。

2.1 获取模板工程

模板工程仓库:https://cnb.cool/sziit-coding/harmony-coding/09_order_create_list

请使用 DevEco Studio 打开该模板工程,并确认工程可以正常运行。

2.2 认识模板工程当前状态

模板工程已经完成登录、首页 Tab、商铺列表页、商品列表页和购物车功能。用户登录后进入首页,点击商铺可以进入商品列表页,在商品列表中添加商品后,底部购物车栏会显示商品总数和总价。

订单相关功能目前只提供了基础骨架:

  • OrderManageView.ets 只显示“订单列表”占位文字
  • OrderListComponent.ets 只提供了空的 Stack
  • OrderItemComponent.ets 已提供订单状态解析方法,但 UI 为空
  • ServerApi.etscreateOrder()getUserOrderList() 仍是待实现方法
  • 购物车底部栏的“去结算”按钮当前只显示“待实现”提示
entry/src/main/ets/order/OrderManageView.ets
@Component
export struct OrderManageView {
build() {
Column() {
Text("订单列表").fontSize(40).fontWeight(FontWeight.Bold).height('100%')
}
.size({ width: '100%', height: '100%' })
}
}
entry/src/main/ets/customer/shoppingcart/ShoppingCartBarComponent.ets
Text($r('app.string.label_create_order'))
...
.onClick(_ => {
promptAction.showToast({ message: "待实现" })
})

2.3 模板工程已提供的订单基础

模板工程中已经提供了部分订单开发所需的基础代码和资源:

文件或资源作用
entry/src/main/ets/model/OrderData.ets订单数据模型
entry/src/main/ets/model/GoodsData.ets商品数据模型,已包含订单商品数量和小计字段
entry/src/main/ets/order/OrderManageView.ets订单管理页主组件
entry/src/main/ets/order/list/OrderListComponent.ets订单列表组件
entry/src/main/ets/order/list/OrderItemComponent.ets订单小项组件
entry/src/main/ets/utils/Constants.ets已定义订单创建事件
entry/src/main/resources/base/media/ic_next_black.svg订单小项商铺标题右箭头图标

OrderData.ets 中已经定义了订单字段与接口字段之间的映射:

entry/src/main/ets/model/OrderData.ets
export class OrderData {
@Expose({name: 'recordId'})
id: number = 0
@Expose({name: 'sn'})
sn: string = ''
@Expose({name: 'storeName'})
storeName: string = ''
@Expose({name: 'storeImage'})
storeImageUrl: string = ''
@Expose({name: 'priceAll'})
priceAll: number = 0
@Expose({name: 'payStatus'})
payStatus: number = 0
@Expose({name: 'cuiSine'})
deliverStatus: number = 0
@Expose({name: 'recordGoodsList'})
@Type(() => GoodsData)
goodsList: GoodsData[] = []
getCreateTime(): string {
return Utils.formatTimestamp(this.createdTime)
}
}

Constants.ets 中已经定义了订单创建事件:

entry/src/main/ets/utils/Constants.ets
export class Constants {
...
// 订单创建消息
static readonly EVENT_ORDER_CREATED: emitter.InnerEvent = {
eventId: 4,
priority: emitter.EventPriority.LOW
}
}

2.4 本实验涉及的文件

文件路径操作作用
entry/src/main/ets/api/ServerApi.ets修改实现创建订单和获取订单列表接口
entry/src/main/ets/customer/shoppingcart/ShoppingCartBarComponent.ets修改在购物车底部栏中接入订单创建流程
entry/src/main/ets/order/OrderManageView.ets修改订单管理页,负责状态流转、分类导航和订单数据源管理
entry/src/main/ets/order/list/OrderListComponent.ets修改订单列表组件,负责空数据和订单列表展示
entry/src/main/ets/order/list/OrderItemComponent.ets修改订单小项组件,负责展示单个订单的完整信息

✅ 预期效果:运行模板工程并完成登录后,进入首页“订单”Tab,可以看到当前订单页仍是占位页面;进入商品列表页加购商品后,点击“去结算”只会显示“待实现”提示。

订单页初始占位效果
购物车底栏初始状态

目标:理解订单创建、订单列表和跨页面通知之间的关系,避免把所有逻辑都堆在一个组件中。

3.1 订单创建为什么放在购物车底部栏

订单创建的入口是购物车底部栏中的“去结算”按钮。这个组件天然能拿到当前购物车的总数、总价和购物车业务对象,并且处在商品列表页的上下文中。

创建订单时还需要当前商铺 ID。商品列表页已经通过 @Provide('storeData') 提供了商铺信息:

entry/src/main/ets/customer/goods/GoodsListView.ets
@Component
export struct GoodsListView {
// 当前查看的商铺
@Provide('storeData') storeData: StoreData = new StoreData()
...
}

因此,购物车底部栏可以通过 @Consume('storeData') 读取当前商铺信息,并调用创建订单接口。

3.2 订单页为什么拆成三个组件

订单页由三个组件协作完成:

组件主要职责
OrderManageView管理页面状态、拉取订单数据、维护分类数据源、显示分类 Tab
OrderListComponent接收某一类订单数据源,处理空数据和列表展示
OrderItemComponent展示单个订单的商铺信息、商品列表、价格、状态、订单号和创建时间

这种拆分可以让每个组件只负责一类问题。订单接口请求和状态切换留在页面级组件中,列表渲染放在列表组件中,单个订单的 UI 细节放在订单小项组件中。

3.3 为什么要为不同订单状态准备多个数据源

订单列表页有四个分类:所有、待支付、待上菜、已完成。每个分类都需要独立展示数据:

分类判断条件
所有所有订单
待支付payStatus == 0
待上菜payStatus == 1 && deliverStatus == 0
已完成payStatus == 1 && deliverStatus == 1

如果每次切换 Tab 时才临时过滤数组,后续插入新订单、刷新列表、局部更新会比较分散。本实验会在订单数据拉取成功后,把订单按状态分发到四个 BasicDataSource 中,让四个 Tab 直接读取各自的数据源。

3.4 为什么需要订单创建事件通知

创建订单发生在商品列表页,订单列表显示在首页的“订单”Tab 中。用户在商品列表页创建订单后返回上一页,订单管理页不一定会重新触发完整的数据拉取。

为了解决这个问题,本实验会在创建订单成功后使用 emitter 发送订单创建事件:

订单创建事件通知示意
emitter.emit(Constants.EVENT_ORDER_CREATED, {
data: {
'orderJson': orderJson
}
})

订单管理页订阅这个事件,收到新订单后立即插入对应的数据源。这样用户创建订单后切换到订单页,可以马上看到新订单。

目标:让购物车底部栏的“去结算”按钮真正发起订单创建,并给用户明确的确认和等待反馈。

4.1 对接创建订单接口

ServerApi.ets 中,找到 createOrder(storeId) 方法。该方法用于在指定商铺内创建订单,请补充请求地址、请求方法、请求参数和响应解析逻辑。

接口文档地址:https://docs.apipost.net/docs/detail/2c39ebb29c64000?target_id=3602ab62b8800f

本实验使用的创建订单接口为:

创建订单接口
POST https://api.food2.sziit.top/order-record/create_v2
Content-Type: application/x-www-form-urlencoded

请求时需要通过表单参数传递当前商铺 ID:

参数名类型是否必填说明
storeIdnumber当前创建订单的商铺 ID

接口成功时会返回创建后的订单数据,外层仍然使用项目统一的 Response<T> 响应结构:

创建订单接口响应结构
{
"code": 200,
"msg": "成功",
"data": {
"recordId": 1,
"sn": "订单编号",
"storeName": "商铺名称",
"storeImage": "商铺图片地址",
"priceAll": 36,
"payStatus": 0,
"cuiSine": 0,
"recordGoodsList": []
}
}

模板工程中的 OrderData.ets 已经通过 @Expose 完成字段映射,例如 recordId 会映射到 idstoreImage 会映射到 storeImageUrlcuiSine 会映射到 deliverStatusrecordGoodsList 会映射到 goodsList。因此本步骤不需要重新定义订单模型,只需要在请求成功后使用 plainToClassFromExist(new Response<OrderData>(OrderData), JSON.parse(rsp.result as string)) 将接口返回值转换成 Response<OrderData>

entry/src/main/ets/api/ServerApi.ets
export class ServerApi {
...
/**
* 在指定的商铺内创建订单
* @param storeId 商铺id
* @returns
*/
static async createOrder(storeId: number): Promise<Response<OrderData>> {
// TODO: 创建 POST 请求,请求地址为 `${Constants.SERVER_HOST}/order-record/create_v2`
// TODO: 设置 Content-Type 为 application/x-www-form-urlencoded,并通过 extraData 传递 storeId
// TODO: 调用 ServerApi.execute() 发送请求,并使用 plainToClassFromExist() 解析为 Response<OrderData>
}
}

✅ 预期效果:ServerApi.createOrder(storeId) 方法具备调用服务端创建订单接口的能力。

4.2 在购物车底部栏引入必要依赖

订单创建时需要使用 Loading 弹窗、商铺信息、订单接口、路由和事件通知。先在 ShoppingCartBarComponent.ets 中补充相关导入。

entry/src/main/ets/customer/shoppingcart/ShoppingCartBarComponent.ets
import promptAction from '@ohos.promptAction'
import router from '@ohos.router'
import emitter from '@ohos.events.emitter'
import { instanceToPlain } from 'class-transformer'
import { LoadingDialog } from '../../common/dialog/LoadingDialog'
import { ServerApi } from '../../api/ServerApi'
import { StoreData } from '../../model/StoreData'
import { OrderData } from '../../model/OrderData'

✅ 预期效果:购物车底部栏已经具备订单创建、Loading 弹窗、页面返回和事件通知所需依赖。

4.3 引入 Loading 弹窗和商铺信息

ShoppingCartBarComponent 组件中新增 Loading 弹窗控制器,并通过 @Consume('storeData') 获取当前商铺。

entry/src/main/ets/customer/shoppingcart/ShoppingCartBarComponent.ets
@Component
export struct ShoppingCartBarComponent {
...
// Loading 弹窗控制器
private loadingDialogController = new CustomDialogController({
// 注意:此处的 LoadingDialog 组件的导入路径是 ../../common/dialog/LoadingDialog'
builder: LoadingDialog({ message: $r('app.string.message_creating_order') }),
autoCancel: false,
customStyle: true,
})
// 当前商铺信息
@Consume('storeData')
storeData: StoreData
}

✅ 预期效果:购物车底部栏可以读取当前商铺 ID,并在创建订单时显示 Loading 弹窗。

4.4 实现创建订单核心流程

ShoppingCartBarComponent 中新增 createOrder() 方法。该方法负责打开 Loading、调用创建订单接口、处理失败提示、清空购物车、关闭 Loading,并暂时返回上一页。

entry/src/main/ets/customer/shoppingcart/ShoppingCartBarComponent.ets
@Component
export struct ShoppingCartBarComponent {
...
/**
* 创建订单
*/
async createOrder() {
// TODO: 打开 Loading 弹窗
// TODO: 调用 ServerApi.createOrder(this.storeData.id)
// TODO: 如果接口失败,提示 message_create_order_failed,关闭 Loading 后结束流程
// TODO: 如果接口成功,调用 this.shoppingCart.cleanShoppingCart() 清空购物车
// TODO: 关闭 Loading,返回上一级页面,并提示“订单创建成功”
}
}

✅ 预期效果:创建订单的业务流程已经封装成独立方法。

4.5 增加创建订单确认弹窗

直接点击“去结算”就创建订单,用户容易误触。请在 ShoppingCartBarComponent 中新增 confirmCreateOrder() 方法,点击确认后再执行 createOrder()

entry/src/main/ets/customer/shoppingcart/ShoppingCartBarComponent.ets
@Component
export struct ShoppingCartBarComponent {
...
/**
* 订单创建确认弹窗
*/
confirmCreateOrder() {
// TODO: 使用 AlertDialog.show() 弹出确认框
// TODO: title 使用 title_create_order_confirm
// TODO: message 使用 message_create_order_confirm
// TODO: 确认按钮(primaryButton)文字(value)使用 label_confirm,点击(action)后调用 this.createOrder()
// TODO: 取消按钮(secondaryButton)文字使用 label_cancel
}
}

✅ 预期效果:订单创建前会先弹出确认框,用户确认后才进入创建流程。

4.6 给“去结算”按钮接入点击事件

找到“去结算”按钮当前的点击事件,把“待实现”提示替换为确认创建订单弹窗。

entry/src/main/ets/customer/shoppingcart/ShoppingCartBarComponent.ets
Text($r('app.string.label_create_order'))
...
.onClick(_ => {
this.confirmCreateOrder()
})

✅ 预期效果:进入商品列表页,添加商品到购物车后点击“去结算”,会先弹出订单创建确认框;确认后显示 Loading,并向服务端发起创建订单请求。

创建订单确认弹窗
创建订单 Loading

目标:订单页需要能从服务端拉取订单数据,并根据请求结果显示 Loading、异常或内容区域。

5.1 对接订单列表接口

ServerApi.ets 中找到 getUserOrderList() 方法,补充订单列表接口请求逻辑。

接口文档地址:https://docs.apipost.net/docs/detail/2c39ebb29c64000?target_id=4fa9a47

本实验使用的订单列表接口为:

订单列表接口
GET https://api.food2.sziit.top/order-record/list

该接口用于获取当前登录用户的所有订单数据。请求时不需要额外传递业务参数,登录态由项目已有的 ServerApi.execute() 统一处理。

接口成功时,data 字段是订单数组,每一项都是一条订单记录:

订单列表接口响应结构
{
"code": 200,
"msg": "成功",
"data": [
{
"recordId": 1,
"sn": "订单编号",
"storeName": "商铺名称",
"storeImage": "商铺图片地址",
"priceAll": 36,
"payStatus": 0,
"cuiSine": 0,
"createdTime": 1710000000000,
"recordGoodsList": []
}
]
}

模板工程已经在 ServerApi.ets 中预留了 json2ResponseOrderDataList(json) 解析函数,用于把接口返回的 JSON 字符串转换成 Response<ArrayList<OrderData>>。因此本步骤只需要创建 GET 请求、调用 ServerApi.execute(),再把 rsp.result 交给 json2ResponseOrderDataList() 解析即可。

entry/src/main/ets/api/ServerApi.ets
export class ServerApi {
...
/**
* 获取用户的所有订单数据
* @returns
*/
static async getUserOrderList(): Promise<Response<ArrayList<OrderData>>> {
// TODO: 创建 GET 请求,请求地址为 `${Constants.SERVER_HOST}/order-record/list`
// TODO: expectDataType 使用 http.HttpDataType.STRING
// TODO: 调用 ServerApi.execute() 发送请求,并使用 json2ResponseOrderDataList() 解析结果
}
}

✅ 预期效果:ServerApi.getUserOrderList() 方法具备拉取当前用户订单列表的能力。

5.2 补充订单管理页导入

OrderManageView 后续会用到接口、状态视图、标题栏、数据源和订单模型。先补充必要导入。

entry/src/main/ets/order/OrderManageView.ets
import ArrayList from '@ohos.util.ArrayList'
import promptAction from '@ohos.promptAction'
import { ServerApi } from '../api/ServerApi'
import { BasicDataSource } from '../common/datsource/BasicDataSource'
import { ErrorView } from '../common/widget/ErrorView'
import { LoadingView } from '../common/widget/LoadingView'
import { TitleBarView } from '../common/widget/TitleBarView'
import { OrderData } from '../model/OrderData'
import { PageStatus } from '../utils/Constants'

✅ 预期效果:订单管理页已经具备请求和状态流转所需依赖。

5.3 定义页面状态并实现订单数据加载函数骨架

OrderManageView 中新增 pageStatus 页面状态变量,并实现 loadOrderDataList(isRefresh) 方法。isRefresh 用于区分首次加载和后续刷新,本实验先完成首次加载的状态流转。

entry/src/main/ets/order/OrderManageView.ets
@Component
export struct OrderManageView {
// 页面状态
@State
pageStatus: PageStatus = PageStatus.Loading
...
/**
* 订单数据拉取以及页面状态流转
*/
async loadOrderDataList(isRefresh: boolean) {
// TODO: 非刷新场景下,先把 pageStatus 设置为 PageStatus.Loading
// TODO: 调用 ServerApi.getUserOrderList() 拉取订单数据
// TODO: 请求失败时,刷新场景显示 Toast (message_load_order_failed),首次加载场景切换到 PageStatus.Error
// TODO: 请求成功时,非刷新场景切换到 PageStatus.Content
}
}

✅ 预期效果:订单管理页已经具备页面状态变量,以及 Loading、异常和内容状态切换逻辑。

5.4 添加页面加载入口

OrderManageViewaboutToAppear() 生命周期中调用 loadOrderDataList(false),让页面出现时自动加载订单数据。

entry/src/main/ets/order/OrderManageView.ets
@Component
export struct OrderManageView {
aboutToAppear() {
// TODO: 加载订单数据
this.loadOrderDataList(false)
}
...
}

✅ 预期效果:进入订单管理页后,会自动触发订单列表数据加载。

5.5 在页面中接入状态视图

替换 OrderManageView 当前的占位 UI。标题栏固定显示在顶部,内容区域根据 pageStatus 显示 Loading、异常或内容占位。

entry/src/main/ets/order/OrderManageView.ets
@Component
export struct OrderManageView {
...
build() {
Column() {
TitleBarView({
title: $r('app.string.title_customer_order_manage'),
backEnabled: false
})
// TODO: pageStatus 为 Loading 时显示 LoadingView,message 使用 message_loading_order
// TODO: pageStatus 为 Error 时显示 ErrorView,message使用message_load_order_failed,点击后重新调用 loadOrderDataList(false)
// TODO: 其他状态暂时显示“订单数据”占位文字
}
.size({ width: '100%', height: '100%' })
}
}

✅ 预期效果:进入订单 Tab 后,页面顶部显示“我的订单”,接口请求期间显示“正在拉取订单数据”,请求失败时显示异常视图并支持点击重试。

订单页 Loading 状态
订单页异常状态
订单页内容状态

目标:订单数据需要按状态分类查看,本步骤使用 Tabs 搭建“所有、待支付、待上菜、已完成”四个分类入口。

6.1 定义当前选中 Tab

OrderManageView 中新增当前选中的 Tab 坐标。

entry/src/main/ets/order/OrderManageView.ets
@Component
export struct OrderManageView {
...
// 当前选中的 Tab 坐标
@State
currentTabIndex: number = 0
}

✅ 预期效果:订单管理页可以记录当前选中的订单分类。

6.2 实现订单分类导航项

OrderManageView 中新增 TabBarBuilder(label, index),用于自定义每个分类 Tab 的文字和底部指示线。

entry/src/main/ets/order/OrderManageView.ets
@Component
export struct OrderManageView {
...
@Builder
TabBarBuilder(label: ResourceStr, index: number) {
// TODO: 使用 Column 作为单个 Tab 的容器
// TODO: 添加 Text(label),选中时使用 primary_color_light,未选中时使用 black_80
// TODO: Text 字号设置为 20
// TODO: 添加 Divider 作为选中指示线,宽度 100,高度 3,顶部 margin 为 10
// TODO: index 等于 currentTabIndex 时显示 Divider,否则隐藏 Divider
// TODO: 点击 Tab 时,把 currentTabIndex 更新为 index
}
}

✅ 预期效果:每个订单分类 Tab 都具备选中态文字颜色和底部指示线。

6.3 实现订单分类容器

新增 TabBuilder(),使用 Tabs 承载四个 TabContent

entry/src/main/ets/order/OrderManageView.ets
@Component
export struct OrderManageView {
...
@Builder
TabBuilder() {
// TODO: 使用 Tabs({ barPosition: BarPosition.Start, index: this.currentTabIndex })
// TODO: 添加“所有”TabContent,tabBar 调用 TabBarBuilder(tab_all_order, 0)
// TODO: 添加“待支付”TabContent,tabBar 调用 TabBarBuilder(tab_wait_pay_order, 1)
// TODO: 添加“待上菜”TabContent,tabBar 调用 TabBarBuilder(tab_wait_deliver_order, 2)
// TODO: 添加“已完成”TabContent,tabBar 调用 TabBarBuilder(tab_completed_order, 3)
// TODO: onChange 中同步 currentTabIndex
// TODO: 设置 layoutWeight(1)、scrollable(false)、barMode(BarMode.Fixed)、height('100%')
}
}

✅ 预期效果:订单管理页具备四个订单分类 Tab。

6.4 在内容区引入分类导航

OrderManageView 内容状态中的“订单数据”占位替换为 this.TabBuilder()

entry/src/main/ets/order/OrderManageView.ets
@Component
export struct OrderManageView {
build() {
Column() {
...
if (this.pageStatus == PageStatus.Loading) {
...
} else if (this.pageStatus == PageStatus.Error) {
...
} else {
this.TabBuilder()
}
}
.size({ width: '100%', height: '100%' })
}
}

✅ 预期效果:订单数据加载成功后,页面显示“所有、待支付、待上菜、已完成”四个分类 Tab。

订单分类 Tab 效果

目标:把接口返回的订单按状态分发到不同数据源中,为四个分类列表提供独立数据。

7.1 定义四个订单数据源

OrderManageView 中新增四个 BasicDataSource<OrderData>

entry/src/main/ets/order/OrderManageView.ets
@Component
export struct OrderManageView {
...
// 全部订单数据源
orderAllDataSource: BasicDataSource<OrderData> = new BasicDataSource()
// 待支付订单数据源
orderWaitPayDataSource: BasicDataSource<OrderData> = new BasicDataSource()
// 待上菜订单数据源
orderWaitDeliverDataSource: BasicDataSource<OrderData> = new BasicDataSource()
// 已完成订单数据源
orderCompletedDataSource: BasicDataSource<OrderData> = new BasicDataSource()
}

✅ 预期效果:订单管理页已经具备四类订单列表的数据容器。

7.2 按订单状态分发数据

新增 setupDataSource(orderDataList) 方法,根据订单支付状态和上菜状态,把订单放入对应的数据源。

entry/src/main/ets/order/OrderManageView.ets
@Component
export struct OrderManageView {
...
/**
* 根据订单状态填充分类数据源
*/
setupDataSource(orderDataList: ArrayList<OrderData>) {
// TODO: 创建 allOrderArray、waitPayOrderArray、waitDeliverOrderArray、completedOrderArray
// TODO: 遍历 orderDataList,把所有订单加入 allOrderArray
// TODO: payStatus == 0 的订单加入 waitPayOrderArray
// TODO: payStatus == 1 && deliverStatus == 0 的订单加入 waitDeliverOrderArray
// TODO: payStatus == 1 && deliverStatus == 1 的订单加入 completedOrderArray
// TODO: 分别调用四个数据源的 setDataList() 更新列表数据
}
}

✅ 预期效果:接口返回的订单数据可以被拆分到四个订单分类中。

7.3 在加载成功后填充数据源

loadOrderDataList() 请求成功后,调用 setupDataSource()

entry/src/main/ets/order/OrderManageView.ets
@Component
export struct OrderManageView {
async loadOrderDataList(isRefresh: boolean) {
...
// 填充各分类订单数据源
this.setupDataSource(rsp.data ?? new ArrayList())
}
}

✅ 预期效果:订单接口请求成功后,四个订单分类的数据源会同步更新。

目标:每个订单分类都需要一套独立的列表 UI,本步骤完成 OrderListComponent 的空数据和列表渲染能力。

8.1 补充订单列表组件导入

OrderListComponent.ets 中补充数据源、状态、空视图、订单模型和订单小项组件。

entry/src/main/ets/order/list/OrderListComponent.ets
import promptAction from '@ohos.promptAction'
import { BasicDataSource } from '../../common/datsource/BasicDataSource'
import { EmptyView } from '../../common/widget/EmptyView'
import { OrderData } from '../../model/OrderData'
import { PageStatus } from '../../utils/Constants'
import { OrderItemComponent } from './OrderItemComponent'

✅ 预期效果:订单列表组件已经具备列表渲染所需依赖。

8.2 搭建订单列表页面框架

OrderListComponent 从父组件接收一个订单数据源,并根据数据源是否为空显示空数据或列表。

entry/src/main/ets/order/list/OrderListComponent.ets
@Component
export struct OrderListComponent {
// 订单数据源
orderDataSource: BasicDataSource<OrderData> = new BasicDataSource()
// 页面状态
@State
pageStatus: PageStatus = PageStatus.Empty
aboutToAppear() {
// TODO: 根据 orderDataSource.totalCount() 判断当前应为 Content 还是 Empty
// TODO: 有数据时设置为 PageStatus.Content,没有数据时设置为 PageStatus.Empty
}
build() {
Stack() {
// TODO: pageStatus 为 Empty 时显示 EmptyView,message 使用 message_empty_order_data
// TODO: pageStatus 不为 Empty 时显示订单列表
}
.size({ width: '100%', height: '100%' })
}
}

✅ 预期效果:订单列表组件可以根据数据源数量显示空数据或列表区域。

8.3 实现订单列表容器

新增 ListBuilder(),使用 LazyForEach 渲染订单数据源。

entry/src/main/ets/order/list/OrderListComponent.ets
@Component
export struct OrderListComponent {
...
@Builder
ListBuilder() {
// TODO: 使用 List() 作为订单列表容器
// TODO: 使用 LazyForEach(this.orderDataSource, ...) 遍历订单
// TODO: 每个订单使用 ListItem 包裹,暂时使用 Text(`订单sn: ${orderData.sn}`) 来占位
// TODO: 列表分割线 strokeWidth 为 0.5,颜色为 black_10
// TODO: edgeEffect 设置为 EdgeEffect.None
// TODO: 宽高均设置为 100%
}
}

✅ 预期效果:订单列表组件具备渲染订单列表的结构。

8.4 在 build 中接入列表

回到 build(),在空数据视图后面调用 this.ListBuilder()

entry/src/main/ets/order/list/OrderListComponent.ets
@Component
export struct OrderListComponent {
build() {
Stack() {
if (this.pageStatus == PageStatus.Empty) {
...
} else {
this.ListBuilder()
}
}
.size({ width: '100%', height: '100%' })
}
}

✅ 预期效果:订单列表组件已经可以在内容状态下显示列表容器。

8.5 在订单分类 Tab 中引入列表组件

回到 OrderManageView.etsTabBuilder(),把每个 TabContent 的占位内容替换为 OrderListComponent,并分别传入对应数据源。

entry/src/main/ets/order/OrderManageView.ets
import { OrderListComponent } from './list/OrderListComponent'
@Component
export struct OrderManageView {
@Builder
TabBuilder() {
Tabs({ barPosition: BarPosition.Start, index: this.currentTabIndex }) {
TabContent() {
OrderListComponent({ orderDataSource: this.orderAllDataSource })
}
.tabBar(this.TabBarBuilder($r('app.string.tab_all_order'), 0))
TabContent() {
OrderListComponent({ orderDataSource: this.orderWaitPayDataSource })
}
.tabBar(this.TabBarBuilder($r('app.string.tab_wait_pay_order'), 1))
TabContent() {
OrderListComponent({ orderDataSource: this.orderWaitDeliverDataSource })
}
.tabBar(this.TabBarBuilder($r('app.string.tab_wait_deliver_order'), 2))
TabContent() {
OrderListComponent({ orderDataSource: this.orderCompletedDataSource })
}
.tabBar(this.TabBarBuilder($r('app.string.tab_completed_order'), 3))
}
...
}
}

✅ 预期效果:切换不同订单分类 Tab 时,每个 Tab 都会显示自己的订单列表区域。

不同分类下的订单列表容器

目标:订单列表中的每一条订单都需要展示商铺、商品、价格、状态、订单号和创建时间。

9.1 补充订单小项组件导入

OrderItemComponent.ets 中补充商品数据源和商品模型。

entry/src/main/ets/order/list/OrderItemComponent.ets
import { BasicDataSource } from '../../common/datsource/BasicDataSource'
import { GoodsData } from '../../model/GoodsData'
import { OrderData } from '../../model/OrderData'

✅ 预期效果:订单小项组件可以管理订单内的商品列表数据。

9.2 定义订单参数和商品数据源

订单小项组件需要接收一个 OrderData,并把订单内的商品列表放入 BasicDataSource

entry/src/main/ets/order/list/OrderItemComponent.ets
@Component
export struct OrderItemComponent {
// 关联的订单数据
orderData: OrderData = new OrderData()
// 订单内商品数据源
goodsDataSource: BasicDataSource<GoodsData> = new BasicDataSource()
aboutToAppear() {
// TODO: 将 this.orderData.goodsList 填充到 goodsDataSource
this.goodsDataSource.setDataList(this.orderData.goodsList)
}
...
}

✅ 预期效果:订单小项组件出现时,会把当前订单中的商品数据准备好。

9.3 搭建订单小项整体骨架

先搭建整体结构:顶部商铺标题区,中间商品横向列表和订单摘要,底部订单号与创建时间。

entry/src/main/ets/order/list/OrderItemComponent.ets
@Component
export struct OrderItemComponent {
...
build() {
Column() {
// TODO: 顶部商铺标题区
Row() {
// TODO: 左侧订单内商品横向列表
Column() {
// TODO: 右侧订单总价、商品总数和订单状态
}
.padding({ left: 20, right: 20 })
}
Row() {
// TODO: 底部订单号和创建时间
}
.margin({ left: 20, top: 10, right: 20, bottom: 0 })
}
.padding({ top: 10, bottom: 10 })
.alignItems(HorizontalAlign.Start)
.width('100%')
}
}

✅ 预期效果:订单小项的整体布局层次已经明确。

9.4 实现商铺标题区

新增 HeadBuilder(),展示商铺图片、商铺名称和右箭头。

entry/src/main/ets/order/list/OrderItemComponent.ets
@Component
export struct OrderItemComponent {
...
@Builder
HeadBuilder() {
// TODO: 使用 Row 作为标题区容器,宽度 100%,padding 为 { left: 20, right: 20, bottom: 10 }
// TODO: Image 使用 this.orderData.storeImageUrl,size 为 30 x 30,borderRadius 为 5
// TODO: Text 使用 this.orderData.storeName,fontSize 为 16,fontColor 为 Color.Black,左边距为 10
// TODO: 右箭头 Image 使用 ic_next_black,size 为 25 x 25,padding 为 3,左边距为 10
}
}

build() 顶部调用 this.HeadBuilder()

entry/src/main/ets/order/list/OrderItemComponent.ets
build() {
Column() {
this.HeadBuilder()
Row() {
...
}
...
}
}

9.5 实现订单内商品小项

新增 GoodsItemBuilder(goods),用于展示订单中的单个商品。

entry/src/main/ets/order/list/OrderItemComponent.ets
@Component
export struct OrderItemComponent {
...
@Builder
GoodsItemBuilder(goods: GoodsData) {
// TODO: 使用 Column 作为商品小项容器
// TODO: Image 来源为 goods.imageUrl,alt 使用 ic_default_store_image
// TODO: 商品图片 objectFit 为 Cover,borderRadius 为 6,size 为 100 x 70
// TODO: Text 显示 goods.name;当 name 为空字符串时显示“已删除商品”
// TODO: 商品名 fontSize 为 15,maxLines 为 1
// TODO: 商品名超出时使用 TextOverflow.Ellipsis,fontColor 为 black_80,顶部 margin 为 6
}
}

✅ 预期效果:订单内每个商品都有图片和商品名展示。

9.6 实现订单内商品横向列表

新增 GoodsListBuilder(),使用横向 List 展示订单内商品。

entry/src/main/ets/order/list/OrderItemComponent.ets
@Component
export struct OrderItemComponent {
...
@Builder
GoodsListBuilder() {
// TODO: 使用 List({ space: 10 }) 作为商品横向列表
// TODO: 使用 LazyForEach(this.goodsDataSource, ...) 遍历订单内商品
// TODO: 每个 ListItem 内调用 this.GoodsItemBuilder(goods)
// TODO: 第一个商品项左侧 margin 为 20,其他商品项左侧 margin 为 0
// TODO: listDirection 设置为 Axis.Horizontal
// TODO: layoutWeight 设置为 1
}
}

build() 的中间 Row 左侧调用 this.GoodsListBuilder()

entry/src/main/ets/order/list/OrderItemComponent.ets
Row() {
this.GoodsListBuilder()
Column() {
...
}
.padding({ left: 20, right: 20 })
}

✅ 预期效果:订单内商品会以横向列表展示,左右可以滑动查看。

9.7 显示订单总价、商品数量和订单状态

在中间区域右侧的 Column 中补充订单摘要信息。

entry/src/main/ets/order/list/OrderItemComponent.ets
Row() {
this.GoodsListBuilder()
Column() {
// TODO: Text 显示 '¥' + this.orderData.priceAll,颜色为 primary_color_light,字号 20,加粗
// TODO: Text 显示 '共' + this.goodsDataSource.totalCount() + '件',颜色为 black_60,字号 18,顶部 margin 为 10
// TODO: Text 显示 this.resolveOrderStatus(this.orderData),颜色为 black_70,字号 18,顶部 margin 为 10
}
.padding({ left: 20, right: 20 })
}

✅ 预期效果:每条订单右侧显示订单总价、商品件数和当前订单状态。

9.8 显示订单号和创建时间

在底部 Row 中显示订单号和创建时间。

entry/src/main/ets/order/list/OrderItemComponent.ets
Row() {
// TODO: Text 显示 "订单号: " + this.orderData.sn,fontSize 为 11,fontColor 为 black_50
// TODO: 添加 Blank().layoutWeight(1),把创建时间推到右侧
// TODO: Text 显示 '创建时间:' + this.orderData.getCreateTime(),fontSize 为 11,fontColor 为 black_50,左边距为 20
}
.margin({ left: 20, top: 10, right: 20, bottom: 0 })

✅ 预期效果:每条订单底部显示订单号和创建时间。

9.9 在订单列表中接入订单小项

回到 OrderListComponent.ets,在 ListBuilder()ListItem 中使用 OrderItemComponent

entry/src/main/ets/order/list/OrderListComponent.ets
@Builder
ListBuilder() {
List() {
LazyForEach(
this.orderDataSource,
(item: OrderData) => {
ListItem() {
OrderItemComponent({ orderData: item })
.onClick(_ => {
// TODO: 订单详情页将在后续实验开发,这里先显示待实现提示
promptAction.showToast({ message: '跳转到订单详情页,待实现' })
})
}
},
(item: OrderData) => JSON.stringify(item)
)
}
}

✅ 预期效果:订单列表中可以显示完整的订单小项,点击订单小项会提示订单详情页待实现。

订单列表效果

目标:创建订单发生在商品列表页,订单列表页需要收到通知并立即插入新订单,避免用户回到订单页后看不到刚创建的订单。

10.1 发送订单创建事件

ShoppingCartBarComponent.etscreateOrder() 中,订单创建成功后,把接口返回的 OrderData 转成 JSON,并通过 emitter 发送出去。

entry/src/main/ets/customer/shoppingcart/ShoppingCartBarComponent.ets
import emitter from '@ohos.events.emitter'
import { plainToClass } from 'class-transformer'
@Component
export struct ShoppingCartBarComponent {
async createOrder() {
...
let rsp = await ServerApi.createOrder(this.storeData.id)
if (rsp.isSuccess()) {
// 通知订单创建事件
const orderJson = JSON.stringify(instanceToPlain<OrderData>(rsp.data))
emitter.emit(Constants.EVENT_ORDER_CREATED, {
data: {
'orderJson': orderJson
}
})
} else {
...
}
}
}

✅ 预期效果:每次订单创建成功后,应用内都会发送一条订单创建事件。

10.2 处理订单创建事件

OrderManageView 中新增 orderCreateHandler(eventData)。新创建的订单通常是待支付状态,因此需要插入“所有订单”和“待支付订单”两个数据源顶部。

entry/src/main/ets/order/OrderManageView.ets
@Component
export struct OrderManageView {
...
/**
* 处理订单创建消息
*/
orderCreateHandler(eventData: emitter.EventData) {
// TODO: 从 eventData.data 中读取 orderJson: let orderJson = eventData.data!.orderJson as string
// TODO: 如果 orderJson 为空,则直接 return
// TODO: 使用 plainToClass(OrderData, JSON.parse(orderJson)) 还原订单对象
// TODO: 将新订单插入 orderAllDataSource 的第 0 位
// TODO: 将新订单插入 orderWaitPayDataSource 的第 0 位
}
}

✅ 预期效果:订单管理页收到新订单事件后,可以把新订单插入列表顶部。

10.3 订阅和取消订阅订单创建事件

aboutToAppear() 中订阅订单创建事件,在 aboutToDisappear() 中取消订阅,避免重复监听。

entry/src/main/ets/order/OrderManageView.ets
@Component
export struct OrderManageView {
aboutToAppear() {
// 监听订单创建事件
emitter.on(Constants.EVENT_ORDER_CREATED, (eventData: emitter.EventData) => {
this.orderCreateHandler(eventData)
})
// 加载订单数据
this.loadOrderDataList(false)
}
aboutToDisappear() {
// 取消监听订单创建事件
emitter.off(Constants.EVENT_ORDER_CREATED.eventId)
}
}

✅ 预期效果:在商品列表页创建新订单后,切换到订单 Tab,新订单会显示在“所有”和“待支付”列表顶部。

新订单插入列表顶部效果

完成上述步骤后,请按下面流程进行验证:

  1. 启动 APP 并完成登录。
  2. 在“点餐”Tab 中进入任意商铺。
  3. 添加至少一个商品到购物车。
  4. 点击底部“去结算”按钮。
  5. 在确认弹窗中点击“确认”。
  6. 等待订单创建完成,确认页面返回上一层并提示创建成功。
  7. 切换到底部“订单”Tab。
  8. 查看“所有”和“待支付”分类,确认刚创建的订单显示在列表顶部。
  9. 切换“待上菜”和“已完成”分类,确认不同状态列表可以正常展示。
  10. 点击某条订单,确认页面提示“跳转到订单详情页,待实现”。
商品列表页
订单列表页

12.1 为订单列表增加下拉刷新

当前订单列表只在进入订单管理页时拉取数据。可以尝试使用 Refresh 组件为 OrderListComponent 增加下拉刷新能力。

实现思路:

  • Refresh 作为 OrderListComponent 的根容器
  • RefreshonRefreshing 中通知父组件重新拉取订单数据
  • 刷新完成后关闭刷新动画
  • 刷新失败时显示 Toast 提示

参考文档:Refresh 组件

12.2 支持订单详情页跳转

订单详情页将在后续实验继续开发。可以先尝试完成路由参数传递:

  • 点击订单小项时,将当前 OrderData 序列化为字符串
  • 使用 router.pushUrl() 跳转到订单详情页
  • 在订单详情页中读取并解析路由参数

请完成点餐 APP 订单创建与订单列表展示功能,并提交最终演示录屏。

录屏需要覆盖以下内容:

  • 从商品列表页添加商品到购物车
  • 点击“去结算”并确认创建订单
  • 创建订单过程显示 Loading
  • 订单创建成功后清空购物车并返回上一页
  • 进入订单 Tab 后查看订单列表
  • 切换“所有、待支付、待上菜、已完成”四个分类
  • 点击订单小项,显示订单详情页待实现提示

评分参考:

评分项要求
功能完整度能完成订单创建、订单列表拉取、分类展示和新订单监听
页面效果订单列表 UI 与示例效果接近,布局清晰,无明显错位
代码结构页面容器、列表组件和订单小项职责清晰
交互反馈创建订单有确认弹窗、Loading 和失败提示
拓展完成情况完成下拉刷新或订单详情页跳转可获得加分