39 底部操作栏:ProductUtil 加购与购买

系列四:商品详情与列表 · 第 9 篇

对应工程:features/multishoppingproduct/src/main/ets/components/ProductUtil.ets

引言

底部操作栏是详情页的"转化出口":加购、购买、客服、购物袋,所有促成下单的按钮都集中在这里。ProductUtil 组件把这一排操作浓缩成一条 48vp 高的圆角胶囊,用 SymbolGlyph 系统图标 + 双按钮组合,按断点切换"通栏左对齐"与"悬浮靠右"两种形态,购买按钮则通过 @Event 上抛事件,由页面层用 bindSheet 拉起支付半屏。本文拆解它的全部细节。

操作栏组成:图标组与双按钮

ProductUtil 的 build 是一条 Row,依次是三个 SymbolGlyph 图标(首页/购物袋/客服)与两个按钮(加购/购买):

Row() {
  SymbolGlyph($r('sys.symbol.house'))      // 首页
    .fontSize(24)
    .fontColor([$r('app.color.text_primary')])
    .margin({ right: this.widthBp === WidthBreakpoint.WIDTH_SM ? 0 : 12 })
  SymbolGlyph($r('sys.symbol.bag'))        // 购物袋
    .fontSize(24)
    .fontColor([$r('app.color.text_primary')])
    .margin({ right: this.widthBp === WidthBreakpoint.WIDTH_SM ? 0 : 12 })
  SymbolGlyph($r('sys.symbol.message'))    // 客服
    .fontSize(24)
    .fontColor([$r('app.color.text_primary')])
    .margin({ right: this.widthBp === WidthBreakpoint.WIDTH_SM ? 0 : 12 })

  if (this.widthBp !== WidthBreakpoint.WIDTH_SM) {
    Blank()
      .layoutWeight(1)
      .displayPriority(1)
  }

  Button(this.labelState.addCartLabel)     // 加购
    .fontSize(16)
    .fontColor('#CF0A2C')
    .height(40)
    .borderRadius(20)
    .backgroundColor($r('app.color.buy_outline_background'))
    .backgroundBlurStyle(BlurStyle.Thin, { colorMode: ThemeColorMode.LIGHT, scale: 1.0 })
    .width(new WidthBreakpointType(128, 188, 140, 140).getValue(this.widthBp))

  Button(this.labelState.buyLabel)         // 购买
    .fontSize(16)
    .fontColor($r('sys.color.font_on_primary'))
    .height(40)
    .width(new WidthBreakpointType(64, 188, 140, 140).getValue(this.widthBp))
    .borderRadius(20)
    .backgroundColor('#CF0A2C')
    .onClick(() => {
      if (this.isLivePage || this.isSplitMode) {
        return;
      }
      this.onBuyClick();
    })
}
.layoutWeight(1)
.justifyContent(this.widthBp !== WidthBreakpoint.WIDTH_SM ? FlexAlign.Start : FlexAlign.SpaceBetween)
.backgroundBlurStyle(BlurStyle.Thin, { colorMode: ThemeColorMode.SYSTEM, adaptiveColor: AdaptiveColor.DEFAULT, scale: 1.0 })
.height(48)
.borderRadius(24)
.width('100%')

