跳转到内容

编程实验:开发订单详情页与支付功能

本实验将基于 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

entry/src/main/ets/order/list/OrderListComponent.ets
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' // 本节补充
@Component
export 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 绑定订单点击事件

在列表项中绑定点击事件,点击订单小项时进入详情页。

entry/src/main/ets/order/list/OrderListComponent.ets
@Builder
ListBuilder() {
List() {
LazyForEach(
this.orderDataSource,
(item: OrderData) => {
ListItem() {
OrderItemComponent({ orderData: item }) // 模板工程已有
.onClick(_ => { // 补充点击事件处理
this.gotoOrderDetail(item)
})
}
},
(item: OrderData) => JSON.stringify(item)
)
}
}

3.3 在订单详情页解析路由参数

OrderDetailView 进入页面时读取路由参数,并把 orderJson 还原成 OrderData

entry/src/main/ets/order/detail/OrderDetailView.ets
import router from '@ohos.router' // 本节补充
import { plainToClass } from 'class-transformer' // 本节补充
import { OrderData } from '../../model/OrderData' // 本节补充
@Component
export 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 承载订单详情内容。

entry/src/main/ets/order/detail/OrderDetailView.ets
import router from '@ohos.router' // 前序步骤已完成
import { plainToClass } from 'class-transformer' // 前序步骤已完成
import { TitleBarView } from '../../common/widget/TitleBarView' // 本节补充
import { OrderData } from '../../model/OrderData' // 前序步骤已完成
@Component
export 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,用于集中绘制订单时间线。

entry/src/main/ets/order/detail/OrderDetailView.ets
@Component
export 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 在页面中引入时间线

把时间线放在订单状态文本下方。

entry/src/main/ets/order/detail/OrderDetailView.ets
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,用于展示订单基础信息。

entry/src/main/ets/order/detail/OrderDetailView.ets
@Component
export 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

订单中的每个商品都需要展示图片、名称、单价、数量和小计金额。

entry/src/main/ets/order/detail/OrderDetailView.ets
import { GoodsData } from '../../model/GoodsData' // 本节补充
@Component
export 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 在页面中引入订单详情卡片

把订单详情卡片放在时间线下方。

entry/src/main/ets/order/detail/OrderDetailView.ets
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 接收订单数据,并绘制支付卡片。

entry/src/main/ets/order/payment/PaymentComponent.ets
import { LoadingDialog } from '../../common/dialog/LoadingDialog' // 模板工程已有
import { OrderData } from '../../model/OrderData' // 本节补充
@Component
export 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 实现支付倒计时

使用定时器每秒减少剩余秒数,并在组件销毁时清理定时器。

entry/src/main/ets/order/payment/PaymentComponent.ets
@Component
export 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 时显示。

entry/src/main/ets/order/detail/OrderDetailView.ets
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 中补充支付接口请求逻辑。

entry/src/main/ets/api/ServerApi.ets
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 中创建弹窗控制器。

entry/src/main/ets/order/payment/PaymentComponent.ets
import { LoadingDialog } from '../../common/dialog/LoadingDialog' // 模板工程已有
import { OrderData } from '../../model/OrderData' // 前序步骤已完成
import { PaymentDialog } from './PaymentDialog' // 本节补充
@Component
export 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 实现支付流程

支付确认后调用接口,并根据接口结果更新订单详情页状态。

entry/src/main/ets/order/payment/PaymentComponent.ets
import { ServerApi } from '../../api/ServerApi' // 本节补充
import promptAction from '@ohos.promptAction' // 本节补充
@Component
export 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 支付成功后发送订单支付事件。

entry/src/main/ets/order/payment/PaymentComponent.ets
import emitter from '@ohos.events.emitter' // 本节补充
import { Constants } from '../../utils/Constants' // 本节补充
@Component
export 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,调整三个数据源中的订单位置。

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() // 模板工程已有
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 订阅和取消订阅支付事件

在订单管理页进入时订阅支付事件,页面退出时取消订阅。

entry/src/main/ets/order/OrderManageView.ets
@Component
export 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 页面风格
拓展任务在基础功能之外补充更完善的异常处理、倒计时结束处理或交互优化