编程实验:开发订单创建与列表页
本实验将基于 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 对象 |
开发思路
本实验按照“先打通业务链路,再完善列表页面,再处理跨页面刷新”的顺序推进:
- 先在购物车底部栏完成“创建订单”流程,让用户可以从购物车提交订单。
- 再搭建订单管理页,完成订单接口请求和页面状态流转。
- 接着实现订单分类 Tab 和分类数据源,把订单按状态分开管理。
- 然后开发订单列表和订单小项 UI,展示订单内商品、价格、状态、订单号和创建时间。
- 最后使用
emitter处理新订单通知,解决创建订单后订单列表不及时更新的问题。
目标:导入并运行模板工程,确认当前工程状态,明确本实验需要修改的文件。
2.1 获取模板工程
模板工程仓库:https://cnb.cool/sziit-coding/harmony-coding/09_order_create_list
请使用 DevEco Studio 打开该模板工程,并确认工程可以正常运行。
2.2 认识模板工程当前状态
模板工程已经完成登录、首页 Tab、商铺列表页、商品列表页和购物车功能。用户登录后进入首页,点击商铺可以进入商品列表页,在商品列表中添加商品后,底部购物车栏会显示商品总数和总价。
订单相关功能目前只提供了基础骨架:
OrderManageView.ets只显示“订单列表”占位文字OrderListComponent.ets只提供了空的StackOrderItemComponent.ets已提供订单状态解析方法,但 UI 为空ServerApi.ets中createOrder()和getUserOrderList()仍是待实现方法- 购物车底部栏的“去结算”按钮当前只显示“待实现”提示
@Componentexport struct OrderManageView { build() { Column() { Text("订单列表").fontSize(40).fontWeight(FontWeight.Bold).height('100%') } .size({ width: '100%', height: '100%' }) }}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 中已经定义了订单字段与接口字段之间的映射:
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 中已经定义了订单创建事件:
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') 提供了商铺信息:
@Componentexport 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_v2Content-Type: application/x-www-form-urlencoded请求时需要通过表单参数传递当前商铺 ID:
| 参数名 | 类型 | 是否必填 | 说明 |
|---|---|---|---|
storeId | number | 是 | 当前创建订单的商铺 ID |
接口成功时会返回创建后的订单数据,外层仍然使用项目统一的 Response<T> 响应结构:
{ "code": 200, "msg": "成功", "data": { "recordId": 1, "sn": "订单编号", "storeName": "商铺名称", "storeImage": "商铺图片地址", "priceAll": 36, "payStatus": 0, "cuiSine": 0, "recordGoodsList": [] }}模板工程中的 OrderData.ets 已经通过 @Expose 完成字段映射,例如 recordId 会映射到 id,storeImage 会映射到 storeImageUrl,cuiSine 会映射到 deliverStatus,recordGoodsList 会映射到 goodsList。因此本步骤不需要重新定义订单模型,只需要在请求成功后使用 plainToClassFromExist(new Response<OrderData>(OrderData), JSON.parse(rsp.result as string)) 将接口返回值转换成 Response<OrderData>。
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 中补充相关导入。
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') 获取当前商铺。
@Componentexport 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,并暂时返回上一页。
@Componentexport 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()。
@Componentexport 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 给“去结算”按钮接入点击事件
找到“去结算”按钮当前的点击事件,把“待实现”提示替换为确认创建订单弹窗。
Text($r('app.string.label_create_order')) ... .onClick(_ => { this.confirmCreateOrder() })✅ 预期效果:进入商品列表页,添加商品到购物车后点击“去结算”,会先弹出订单创建确认框;确认后显示 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() 解析即可。
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 后续会用到接口、状态视图、标题栏、数据源和订单模型。先补充必要导入。
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 用于区分首次加载和后续刷新,本实验先完成首次加载的状态流转。
@Componentexport 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 添加页面加载入口
在 OrderManageView 的 aboutToAppear() 生命周期中调用 loadOrderDataList(false),让页面出现时自动加载订单数据。
@Componentexport struct OrderManageView { aboutToAppear() { // TODO: 加载订单数据 this.loadOrderDataList(false) }
...}✅ 预期效果:进入订单管理页后,会自动触发订单列表数据加载。
5.5 在页面中接入状态视图
替换 OrderManageView 当前的占位 UI。标题栏固定显示在顶部,内容区域根据 pageStatus 显示 Loading、异常或内容占位。
@Componentexport 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 后,页面顶部显示“我的订单”,接口请求期间显示“正在拉取订单数据”,请求失败时显示异常视图并支持点击重试。
目标:订单数据需要按状态分类查看,本步骤使用 Tabs 搭建“所有、待支付、待上菜、已完成”四个分类入口。
6.1 定义当前选中 Tab
在 OrderManageView 中新增当前选中的 Tab 坐标。
@Componentexport struct OrderManageView { ...
// 当前选中的 Tab 坐标 @State currentTabIndex: number = 0}✅ 预期效果:订单管理页可以记录当前选中的订单分类。
6.2 实现订单分类导航项
在 OrderManageView 中新增 TabBarBuilder(label, index),用于自定义每个分类 Tab 的文字和底部指示线。
@Componentexport 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。
@Componentexport 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()。
@Componentexport struct OrderManageView { build() { Column() { ... if (this.pageStatus == PageStatus.Loading) { ... } else if (this.pageStatus == PageStatus.Error) { ... } else { this.TabBuilder() } } .size({ width: '100%', height: '100%' }) }}✅ 预期效果:订单数据加载成功后,页面显示“所有、待支付、待上菜、已完成”四个分类 Tab。
目标:把接口返回的订单按状态分发到不同数据源中,为四个分类列表提供独立数据。
7.1 定义四个订单数据源
在 OrderManageView 中新增四个 BasicDataSource<OrderData>。
@Componentexport 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) 方法,根据订单支付状态和上菜状态,把订单放入对应的数据源。
@Componentexport 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()。
@Componentexport struct OrderManageView { async loadOrderDataList(isRefresh: boolean) { ...
// 填充各分类订单数据源 this.setupDataSource(rsp.data ?? new ArrayList()) }}✅ 预期效果:订单接口请求成功后,四个订单分类的数据源会同步更新。
目标:每个订单分类都需要一套独立的列表 UI,本步骤完成 OrderListComponent 的空数据和列表渲染能力。
8.1 补充订单列表组件导入
在 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 从父组件接收一个订单数据源,并根据数据源是否为空显示空数据或列表。
@Componentexport 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 渲染订单数据源。
@Componentexport 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()。
@Componentexport struct OrderListComponent { build() { Stack() { if (this.pageStatus == PageStatus.Empty) { ... } else { this.ListBuilder() } } .size({ width: '100%', height: '100%' }) }}✅ 预期效果:订单列表组件已经可以在内容状态下显示列表容器。
8.5 在订单分类 Tab 中引入列表组件
回到 OrderManageView.ets 的 TabBuilder(),把每个 TabContent 的占位内容替换为 OrderListComponent,并分别传入对应数据源。
import { OrderListComponent } from './list/OrderListComponent'
@Componentexport 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 中补充商品数据源和商品模型。
import { BasicDataSource } from '../../common/datsource/BasicDataSource'import { GoodsData } from '../../model/GoodsData'import { OrderData } from '../../model/OrderData'✅ 预期效果:订单小项组件可以管理订单内的商品列表数据。
9.2 定义订单参数和商品数据源
订单小项组件需要接收一个 OrderData,并把订单内的商品列表放入 BasicDataSource。
@Componentexport 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 搭建订单小项整体骨架
先搭建整体结构:顶部商铺标题区,中间商品横向列表和订单摘要,底部订单号与创建时间。
@Componentexport 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(),展示商铺图片、商铺名称和右箭头。
@Componentexport 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()。
build() { Column() { this.HeadBuilder() Row() { ... } ... }}9.5 实现订单内商品小项
新增 GoodsItemBuilder(goods),用于展示订单中的单个商品。
@Componentexport 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 展示订单内商品。
@Componentexport 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()。
Row() { this.GoodsListBuilder() Column() { ... } .padding({ left: 20, right: 20 })}✅ 预期效果:订单内商品会以横向列表展示,左右可以滑动查看。
9.7 显示订单总价、商品数量和订单状态
在中间区域右侧的 Column 中补充订单摘要信息。
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 中显示订单号和创建时间。
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。
@BuilderListBuilder() { List() { LazyForEach( this.orderDataSource, (item: OrderData) => { ListItem() { OrderItemComponent({ orderData: item }) .onClick(_ => { // TODO: 订单详情页将在后续实验开发,这里先显示待实现提示 promptAction.showToast({ message: '跳转到订单详情页,待实现' }) }) } }, (item: OrderData) => JSON.stringify(item) ) }}✅ 预期效果:订单列表中可以显示完整的订单小项,点击订单小项会提示订单详情页待实现。
目标:创建订单发生在商品列表页,订单列表页需要收到通知并立即插入新订单,避免用户回到订单页后看不到刚创建的订单。
10.1 发送订单创建事件
在 ShoppingCartBarComponent.ets 的 createOrder() 中,订单创建成功后,把接口返回的 OrderData 转成 JSON,并通过 emitter 发送出去。
import emitter from '@ohos.events.emitter'import { plainToClass } from 'class-transformer'
@Componentexport 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)。新创建的订单通常是待支付状态,因此需要插入“所有订单”和“待支付订单”两个数据源顶部。
@Componentexport 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() 中取消订阅,避免重复监听。
@Componentexport 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,新订单会显示在“所有”和“待支付”列表顶部。
完成上述步骤后,请按下面流程进行验证:
- 启动 APP 并完成登录。
- 在“点餐”Tab 中进入任意商铺。
- 添加至少一个商品到购物车。
- 点击底部“去结算”按钮。
- 在确认弹窗中点击“确认”。
- 等待订单创建完成,确认页面返回上一层并提示创建成功。
- 切换到底部“订单”Tab。
- 查看“所有”和“待支付”分类,确认刚创建的订单显示在列表顶部。
- 切换“待上菜”和“已完成”分类,确认不同状态列表可以正常展示。
- 点击某条订单,确认页面提示“跳转到订单详情页,待实现”。
12.1 为订单列表增加下拉刷新
当前订单列表只在进入订单管理页时拉取数据。可以尝试使用 Refresh 组件为 OrderListComponent 增加下拉刷新能力。
实现思路:
- 将
Refresh作为OrderListComponent的根容器 - 在
Refresh的onRefreshing中通知父组件重新拉取订单数据 - 刷新完成后关闭刷新动画
- 刷新失败时显示 Toast 提示
参考文档:Refresh 组件
12.2 支持订单详情页跳转
订单详情页将在后续实验继续开发。可以先尝试完成路由参数传递:
- 点击订单小项时,将当前
OrderData序列化为字符串 - 使用
router.pushUrl()跳转到订单详情页 - 在订单详情页中读取并解析路由参数
请完成点餐 APP 订单创建与订单列表展示功能,并提交最终演示录屏。
录屏需要覆盖以下内容:
- 从商品列表页添加商品到购物车
- 点击“去结算”并确认创建订单
- 创建订单过程显示 Loading
- 订单创建成功后清空购物车并返回上一页
- 进入订单 Tab 后查看订单列表
- 切换“所有、待支付、待上菜、已完成”四个分类
- 点击订单小项,显示订单详情页待实现提示
评分参考:
| 评分项 | 要求 |
|---|---|
| 功能完整度 | 能完成订单创建、订单列表拉取、分类展示和新订单监听 |
| 页面效果 | 订单列表 UI 与示例效果接近,布局清晰,无明显错位 |
| 代码结构 | 页面容器、列表组件和订单小项职责清晰 |
| 交互反馈 | 创建订单有确认弹窗、Loading 和失败提示 |
| 拓展完成情况 | 完成下拉刷新或订单详情页跳转可获得加分 |