首先先看一下效果

然后下面是代码封装的逻辑

export interface PopupItem {
  code: number | string
  name: string
}

@CustomDialog
export struct GlobalPopup {
  controller: CustomDialogController
  @State selectedItem?: PopupItem | null = null
  private items?: PopupItem[] = []
  private title: string = '选择'
  private onConfirm?: (selectedItem: PopupItem) => void
  private onCancel?: () => void

  build() {
    Row() {
      Column() {
        // 标题
        Column() {
          Row() {
            Text(this.title)
              .fontWeight(FontWeight.Bold)
              .fontColor('#EE9723')
          }
          .alignItems(VerticalAlign.Center)
          .justifyContent(FlexAlign.Center)
          .height('60vp')
        }
        .width('100%')
        .height('60vp')
        .linearGradient({
          colors: [['#FAE0BD', 0], ['#FFFFFF', 1]],
          angle: 180
        })
        .borderRadius({ topLeft: 16, topRight: 16 })

        // 内容滚动列表 - 自适应高度但有最大限制
        Scroll(){
          Column(){
            ForEach(this.items, (item: PopupItem, index: number) => {
              Row(){
                Text(item.name)
                  .fontColor(item.code == this.selectedItem?.code ? '#FFFFFF' : $r('app.color.black_color'))
                  .fontSize(14)
                if (item.code == this.selectedItem?.code) {
                  Image($r('app.media.check'))
                    .width(24)
                    .height(24)
                }
              }
              .justifyContent(FlexAlign.SpaceBetween)
              .alignItems(VerticalAlign.Center)
              .width('100%')
              .padding({
                left: 10,
                right: 10
              })
              .height(40)
              .margin({
                bottom: 10
              })
              .borderRadius(6)
              .backgroundColor(item.code == this.selectedItem?.code ? '#EE9723' : $r('app.color.gray_color'))
              .onClick(() => {
                this.selectedItem = {
                  code: item.code,
                  name: item.name
                }
              })
            })
          }
          .padding({
            bottom: 20
          })
          .margin({
            left: 16,
            right: 16,
            bottom: 20
          })
          .backgroundColor($r('app.color.white_color'))
        }
        .width('100%')
        // 关键修改:使用constraintSize限制最大高度,让内容自适应
        .constraintSize({
          maxHeight: '50%'  // 设置滚动区域最大高度为屏幕的50%
        })

        // 底部操作按钮 - 固定高度
        Row() {
          Button('取消')
            .fontSize(14)
            .type(ButtonType.Normal)
            .fontColor('#666')
            .backgroundColor('#F5F5F5')
            .borderRadius(4)
            .width('45%')
            .height(40)
            .onClick(() => {
              this.onCancel?.()
              this.controller.close()
            })

          Button('确定')
            .fontSize(14)
            .type(ButtonType.Normal)
            .fontColor('#FFFFFF')
            .backgroundColor('#EE9723')
            .borderRadius(4)
            .width('45%')
            .height(40)
            .onClick(() => {
              if (this.selectedItem) {
                this.onConfirm?.(this.selectedItem)
              }
              this.controller.close()
            })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .padding({
          top: 16,
          bottom: 26,
          left: 16,
          right: 16
        })
        .backgroundColor('#FFFFFF')
        .shadow({
          radius: 8,
          color: 'rgba(0,0,0,0.1)',
          offsetX: 0,
          offsetY: -2
        })
      }
      .width('100%')
      // 关键修改:移除固定高度,使用constraintSize限制最大高度
      .constraintSize({
        maxHeight: '70%'  // 整个弹窗最大高度为屏幕的70%
      })
      .backgroundColor($r('app.color.white_color'))
      .borderRadius(16)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.1)')
    .justifyContent(FlexAlign.End)
    .alignItems(VerticalAlign.Bottom)
  }
}

定义了一个通用的全局弹窗组件 GlobalPopup,它基于装饰器 @CustomDialog 创建,目的是提供一个可以在应用中随时调用的选择弹窗。整个组件的功能围绕“展示一组选项,并让用户选择其中一项,最后通过确定或取消按钮返回结果”展开。

首先,代码定义了一个接口 PopupItem,它包含 codename 两个字段,用来描述每个选项的唯一标识和显示名称。这为组件提供了标准化的数据结构,使调用方可以传入任意的数据集,只要符合该接口,就能在弹窗中展示。

GlobalPopup 结构体中,内部维护了几个关键状态和属性:

