编程实验:开发订单详情页与支付功能
本实验将基于 10_order_details_pay 模板工程,在已经完成订单创建与订单列表页的基础上,继续开发点餐 APP 的订单详情页与支付功能。最终效果是:用户点击订单列表中的订单后进入订单详情页,查看订单状态、时间线、商铺信息和商品明细;待支付订单可以选择支付方式并完成支付,支付成功后订单列表分类同步更新。
本实验主要完成以下任务:
- 使用路由参数把订单列表中的订单数据传递到订单详情页
- 搭建订单详情页骨架,展示订单状态与订单时间线
- 展示订单号、商铺信息、订单商品明细和订单合计金额
- 开发待支付组件,显示订单总价、支付倒计时和支付入口
- 对接订单支付接口,完成支付请求与页面状态更新
- 使用
emitter通知订单管理页,让支付后的订单自动移动到待上菜分类
最终效果
目标:导入本实验模板工程,确认订单列表、订单详情页空壳和支付相关基础文件已经准备完成。
模板工程仓库:
https://cnb.cool/sziit-coding/harmony-coding/10_order_details_pay请将仓库克隆到本地后,使用 DevEco Studio 打开并运行。模板工程中已经具备以下内容:
- 订单创建功能与订单列表页
OrderManageView中的订单分类数据源pages/OrderDetail.ets页面入口order/detail/OrderDetailView.ets订单详情页空组件order/payment/PaymentComponent.ets待支付组件空骨架order/payment/PaymentDialog.ets支付渠道选择弹窗ServerApi.payOrder()支付接口方法骨架
运行模板工程后,完成登录、进入商铺、添加商品、创建订单,并确认订单 Tab 中可以看到新创建的订单。
✅ 预期效果:模板工程可以正常运行,订单 Tab 能展示已创建的订单列表。
目标:订单列表页只保存列表项数据,订单详情页需要接收被点击订单的完整信息。本步骤通过路由参数把 OrderData 从列表页传递到详情页。
1.1 定义跳转订单详情页方法
在 OrderListComponent 中新增 gotoOrderDetail 方法,将订单对象转成普通 JSON 数据后传入 pages/OrderDetail。
import router from '@ohos.router'import { instanceToPlain } from 'class-transformer'
@Componentexport struct OrderListComponent { ...
gotoOrderDetail(order: OrderData) { // TODO: 调用 router.pushUrl 跳转到 pages/OrderDetail // TODO: 在 params 中传入 orderJson // · 使用 instanceToPlain(order) 去掉 OrderData 的类实例包装 // · 使用 JSON.stringify(...) 转成字符串,便于路由参数传递 }}1.2 绑定订单点击事件
在列表项中绑定点击事件,点击订单小项时进入详情页。
@BuilderListBuilder() { List() { LazyForEach( this.orderDataSource, (item: OrderData) => { ListItem() { OrderItemComponent({ orderData: item }) // TODO: 给订单小项绑定 onClick 事件 // · 点击后调用 this.gotoOrderDetail(item),跳转到订单详情页并传递当前订单数据 } }, (item: OrderData) => JSON.stringify(item) ) }}1.3 在订单详情页解析路由参数
OrderDetailView 进入页面时读取路由参数,并把 orderJson 还原成 OrderData。
import router from '@ohos.router'import { plainToClass } from 'class-transformer'import { OrderData } from '../../model/OrderData'
@Componentexport struct OrderDetailView { // TODO: 声明 orderData 状态,类型为 OrderData,默认值为 new OrderData()
aboutToAppear() { const params = router.getParams() as Record<string, Object> if (params == undefined || params.orderJson == undefined) { return } // TODO: 使用 plainToClass 将 params.orderJson 还原为 OrderData,并赋值给 this.orderData }
build() { Column() { } .backgroundColor($r('app.color.black_5')) .size({ width: '100%', height: '100%' }) }}✅ 预期效果:点击订单列表中的任意订单,可以进入订单详情页;在详情页中可以通过临时文本或日志确认已拿到该订单数据。
目标:订单详情页需要让用户立即判断订单处于待支付、待上菜还是已完成状态。本步骤完成页面骨架、订单状态标题和三段式时间线。
2.1 搭建订单详情页骨架
在页面顶部加入标题栏,主体区域使用 Scroll 承载订单详情内容。
import { TitleBarView } from '../../common/widget/TitleBarView'
@Componentexport struct OrderDetailView { ...
build() { Column() { TitleBarView({ title: $r('app.string.title_order_detail'), backEnabled: true }) Scroll() { Column() { // TODO: 显示订单状态 Text(this.orderData.getStatusDesc()) // · fontColor: Color.Black // · fontSize: 32 // · margin: { left: 20, top: 30 } } .alignItems(HorizontalAlign.Start) } .align(Alignment.Top) .layoutWeight(1) } .backgroundColor($r('app.color.black_5')) .size({ width: '100%', height: '100%' }) }}2.2 定义时间线 Builder
在 OrderDetailView 中新增 TimelineBuilder,用于集中绘制订单时间线。
@Componentexport struct OrderDetailView { ...
@Builder TimelineBuilder() { // TODO: 使用 Column 纵向组织时间线,margin({ top: 30 }),width('100%') // TODO: 第一行显示下单时间与上菜时间 // · 左侧文本:`下单时间:\n${this.orderData.getCreateTime()}`,fontSize: 15,fontColor: Color.Black // · 中间使用 Blank().layoutWeight(1) 撑开两侧文本 // · 右侧文本根据 deliverStatus 显示 `上菜时间:\n-` 或 this.orderData.getDeliverTime() // · 未上菜时 fontColor 使用 black_20,已上菜时使用 Color.Black // · Row.padding({ left: 20, right: 20 }),width('100%') // TODO: 第二行绘制三个 Circle 节点和两条 Divider 连线 // · Circle 尺寸均为 { width: 10, height: 10 } // · 第一段连线和第二个节点根据 payStatus 切换 black / black_20 // · 第二段连线和第三个节点根据 deliverStatus 切换 black / black_20 // · 两条 Divider 均设置 strokeWidth(1)、layoutWeight(1) // · Row.margin({ top: 10 }),padding({ left: 25, right: 25 }),width('100%') // TODO: 第三行居中显示支付时间 // · 未支付时显示 `支付时间:\n-`,已支付时显示 this.orderData.getPayTime() // · 未支付时 fontColor 使用 black_20,已支付时使用 Color.Black // · Row.justifyContent(FlexAlign.Center),margin({ top: 10 }),padding({ left: 20, right: 20 }),width('100%') }}2.3 在页面中引入时间线
把时间线放在订单状态文本下方。
Scroll() { Column() { Text(this.orderData.getStatusDesc()) .fontColor(Color.Black) .fontSize(32) .margin({ left: 20, top: 30 })
this.TimelineBuilder() } .alignItems(HorizontalAlign.Start)}✅ 预期效果:订单详情页显示订单状态标题和时间线;未支付订单的支付时间、上菜时间显示为 -,相关节点为浅灰色。
目标:订单详情页需要展示订单的完整业务信息。本步骤完成订单号、商铺、商品明细和订单总价的展示。
3.1 定义订单详情卡片
在 OrderDetailView 中新增 OrderInfoBuilder,用于展示订单基础信息。
@Componentexport struct OrderDetailView { ...
@Builder OrderInfoBuilder() { // TODO: 使用 Column 作为白色详情卡片 // · alignItems(HorizontalAlign.Start) // · width('100%'),borderRadius(7),backgroundColor(Color.White) // · padding({ left: 20, right: 20, top: 20, bottom: 20 }) // TODO: 显示订单号 Text('订单号: ' + this.orderData.sn) // · fontSize: 20,fontColor 使用 black_70 // TODO: 显示商铺信息 Row,margin({ top: 10 }) // · 商铺标签 Text('商铺: '),fontSize: 20,fontColor 使用 black_70 // · 商铺图片 Image(this.orderData.storeImageUrl),size: { width: 20, height: 20 },borderRadius: 4,margin({ left: 10 }) // · 商铺名称 Text(this.orderData.storeName),fontSize: 20,fontColor 使用 black_60,margin({ left: 5 }) // TODO: 显示“订单商品”标题,margin({ top: 25, bottom: 10 }) // TODO: 使用 ForEach 遍历 this.orderData.goodsList,调用 this.GoodsItemBuilder(goods) // TODO: 显示合计金额 Text('合计:¥' + this.orderData.priceAll) // · fontSize: 20,fontColor 使用 black,alignSelf(ItemAlign.End),margin({ top: 15 }) }}3.2 定义商品小项 Builder
订单中的每个商品都需要展示图片、名称、单价、数量和小计金额。
import { GoodsData } from '../../model/GoodsData'
@Componentexport struct OrderDetailView { ...
@Builder GoodsItemBuilder(goods: GoodsData) { // TODO: 使用 Column 作为商品小项容器 // TODO: 第一行使用 Row 展示商品信息,alignItems(VerticalAlign.Top) // TODO: 左侧商品图片 Image(goods.imageUrl) // · size: { width: 70, height: 70 },borderRadius: 8 // TODO: 中间商品文本 Column // · 商品名称 Text(goods.name),fontSize: 21,fontColor: Color.Black // · 商品单价 Text('单价:' + goods.price),margin({ top: 6 }),fontSize: 17,fontColor 使用 black_60 // · 商品数量 Text('数量:' + goods.number),margin({ top: 6 }),fontSize: 17,fontColor 使用 black_60 // · Column.alignItems(HorizontalAlign.Start),margin({ left: 10 }),layoutWeight(1) // TODO: 右侧商品小计 Text('¥' + goods.countPrice) // · fontSize: 20,fontColor 使用 primary_color_light // TODO: 商品行下方追加 Divider // · height: 0.5,width: '100%',color 使用 black_10 // · margin({ top: 8, bottom: 8 }) }}3.3 在页面中引入订单详情卡片
把订单详情卡片放在时间线下方。
Scroll() { Column() { Text(this.orderData.getStatusDesc()) ... this.TimelineBuilder()
Row() { this.OrderInfoBuilder() } .width('100%') .margin({ top: 10 }) .padding({ left: 20, right: 20, top: 20, bottom: 20 }) }}✅ 预期效果:订单详情页可以完整展示订单号、商铺图片、商铺名称、商品列表、商品数量、商品小计和订单合计金额。
目标:待支付订单需要提供明确的支付入口。本步骤开发支付卡片,并只在订单未支付时显示。
4.1 搭建待支付组件 UI
在 PaymentComponent 中通过 @Link 接收订单数据,并绘制支付卡片。
import { OrderData } from '../../model/OrderData'
@Componentexport struct PaymentComponent { @Link orderData: OrderData
...
build() { // TODO: 使用 Column 作为待支付卡片容器 // · alignItems(HorizontalAlign.Start) // · width('100%'),borderRadius(7),backgroundColor(Color.White) // · padding({ left: 20, right: 20, top: 20, bottom: 20 }) // TODO: 显示订单总价 Text('订单总价:¥' + this.orderData.priceAll) // · fontSize: 24,fontColor 使用 black_90 // TODO: 显示剩余支付时间 Row,margin({ top: 15 }) // · 标签文本 Text('剩余支付时间(仅演示):'),fontSize: 18,fontColor 使用 black_60 // · 倒计时文本 Text(this.formattedTime()),fontSize: 18,fontColor 使用 black_60 // TODO: 显示“去支付”按钮 Button($r('app.string.label_go_payment')) // · type: ButtonType.Normal,fontSize: 20,fontColor: Color.White // · width: '100%',borderRadius: 2,margin({ top: 15 }) // · backgroundColor 使用 primary_color,点击后调用 this.choosePayChannel() }}4.2 实现支付倒计时
使用定时器每秒减少剩余秒数,并在组件销毁时清理定时器。
@Componentexport struct PaymentComponent { @State remainSeconds: number = 60 * 15 intervalId: number = 0
aboutToAppear() { this.intervalId = setInterval(() => { // TODO: 每秒让 this.remainSeconds 自减 1 // TODO: 当剩余秒数小于等于 0 时,停止继续递减 }, 1000) }
aboutToDisappear() { if (this.intervalId != 0) { clearInterval(this.intervalId) } }
formattedTime(): string { // TODO: 根据 this.remainSeconds 计算 minutes 和 seconds // TODO: 使用 padStart(2, '0') 把分钟和秒格式化为两位数字 // TODO: 返回 `mm:ss` 格式的字符串 return '00:00' }
...}4.3 在订单详情页引入待支付组件
待支付组件只在 payStatus == 0 时显示。
import { PaymentComponent } from '../payment/PaymentComponent'
Scroll() { Column() { Text(this.orderData.getStatusDesc()) ... this.TimelineBuilder()
PaymentComponent({ orderData: $orderData }) .visibility(this.orderData.payStatus == 0 ? Visibility.Visible : Visibility.None) .padding({ left: 20, right: 20, top: 20 })
Row() { this.OrderInfoBuilder() } ... }}✅ 预期效果:待支付订单的详情页显示支付卡片和倒计时;已支付或已完成订单不显示支付卡片。
目标:支付按钮需要发起真实支付请求,并在请求过程中提供弹窗选择、Loading 等待和成功失败反馈。
5.1 对接订单支付接口
在 ServerApi 中补充支付接口请求逻辑。
export class ServerApi { ...
static async payOrder(orderId: number): Promise<Response<Object>> { let request = http.createHttp() const options: http.HttpRequestOptions = { method: http.RequestMethod.POST, header: { 'Content-Type': 'application/x-www-form-urlencoded' }, extraData: `recordId=${orderId}`, expectDataType: http.HttpDataType.OBJECT } // TODO: 调用 ServerApi.execute,请求 `${Constants.SERVER_HOST}/order-record/pay` // TODO: 使用 plainToClassFromExist(new Response<Object>(Object), rsp.result) 返回响应数据 }}5.2 引入支付渠道弹窗
模板工程已提供 PaymentDialog,在 PaymentComponent 中创建弹窗控制器。
import { PaymentDialog } from './PaymentDialog'
@Componentexport struct PaymentComponent { ...
private payChannelDialogController = new CustomDialogController({ builder: PaymentDialog({ // TODO: 在 onConfirmListener 中调用 this.payOrder() }), // TODO: 设置 autoCancel: false,避免点击弹窗外部自动关闭 // TODO: 设置 gridCount: 6,控制弹窗宽度 })
choosePayChannel() { // TODO: 调用 payChannelDialogController.open() 打开支付渠道选择弹窗 }}5.3 实现支付流程
支付确认后调用接口,并根据接口结果更新订单详情页状态。
import promptAction from '@ohos.promptAction'import { ServerApi } from '../../api/ServerApi'
@Componentexport struct PaymentComponent { ...
async payOrder() { this.loadingDialogController.open() const rsp = await ServerApi.payOrder(this.orderData.id) if (!rsp.isSuccess()) { promptAction.showToast({ message: $r('app.string.message_pay_order_failed') }) this.loadingDialogController.close() return }
// TODO: 将 this.orderData.payStatus 更新为已支付状态 // TODO: 将 this.orderData.payTime 更新为当前秒级时间戳 // TODO: 使用 message_pay_order_success 显示支付成功 Toast // TODO: 关闭 Loading 弹窗 }}✅ 预期效果:点击“去支付”后弹出支付方式选择弹窗;确认支付后显示 Loading;支付成功后详情页状态变为待上菜,支付时间显示为当前时间。
目标:支付成功后,订单详情页状态已经更新,但订单管理页中的分类数据源也需要同步调整。本步骤通过事件通知订单管理页更新分类列表。
6.1 支付成功后发送事件
在 PaymentComponent 支付成功后发送订单支付事件。
import emitter from '@ohos.events.emitter'import { Constants } from '../../utils/Constants'
@Componentexport struct PaymentComponent { ...
async payOrder() { ... promptAction.showToast({ message: $r('app.string.message_pay_order_success') }) this.loadingDialogController.close()
// TODO: 使用 emitter.emit 发送 Constants.EVENT_ORDER_PAYED 事件 // · data 中传入 orderId: this.orderData.id }}6.2 编写订单支付事件处理函数
在 OrderManageView 中根据支付完成的订单 id,调整三个数据源中的订单位置。
@Componentexport struct OrderManageView { ...
orderPayedHandler(eventData: emitter.EventData) { const orderId = eventData.data!.orderId as number if (orderId == undefined || orderId == null) { return }
// TODO: 在 orderAllDataSource 中查找 id 等于 orderId 的订单下标 // TODO: 如果没有找到该订单,直接 return // TODO: 取出订单对象,将 payStatus 更新为 1,并把 payTime 更新为当前秒级时间戳 // TODO: 调用 orderAllDataSource.updateData 更新“全部”列表中的订单
// TODO: 在 orderWaitPayDataSource 中查找 id 等于 orderId 的订单下标 // TODO: 如果找到了该订单,把它从“待支付”列表中移除
// TODO: 将更新后的订单插入 orderWaitDeliverDataSource 的第 0 位 }}6.3 订阅和取消订阅支付事件
在订单管理页进入时订阅支付事件,页面退出时取消订阅。
@Componentexport struct OrderManageView { ...
aboutToAppear() { emitter.on(Constants.EVENT_ORDER_CREATED, (eventData) => { this.orderCreateHandler(eventData) }) emitter.on(Constants.EVENT_ORDER_PAYED, (eventData) => { this.orderPayedHandler(eventData) }) this.loadOrderDataList(false) }
aboutToDisappear() { emitter.off(Constants.EVENT_ORDER_CREATED.eventId) emitter.off(Constants.EVENT_ORDER_PAYED.eventId) }}✅ 预期效果:待支付订单完成支付后,返回订单管理页,该订单会自动从“待支付”分类移除,并出现在“待上菜”分类中。
完成点餐 APP 订单详情页与支付功能开发,并提交最终演示录屏。
验收清单
- 可以从订单列表页点击任意订单进入订单详情页
- 订单详情页能展示订单状态、时间线、订单号、商铺信息、商品明细和订单合计金额
- 待支付订单能显示支付卡片、倒计时和“去支付”按钮
- 点击“去支付”后能选择微信支付、支付宝支付或银联支付
- 确认支付后能显示 Loading,支付成功后更新详情页订单状态和支付时间
- 返回订单管理页后,支付成功的订单能自动从“待支付”移动到“待上菜”
提交要求
- 提交最终演示录屏,视频画面需清晰
- 录屏需覆盖创建订单、进入订单详情、选择支付方式、完成支付、返回订单管理页查看分类更新的完整流程
评分标准
| 评分项 | 要求 |
|---|---|
| 订单详情页数据展示 | 订单状态、时间线、订单号、商铺信息、商品明细和合计金额展示完整 |
| 支付组件开发 | 待支付订单能显示支付卡片、倒计时和支付入口,非待支付订单不显示支付卡片 |
| 支付流程 | 支付渠道选择、Loading、接口调用、成功失败反馈流程完整 |
| 列表分类更新 | 支付成功后订单能从待支付分类移动到待上菜分类 |
| 页面效果 | 页面布局整齐,文本、图片、按钮和卡片样式符合点餐 APP 页面风格 |
| 拓展任务 | 在基础功能之外补充更完善的异常处理、倒计时结束处理或交互优化 |