编程实验:开发订单详情页与支付功能
本实验将基于 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 从列表页传递到详情页。
3.1 定义跳转订单详情页方法
在 OrderListComponent 中新增 gotoOrderDetail 方法,将订单对象转成普通 JSON 数据后传入 pages/OrderDetail。
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' // 模板工程已有import promptAction from '@ohos.promptAction' // 模板工程已有import router from '@ohos.router' // 本节补充import { instanceToPlain } from 'class-transformer' // 本节补充
@Componentexport struct OrderListComponent { orderDataSource: BasicDataSource<OrderData> = new BasicDataSource() // 模板工程已有 refreshOrderListAsyncCallback: (() => Promise<void>) | null = null // 模板工程已有 @State pageStatus: PageStatus = PageStatus.Empty // 模板工程已有 @State isRefreshing: boolean = false // 模板工程已有
gotoOrderDetail(order: OrderData) { router.pushUrl({ url: 'pages/OrderDetail', params: { orderJson: JSON.stringify(instanceToPlain(order)) } }) }}3.2 绑定订单点击事件
在列表项中绑定点击事件,点击订单小项时进入详情页。
@BuilderListBuilder() { List() { LazyForEach( this.orderDataSource, (item: OrderData) => { ListItem() { OrderItemComponent({ orderData: item }) // 模板工程已有 .onClick(_ => { // 补充点击事件处理 this.gotoOrderDetail(item) }) } }, (item: OrderData) => JSON.stringify(item) ) }}3.3 在订单详情页解析路由参数
OrderDetailView 进入页面时读取路由参数,并把 orderJson 还原成 OrderData。
import router from '@ohos.router' // 本节补充import { plainToClass } from 'class-transformer' // 本节补充import { OrderData } from '../../model/OrderData' // 本节补充
@Componentexport struct OrderDetailView { @State orderData: OrderData = new OrderData() // 本节补充
aboutToAppear() { const params = router.getParams() as Record<string, Object> if (params == undefined || params.orderJson == undefined) { return } this.orderData = plainToClass(OrderData, JSON.parse(params.orderJson as string)) // 使用日志确认是否已经获取到订单数据 console.log(`获取到的订单号为:${this.orderData.sn}`); }
build() { Column() { // 模板工程已有的空页面骨架 } .backgroundColor($r('app.color.black_5')) .size({ width: '100%', height: '100%' }) }}✅ 预期效果:点击订单列表中的任意订单,可以进入订单详情页;在详情页中可以通过临时文本或日志确认已拿到该订单数据。
目标:订单详情页需要让用户立即判断订单处于待支付、待上菜还是已完成状态。本步骤完成页面骨架、订单状态标题和三段式时间线。
4.1 搭建订单详情页骨架
在页面顶部加入标题栏,主体区域使用 Scroll 承载订单详情内容。
import router from '@ohos.router' // 前序步骤已完成import { plainToClass } from 'class-transformer' // 前序步骤已完成import { TitleBarView } from '../../common/widget/TitleBarView' // 本节补充import { OrderData } from '../../model/OrderData' // 前序步骤已完成
@Componentexport struct OrderDetailView { @State orderData: OrderData = new OrderData() // 前序步骤已完成 aboutToAppear() { ... } // 前序步骤已完成
build() { Column() { TitleBarView({ // 本节补充 title: $r('app.string.title_order_detail'), backEnabled: true }) Scroll() { Column() { 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%' }) }}4.2 定义时间线 Builder
在 OrderDetailView 中新增 TimelineBuilder,用于集中绘制订单时间线。
@Componentexport struct OrderDetailView { @State orderData: OrderData = new OrderData() // 前序步骤已完成 aboutToAppear() { ... } // 前序步骤已完成 build() { ... } // 前序步骤已完成
@Builder TimelineBuilder() { Column() { Row() { Text('下单时间:\n' + this.orderData.getCreateTime()) .fontSize(15) .fontColor(Color.Black) Blank() .layoutWeight(1) Text('上菜时间:\n' + (this.orderData.deliverStatus == 0 ? '-' : this.orderData.getDeliverTime())) .fontSize(15) .fontColor(this.orderData.deliverStatus == 0 ? $r('app.color.black_20') : Color.Black) } .padding({ left: 20, right: 20 }) .width('100%')
Row() { Circle({ width: 10, height: 10 }) .fill($r('app.color.black')) Divider() .strokeWidth(1) .color(this.orderData.payStatus == 0 ? $r('app.color.black_20') : Color.Black) .layoutWeight(1) Circle({ width: 10, height: 10 }) .fill(this.orderData.payStatus == 0 ? $r('app.color.black_20') : Color.Black) Divider() .strokeWidth(1) .color(this.orderData.deliverStatus == 0 ? $r('app.color.black_20') : Color.Black) .layoutWeight(1) Circle({ width: 10, height: 10 }) .fill(this.orderData.deliverStatus == 0 ? $r('app.color.black_20') : Color.Black) } .margin({ top: 10 }) .padding({ left: 25, right: 25 }) .width('100%')
Row() { Text('支付时间:\n' + (this.orderData.payStatus == 0 ? '-' : this.orderData.getPayTime())) .fontSize(15) .fontColor(this.orderData.payStatus == 0 ? $r('app.color.black_20') : Color.Black) } .justifyContent(FlexAlign.Center) .margin({ top: 10 }) .padding({ left: 20, right: 20 }) .width('100%') } .margin({ top: 30 }) .width('100%') }}4.3 在页面中引入时间线
把时间线放在订单状态文本下方。
Scroll() { Column() { Text(this.orderData.getStatusDesc()) // 前序步骤已完成 .fontColor(Color.Black) .fontSize(32) .margin({ left: 20, top: 30 })
this.TimelineBuilder() // 本节补充 } .alignItems(HorizontalAlign.Start)}✅ 预期效果:订单详情页显示订单状态标题和时间线;未支付订单的支付时间、上菜时间显示为 -,相关节点为浅灰色。
目标:订单详情页需要展示订单的完整业务信息。本步骤完成订单号、商铺、商品明细和订单总价的展示。
5.1 定义订单详情卡片
在 OrderDetailView 中新增 OrderInfoBuilder,用于展示订单基础信息。
@Componentexport struct OrderDetailView { @State orderData: OrderData = new OrderData() // 前序步骤已完成 TimelineBuilder() { ... } // 前序步骤已完成
@Builder OrderInfoBuilder() { Column() { Text('订单号: ' + this.orderData.sn) .fontSize(20) .fontColor($r('app.color.black_70'))
Row() { Text('商铺: ') .fontSize(20) .fontColor($r('app.color.black_70')) Image(this.orderData.storeImageUrl) .size({ width: 20, height: 20 }) .borderRadius(4) .margin({ left: 10 }) Text(this.orderData.storeName) .fontSize(20) .fontColor($r('app.color.black_60')) .margin({ left: 5 }) } .margin({ top: 10 })
Text('订单商品') .margin({ top: 25, bottom: 10 })
ForEach( this.orderData.goodsList, (goods: GoodsData) => { this.GoodsItemBuilder(goods) }, (goods: GoodsData) => JSON.stringify(goods) )
Text('合计:¥' + this.orderData.priceAll) .fontColor($r('app.color.black')) .fontSize(20) .alignSelf(ItemAlign.End) .margin({ top: 15 }) } .alignItems(HorizontalAlign.Start) .width('100%') .borderRadius(7) .backgroundColor(Color.White) .padding({ left: 20, right: 20, top: 20, bottom: 20 }) }}5.2 定义商品小项 Builder
订单中的每个商品都需要展示图片、名称、单价、数量和小计金额。
import { GoodsData } from '../../model/GoodsData' // 本节补充
@Componentexport struct OrderDetailView { OrderInfoBuilder() { ... } // 前序步骤已完成
@Builder GoodsItemBuilder(goods: GoodsData) { Column() { Row() { Image(goods.imageUrl) .size({ width: 70, height: 70 }) .borderRadius(8)
Column() { Text(goods.name) .fontColor(Color.Black) .fontSize(21) Text('单价:' + goods.price) .fontColor($r('app.color.black_60')) .fontSize(17) .margin({ top: 6 }) Text('数量:' + goods.number) .fontColor($r('app.color.black_60')) .fontSize(17) .margin({ top: 6 }) } .alignItems(HorizontalAlign.Start) .margin({ left: 10 }) .layoutWeight(1)
Text('¥' + goods.countPrice) .fontColor($r('app.color.primary_color_light')) .fontSize(20) } .alignItems(VerticalAlign.Top) Divider() .height(0.5) .width('100%') .color($r('app.color.black_10')) .margin({ top: 8, bottom: 8 }) } }}5.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 }) }}✅ 预期效果:订单详情页可以完整展示订单号、商铺图片、商铺名称、商品列表、商品数量、商品小计和订单合计金额。
目标:待支付订单需要提供明确的支付入口。本步骤开发支付卡片,并只在订单未支付时显示。
6.1 搭建待支付组件 UI
在 PaymentComponent 中通过 @Link 接收订单数据,并绘制支付卡片。
import { LoadingDialog } from '../../common/dialog/LoadingDialog' // 模板工程已有import { OrderData } from '../../model/OrderData' // 本节补充
@Componentexport struct PaymentComponent { @Link orderData: OrderData // 本节补充
private loadingDialogController = new CustomDialogController({ // 模板工程已有 builder: LoadingDialog({ message: $r('app.string.message_paying_order') }), autoCancel: false, customStyle: true, })
choosePayChannel() { // 本节补充 // 后续步骤中实现 }
async payOrder() { // 本节补充 // 后续步骤中实现 }
build() { Column() { // 本节补充 Text('订单总价:¥' + this.orderData.priceAll) .fontSize(24) .fontColor($r('app.color.black_90'))
Row() { Text('剩余支付时间(仅演示):') .fontColor($r('app.color.black_60')) .fontSize(18) Text(this.formattedTime()) .fontColor($r('app.color.black_60')) .fontSize(18) .margin({ left: 6 }) } .margin({ top: 15 })
Button($r('app.string.label_go_payment')) .type(ButtonType.Normal) .fontSize(20) .fontColor(Color.White) .width('100%') .borderRadius(2) .margin({ top: 15 }) .backgroundColor($r('app.color.primary_color')) .onClick(_ => { this.choosePayChannel() }) } .alignItems(HorizontalAlign.Start) .width('100%') .borderRadius(7) .backgroundColor(Color.White) .padding({ left: 20, right: 20, top: 20, bottom: 20 }) }}6.2 实现支付倒计时
使用定时器每秒减少剩余秒数,并在组件销毁时清理定时器。
@Componentexport struct PaymentComponent { @Link orderData: OrderData // 前序步骤已完成 @State remainSeconds: number = 60 * 15 // 本节补充 intervalId: number = 0 // 本节补充
aboutToAppear() { this.intervalId = setInterval(() => { if (this.remainSeconds <= 0) { return } this.remainSeconds-- }, 1000) }
aboutToDisappear() { if (this.intervalId != 0) { clearInterval(this.intervalId) } }
formattedTime(): string { const minutes = Math.floor(this.remainSeconds / 60) const seconds = this.remainSeconds % 60 return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}` }
build() { ... } // 前序步骤已完成}6.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 等待和成功失败反馈。
7.1 对接订单支付接口
在 ServerApi 中补充支付接口请求逻辑。
export class ServerApi { private static async execute(...) { ... } // 模板工程与前序步骤已完成
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 } const rsp = await ServerApi.execute(request, `${Constants.SERVER_HOST}/order-record/pay`, options) return plainToClassFromExist(new Response<Object>(Object), rsp.result) }}7.2 引入支付渠道弹窗
模板工程已提供 PaymentDialog,在 PaymentComponent 中创建弹窗控制器。
import { LoadingDialog } from '../../common/dialog/LoadingDialog' // 模板工程已有import { OrderData } from '../../model/OrderData' // 前序步骤已完成import { PaymentDialog } from './PaymentDialog' // 本节补充
@Componentexport struct PaymentComponent { @Link orderData: OrderData // 前序步骤已完成 private loadingDialogController = new CustomDialogController({ ... }) // 模板工程已有
private payChannelDialogController = new CustomDialogController({ builder: PaymentDialog({ onConfirmListener: () => { this.payOrder() } }), autoCancel: false, gridCount: 6, })
choosePayChannel() { this.payChannelDialogController.open() }}7.3 实现支付流程
支付确认后调用接口,并根据接口结果更新订单详情页状态。
import { ServerApi } from '../../api/ServerApi' // 本节补充import promptAction from '@ohos.promptAction' // 本节补充
@Componentexport struct PaymentComponent { @Link orderData: OrderData // 前序步骤已完成 private loadingDialogController = new CustomDialogController({ ... }) // 模板工程已有 private payChannelDialogController = new CustomDialogController({ ... }) // 前序步骤已完成
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 } this.orderData.payStatus = 1 this.orderData.payTime = Math.floor(Date.now() / 1000) promptAction.showToast({ message: $r('app.string.message_pay_order_success') }) this.loadingDialogController.close() }}✅ 预期效果:点击“去支付”后弹出支付方式选择弹窗;确认支付后显示 Loading;支付成功后详情页状态变为待上菜,支付时间显示为当前时间。
目标:支付成功后,订单详情页状态已经更新,但订单管理页中的分类数据源也需要同步调整。本步骤通过事件通知订单管理页更新分类列表。
8.1 支付成功后发送事件
在 PaymentComponent 支付成功后发送订单支付事件。
import emitter from '@ohos.events.emitter' // 本节补充import { Constants } from '../../utils/Constants' // 本节补充
@Componentexport struct PaymentComponent { @Link orderData: OrderData // 前序步骤已完成
async payOrder() { ... // 前序步骤已完成 promptAction.showToast({ message: $r('app.string.message_pay_order_success') }) // 前序步骤已完成 this.loadingDialogController.close() // 前序步骤已完成
emitter.emit(Constants.EVENT_ORDER_PAYED, { // 本节补充 data: { orderId: this.orderData.id } }) }}8.2 编写订单支付事件处理函数
在 OrderManageView 中根据支付完成的订单 id,调整三个数据源中的订单位置。
@Componentexport struct OrderManageView { orderAllDataSource: BasicDataSource<OrderData> = new BasicDataSource() // 模板工程已有 orderWaitPayDataSource: BasicDataSource<OrderData> = new BasicDataSource() // 模板工程已有 orderWaitDeliverDataSource: BasicDataSource<OrderData> = new BasicDataSource() // 模板工程已有
orderPayedHandler(eventData: emitter.EventData) { const orderId = eventData.data!.orderId as number if (orderId == undefined || orderId == null) { return }
const allOrderIndex = this.orderAllDataSource.findIndex((orderData: OrderData) => orderData.id == orderId) if (allOrderIndex == -1) { return }
const orderData = this.orderAllDataSource.getData(allOrderIndex) orderData.payStatus = 1 orderData.payTime = Math.floor(Date.now() / 1000) this.orderAllDataSource.updateData(allOrderIndex, orderData)
const waitPayOrderIndex = this.orderWaitPayDataSource.findIndex((orderData: OrderData) => orderData.id == orderId) if (waitPayOrderIndex != -1) { this.orderWaitPayDataSource.removeData(waitPayOrderIndex) }
this.orderWaitDeliverDataSource.addData(0, orderData) }}8.3 订阅和取消订阅支付事件
在订单管理页进入时订阅支付事件,页面退出时取消订阅。
@Componentexport struct OrderManageView { orderCreateHandler(eventData: emitter.EventData) { ... } // 前序步骤已完成
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 订单详情页与支付功能开发,并提交最终演示录屏。
9.1 验收清单
- 可以从订单列表页点击任意订单进入订单详情页
- 订单详情页能展示订单状态、时间线、订单号、商铺信息、商品明细和订单合计金额
- 待支付订单能显示支付卡片、倒计时和“去支付”按钮
- 点击“去支付”后能选择微信支付、支付宝支付或银联支付
- 确认支付后能显示 Loading,支付成功后更新详情页订单状态和支付时间
- 返回订单管理页后,支付成功的订单能自动从“待支付”移动到“待上菜”
9.2 提交要求
- 提交最终演示录屏,视频画面需清晰
- 录屏需覆盖创建订单、进入订单详情、选择支付方式、完成支付、返回订单管理页查看分类更新的完整流程
9.3 评分标准
| 评分项 | 要求 |
|---|---|
| 订单详情页数据展示 | 订单状态、时间线、订单号、商铺信息、商品明细和合计金额展示完整 |
| 支付组件开发 | 待支付订单能显示支付卡片、倒计时和支付入口,非待支付订单不显示支付卡片 |
| 支付流程 | 支付渠道选择、Loading、接口调用、成功失败反馈流程完整 |
| 列表分类更新 | 支付成功后订单能从待支付分类移动到待上菜分类 |
| 页面效果 | 页面布局整齐,文本、图片、按钮和卡片样式符合点餐 APP 页面风格 |
| 拓展任务 | 在基础功能之外补充更完善的异常处理、倒计时结束处理或交互优化 |