  • controller:弹窗的控制器,负责打开和关闭弹窗。

  • selectedItem:当前用户选择的条目,使用 @State 修饰,确保其变更后 UI 会自动刷新。

  • items:候选项列表,默认为空数组。

  • title:弹窗标题,默认值为“选择”。

  • onConfirmonCancel:回调函数,用来在点击按钮时通知外部调用者用户的操作结果。

build() 方法中,整个 UI 布局通过声明式方式构建:

  1. 标题区域
    使用 RowColumn 组合,标题文字居中显示,颜色为 #EE9723,并在背景上应用了一个自上而下的渐变色(由浅米色到白色)。标题区域还设置了顶部圆角,既美观又与弹窗整体样式保持一致。

  2. 内容滚动区
    使用 Scroll 包裹一个 Column,内部通过 ForEach 遍历 items 数组,渲染每一个选项。每个选项用 Row 显示,左边是文字,右边在被选中时会显示一个勾选图标。

    • 选中项的背景色为橙色(#EE9723),文字为白色;未选中项则是灰色背景,黑色文字。

    • 点击某个选项时,会更新 selectedItem 的值。
      此部分特别设置了 .constraintSize({ maxHeight: '50%' }),确保列表不会无限撑大,而是最多占据屏幕一半的高度,避免内容过多时影响体验。

  3. 底部操作按钮
    固定高度的 Row,左右分别放置“取消”和“确定”按钮。

    • “取消”按钮是浅灰色,点击后触发 onCancel 回调并关闭弹窗。

    • “确定”按钮为主题橙色,点击时若有选择项,则调用 onConfirm 回调传出该项,然后关闭弹窗。
      按钮区域还设置了阴影和内边距,让视觉层级更加突出。

  4. 整体弹窗容器
    外层 Row 将内容吸附到底部(justifyContent(FlexAlign.End)),并用半透明黑色背景模拟遮罩层,确保弹窗突出显示。整个容器还设置了最大高度为屏幕的 70%,这样即便内容很多,也不会超出合理范围。

从整体设计上看,这个组件的亮点在于:

  • 使用了 constraintSize 来控制高度自适应,既能在内容较少时紧凑显示,又能在内容过多时限制弹窗的尺寸,保证用户体验。

  • 样式方面统一使用橙色作为主题色,配合灰色与白色,既有层次感,又能清晰地引导用户注意力。

  • 逻辑上解耦良好:调用者只需要传入 itemsonConfirm/onCancel 回调,就能完成业务,不需要关心内部渲染细节。

综上,实现了一个高度通用且美观的全局选择弹窗,适合在各种需要用户选择的场景下复用,例如选择支付方式、地址、标签等。它不仅解决了基本的交互需求,还通过高度可配置和良好的用户体验设计,展现了组件化开发的优势。

使用示例

// 定义候选项
const items: PopupItem[] = [
  { code: 1, name: '支付宝' },
  { code: 2, name: '微信支付' },
  { code: 3, name: '银行卡' },
]

// 打开弹窗
CustomDialog.show<GlobalPopup>({
  builder: GlobalPopup,  // 指定弹窗组件
  params: {
    title: '请选择支付方式',    // 修改标题
    items: items,            // 传入选项
    onConfirm: (selectedItem: PopupItem) => {
      console.log('用户选择了:', selectedItem)
      // 这里可以执行后续逻辑,例如调用支付接口
    },
    onCancel: () => {
      console.log('用户点击了取消')
    }
  }
})

解释说明

  1. 准备数据
    首先构建一个 items 数组,每个元素符合 PopupItem 接口,包含 codename。这里以支付方式为例,列出了支付宝、微信支付和银行卡。

  2. 调用弹窗
    使用 CustomDialog.show<GlobalPopup>() 打开我们之前写的 GlobalPopup 弹窗组件。

  3. 参数传递

    • title:自定义弹窗标题。

    • items:传入候选项列表,组件会自动渲染成可选择的列表。

    • onConfirm:确定按钮的回调,能拿到用户选中的条目。

    • onCancel:取消按钮的回调。

  4. 效果
    打开后,弹窗会显示一个带标题和滚动列表的界面,用户可以点击选项高亮,最后点击“确定”时返回选中项。

https://developer.huawei.com/consumer/cn/training/classDetail/441e866c940048bdb61f08cbc6987967?type=1?ha_source=hmosclass&ha_sourceId=89000248

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