设计要点:

  • SymbolGlyph 系统符号:用 $r('sys.symbol.house/bag/message') 直接引用系统 SF 风格符号,替代本地 SVG 图标,天然适配多尺寸与深浅色,且零资源体积。
  • Blank + displayPriority:非窄屏下插入 Blank() 占位,把按钮组推到最右;displayPriority(1) 保证空间不足时 Blank 优先被压缩而不是按钮。窄屏下 justifyContent(SpaceBetween) 让图标与按钮两端分布,充分利用宽度。
  • 双按钮胶囊:加购为描边风格(buy_outline_background + 红色文字),购买为实心红(#CF0A2C + 白色文字),主次分明;两按钮宽度随断点变化(窄屏 128/64,中屏 188/188,宽屏 140/140)。
  • backgroundBlurStyle 毛玻璃:整条操作栏叠加 BlurStyle.Thin 系统模糊,滚动时内容从毛玻璃下透出,视觉更高级;ThemeColorMode.SYSTEM 让模糊底色跟随系统深浅色。

断点驱动的布局差异

ProductUtil 接收父容器传入的 @Param widthBp,内部按断点做三处差异化:

  • 图标间距:窄屏(SM)图标间距 0,宽屏 12vp——窄屏靠 SpaceBetween 拉伸,宽屏靠显式 margin。
  • 按钮宽度WidthBreakpointType(128, 188, 140, 140),中屏(MD)两按钮都加宽到 188vp 适配折叠屏展开态。
  • 整体对齐justifyContent 在窄屏 SpaceBetween、宽屏 Start,配合父容器 alignRules(窄屏通栏、宽屏悬浮右栏)完成形态切换。

点击购买拉起 PayCard 半屏

购买按钮不直接弹窗,而是通过 @Event onBuyClick 上抛意图。ProductUtil@Event 声明与守卫逻辑:

@Event private onBuyClick: () => void = () => {
};
// ...
Button(this.labelState.buyLabel)
  .onClick(() => {
    if (this.isLivePage || this.isSplitMode) {
      return;   // 直播页/分屏下禁用购买跳转
    }
    this.onBuyClick();
  })

页面层 ProductDetailonBuyClick 回调里把 detailType 置为 'detailPayCard'isDialogOpentrue,从而触发 bindSheet 打开 PayCard 半屏弹窗(620vp 高、居中、不可拖拽、点外不穿透)。PayCard 是支付模块 multishoppingpay 的组件,内部包含收货地址、规格选择、支付方式与支付按钮,属于"支付领域"的独立模块——操作栏与支付模块之间只隔一个 @Event,零 import 依赖。

需要说明的是,本项目的加购按钮与购物袋图标在演示工程中未绑定完整业务(购物袋完整逻辑见系列六 ShopBag/ShopList),但结构上它们都指向同一个目标:把用户操作转成状态变更,由页面层决定下一步动作。购物袋入口接入时,只需在 bag 图标的 onClick 里上抛另一个 @Event onBagClick,由页面层拉起 detailType = 'ShopBag' 的弹窗即可。

安全区与多端适配

操作栏的安全区适配由父容器 ProductDetailContent 完成:RelativeContaineralignRules 让操作栏永远锚定容器底部,配合 expandSafeArea 沉浸式策略,系统手势区不会遮挡按钮;窄屏下宽度 calc(100% - 32vp) 自带左右 16vp 留白,视觉上"悬浮贴底",与 iPhone 风格 Home Indicator 类似但完全由 ArkUI 原生实现。

注意事项与总结

  • @Event 上抛而非直接改父状态ProductUtil 不持有 isDialogOpen,避免组件与页面状态强耦合;复用操作栏到直播页时只需换一个 onBuyClick 实现。
  • 守卫条件isLivePage || isSplitMode 时禁用购买,避免在直播/分屏场景误触拉起支付弹窗;接入真实业务时建议把守卫逻辑下沉到页面层,操作栏保持"纯展示 + 上抛"。
  • displayPriority 善用:悬浮栏空间紧张时优先压缩 Blank 而非图标,保证核心按钮永远可见。
  • 毛玻璃开销backgroundBlurStyle 有 GPU 成本,滚动频繁的页面建议评估性能;必要时降级为半透明纯色背景。
ProductUtil 用 110 行实现了"图标入口 + 双按钮 + 断点形态 + 事件上抛"的完整操作栏,是 @Event 组件通信模式的最佳示范。系列四的收官篇,我们把镜头从详情页切回列表页,看入口 MobilePhoneList 如何用栅格与懒加载撑起商品列表。
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