跳转到内容

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

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

entry/src/main/ets/order/list/OrderListComponent.ets
import router from '@ohos.router'
import { instanceToPlain } from 'class-transformer'
@Component
export struct OrderListComponent {
...
gotoOrderDetail(order: OrderData) {
// TODO: 调用 router.pushUrl 跳转到 pages/OrderDetail
// TODO: 在 params 中传入 orderJson
// · 使用 instanceToPlain(order) 去掉 OrderData 的类实例包装
// · 使用 JSON.stringify(...) 转成字符串,便于路由参数传递
}
}

1.2 绑定订单点击事件

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

entry/src/main/ets/order/list/OrderListComponent.ets
@Builder
ListBuilder() {
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

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 {
// 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 承载订单详情内容。

entry/src/main/ets/order/detail/OrderDetailView.ets
import { TitleBarView } from '../../common/widget/TitleBarView'
@Component
export 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,用于集中绘制订单时间线。

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

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

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)
}

✅ 预期效果:订单详情页显示订单状态标题和时间线;未支付订单的支付时间、上菜时间显示为 -,相关节点为浅灰色。

待支付订单时间线
已支付订单时间线

目标:订单详情页需要展示订单的完整业务信息。本步骤完成订单号、商铺、商品明细和订单总价的展示。

3.1 定义订单详情卡片

OrderDetailView 中新增 OrderInfoBuilder,用于展示订单基础信息。

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

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

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

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

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 })
}
}

✅ 预期效果:订单详情页可以完整展示订单号、商铺图片、商铺名称、商品列表、商品数量、商品小计和订单合计金额。

订单详情信息卡片

目标:待支付订单需要提供明确的支付入口。本步骤开发支付卡片,并只在订单未支付时显示。

4.1 搭建待支付组件 UI

PaymentComponent 中通过 @Link 接收订单数据,并绘制支付卡片。

entry/src/main/ets/order/payment/PaymentComponent.ets
import { OrderData } from '../../model/OrderData'
@Component
export 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 实现支付倒计时

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

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

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 等待和成功失败反馈。

5.1 对接订单支付接口

ServerApi 中补充支付接口请求逻辑。

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

entry/src/main/ets/order/payment/PaymentComponent.ets
import { PaymentDialog } from './PaymentDialog'
@Component
export struct PaymentComponent {
...
private payChannelDialogController = new CustomDialogController({
builder: PaymentDialog({
// TODO: 在 onConfirmListener 中调用 this.payOrder()
}),
// TODO: 设置 autoCancel: false,避免点击弹窗外部自动关闭
// TODO: 设置 gridCount: 6,控制弹窗宽度
})
choosePayChannel() {
// TODO: 调用 payChannelDialogController.open() 打开支付渠道选择弹窗
}
}

5.3 实现支付流程

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

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

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

entry/src/main/ets/order/OrderManageView.ets
@Component
export 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 订阅和取消订阅支付事件

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

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