1、前言

🎉 新作已上架 —— 「古诗学习宝」鸿蒙原生应用已在华为应用市场上架,搜索「古诗学习宝」即可下载体验。零广告 / 零内购 / 277 首小学必背古诗全收录,烦请帮忙点个五星 🌟。

写鸿蒙应用做到中后期,每个开发者都会撞上「弹窗选型纠结症」:

  • 用户点收藏,要不要弹个 Toast?还是 Popup?
  • 选头像要弹底部 Sheet 还是中央 Dialog?
  • 清空记录的二次确认弹啥?
  • 庆祝打卡成功用什么形态?2 秒自动关掉的 Dialog?
  • 选年级选朝代用 Picker 还是 ActionMenu?

HarmonyOS 6 提供了七大类弹窗 API,每一类都对应一种用户心智。本文用「古诗学习宝」线上版本里真实在用的 5 类弹窗实例,加 2 类应该用的扩展场景,给你一棵完整的弹窗选型决策树 —— 看完再也不会站在那里纠结:“这里到底该弹哪种?”。

七大类弹窗:

# 类型 API 使用场景
1 Toast promptAction.showToast 轻量提示、操作结果反馈
2 Popup .bindPopup 气泡式提示、定位某控件
3 Menu .bindMenu / .bindContextMenu 长按 / 右键菜单
4 Dialog 固定样式 showAlertDialog / showDialog / showActionSheet / showActionMenu 二次确认、列表选择
5 CustomDialog openCustomDialog + ComponentContent + wrapBuilder 自定义 UI 弹窗(庆祝、自定义表单)
6 PickerDialog showDatePickerDialog / showTextPickerDialog / CalendarPickerDialog 日期 / 选项 / 级联选择
7 模态页面 .bindSheet / .bindContentCover 半模态上拉面板 / 全屏覆盖
8 OverlayManager getUIContext().getOverlayManager() 全局浮层(吊球、悬浮入口)

2、整体架构

2.1 选型决策树

                       要弹窗了?
                          │
              ┌───────────┴───────────┐
              │                       │
        【纯反馈/无交互】          【需要用户输入/确认】
              │                       │
              ▼                       ▼
        Toast (1)            ┌─────────────────────┐
                             │   是否覆盖整页?     │
                             └──┬──────────────────┘
                                │
                ┌───────────────┴──────────────┐
                │                              │
            【不覆盖】                       【覆盖】
                │                              │
                ▼                              ▼
        ┌──────────────┐              【是模态级体验吗?】
        │  位置在哪里? │                   │
        └──┬──────────┘             ┌──────┴──────┐
           │                        │             │
       【定位控件】              【是】          【否】
           │                        │             │
           ▼                        ▼             ▼
     Popup (2)               全屏交互           CustomDialog (5)
       Menu (3)              bindContentCover (7) 或 PickerDialog (6)
                             或 bindSheet (7)

      【全局浮动】── OverlayManager (8) 吊球

简记口诀

  • 轻量提示 → Toast
  • 指向控件 → Popup / Menu
  • 二次确认 → showDialog
  • 列表选项 → showActionMenu / showActionSheet
  • 自定义 UI → openCustomDialog
  • 选项选择 → showTextPickerDialog / CalendarPickerDialog
  • 半模态 → bindSheet
  • 全屏覆盖 → bindContentCover
  • 全局吊球 → OverlayManager

2.2 项目中的实例分布

弹窗类型 wqsy 用在哪 文件
Toast 操作反馈(已收藏 / 已掌握 / 已刷新 / 已清空 / 错题移除) 散落各 view
showDialog 关于 / 清空反馈历史确认 ProfileView / FeedbackPage
showActionMenu 每日学习目标 5 档选择 ProfileView
openCustomDialog 庆祝弹窗(打卡成功 / 成就解锁) DialogService + CelebrateDialog
自定义 BottomSheet 头像选择 + 卡通 4 选 ProfileView.AvatarSheet

3、效果展示

3.1 ActionMenu - 每日学习目标 5 档

07_goal_picker.jpeg外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

点「每日学习目标」菜单 → 弹起 ActionMenu 系统弹窗:1首/2首/3首推荐/4首/5首挑战——不同 button 用不同 color 字段表达"推荐 vs 挑战"产品意图。这是 7 类弹窗里**最适合做"5 个以内选项 + 单选"**的形态。

3.2 自定义 BottomSheet - 头像选择

06_avatar_sheet.jpeg

「更换头像」从底部上拉的浮层:3 个动作(拍照 / 相册 / 清除)+ 4 个卡通预设(李白 / 杜甫 / 王维 / 苏轼水墨头像)+ 取消按钮。这是自己用 @Local showAvatarSheet: boolean + 半透明遮罩 + 底部圆角面板实现,不走系统 API——好处是样式 100% 可控。

3.3 CustomDialog - 庆祝打卡(设计示意)

┌─────────────────────────────────────┐
│                                     │
│         ●●●●●●●●●●●●               │  ← 光晕圈
│        ● 🎉 ●●●●●●●●               │
│         ●●●●●●●●●●●●               │
│                                     │
│      连续打卡 5 天达成!              │
│      你又向「秀才」迈进一步 ✨        │
│                                     │
│       ✨ 连续 5 天 ✨                │  ← 徽标
│                                     │
│       点击任意处关闭                  │
└─────────────────────────────────────┘
        ↑ 2.2 秒后自动关闭

DialogService.showCelebrate(ui, data, 2200) 一行代码弹起庆祝弹窗,2.2 秒自动关闭。

3.4 showDialog - 关于 App

点「关于」菜单:

┌──────────────────────┐
│   关于 古诗学习宝     │
│  ─────────────────   │
│  一款给小学生的       │
│  古诗词学习陪伴应用    │
│                      │
│  版本:v1.0          │
│  用心打造每一首诗的    │
│  温度。              │
│                      │
│        [好的]         │
└──────────────────────┘

3.5 Toast - 反馈

   ┌────────────────────┐
   │  ✓ 已收藏           │
   └────────────────────┘

短暂出现 1.5 秒,自动消失。每个 toast 都不打断用户操作。


4、核心功能详解

4.1 第 1 类:Toast 轻量反馈

最简单也最高频。所有「操作完成、不需要用户确认」的反馈都用 Toast:

import { promptAction } from '@kit.ArkUI';

// 收藏成功
promptAction.showToast({
  message: '已收藏 ⭐',
  duration: 1500,
  bottom: 80,         // 距底部 80vp(避开 TabBar)
});

// 错题已掌握移除
promptAction.showToast({
  message: '太棒了!该题已从错题本移除',
  duration: 1500,
});

坑点 1promptAction.showToast 静态调用在新版本已 deprecated。推荐改成 UIContext 路径:

this.getUIContext().getPromptAction().showToast({ message: '已收藏' });

项目里两种写法混用没问题,但新代码统一用 UIContext。

坑点 2:Toast bottom 参数默认贴底,会与底部 TabBar 重叠看不清。记得设 bottom: 80 或 100

4.2 第 4 类(部分):showDialog 二次确认

需要用户做决策的场景,比如清空数据、退出登录、删除内容:

async clearHistory(): Promise<void> {
  try {
    const result = await this.getUIContext().getPromptAction().showDialog({
      title: '清空反馈记录?',
      message: '该操作将清除本地保存的反馈历史,且不可恢复。',
      buttons: [
        { text: '取消',     color: '#6B7280' },
        { text: '确定清空', color: '#DC2626' },    // 危险动作红色
      ],
    });
    if (result.index === 1) {                       // 用户点了"确定清空"
      await Pref.setJSON(FEEDBACK_STORAGE_KEY, []);
      this.history = [];
      promptAction.showToast({ message: '已清空' });
    }
  } catch (_e) {
    // 用户点空白处或返回手势 → reject
  }
}

要点:

  • 左取消、右确定是 iOS / 鸿蒙惯例,用户认知不冲突
  • 危险动作用红色(#DC2626 或 #B35C5C),主动作用主色,视觉权重一眼分辨
  • result.index 对应 buttons 数组下标,用户点空白处会 reject,必须 try-catch

4.3 第 4 类(部分):showActionMenu 列表选项

5 个以内的单选选项用 ActionMenu,比 Dialog 紧凑、比 Sheet 轻量:

async openGoalPicker(): Promise<void> {
  try {
    const result = await this.getUIContext().getPromptAction().showActionMenu({
      title: '每日学习目标',
      buttons: [
        { text: '1 首 / 天(轻松)',   color: '#3F4543' },
        { text: '2 首 / 天',           color: '#3F4543' },
        { text: '3 首 / 天(推荐)',   color: '#436444' },   // 主色推荐
        { text: '4 首 / 天',           color: '#3F4543' },
        { text: '5 首 / 天(挑战)',   color: '#B35C5C' },   // 警示色挑战
      ],
    });
    const idx = result.index;
    if (idx >= 0 && idx <= 4) {
      const v = idx + 1;
      await Pref.setString(StorageKey.DailyGoal, `${v}`);
      this.toast(`目标已设为每日 ${v} 首 ✨`);
    }
  } catch (_e) {
    // 用户取消
  }
}

坑点 3showActionMenu 选项最多 6 个(Android iOS 也是这个数)。超过 6 个该用 BottomSheet 或 Picker。

坑点 4:取消时会 reject,必须 try-catch 包裹,否则控制台报"unhandled promise rejection"。

4.4 第 4 类(部分):showActionSheet 滚动列表

当选项需要带图标 + 描述 + 滚动时,用 ActionSheet:

async openShareMenu(): Promise<void> {
  await this.getUIContext().getPromptAction().showActionSheet({
    title: '分享到',
    message: '选择分享方式',
    confirm: { value: '取消', action: () => {} },
    sheets: [
      { icon: $r('app.media.wechat'),  title: '微信',   action: () => this.shareToWeChat() },
      { icon: $r('app.media.moments'), title: '朋友圈', action: () => this.shareToMoments() },
      { icon: $r('app.media.weibo'),   title: '微博',   action: () => this.shareToWeibo() },
      { icon: $r('app.media.copy'),    title: '复制链接', action: () => this.copyLink() },
    ],
  });
}

sheets 数组支持 icon 字段,且可以滚动(适合 8+ 选项),ActionMenu 不行。

取舍:「古诗学习宝」头像选择没用 ActionSheet,而是自己写了一个 BottomSheet——因为头像选择不只是"列表选项",还有网格布局的卡通预设,ActionSheet 满足不了。

4.5 第 5 类:openCustomDialog 自定义庆祝弹窗

这是最强大也最灵活的弹窗——你完全控制 UI。「古诗学习宝」用它做打卡成功 / 成就解锁的庆祝。

5.1 定义 ComponentContent 的 @Builder
// components/CelebrateDialog.ets
export interface CelebrateData {
  emoji: string;       // 大 emoji,如 🎉 🏅 🔥
  title: string;       // 主标题
  subtitle: string;    // 副标题
  badge?: string;      // 右上角徽标
  accent?: string;     // 主色 hex
}

@Builder
export function buildCelebrate(data: CelebrateData) {
  Column({ space: 14 }) {
    // 顶部 emoji + 光晕
    Stack() {
      Column().width(110).height(110).borderRadius(55)
        .backgroundColor((data.accent ?? '#436444') + '22')
      Stack() { Text(data.emoji).fontSize(48) }
        .width(88).height(88).borderRadius(44)
        .backgroundColor(data.accent ?? '#436444')
    }
    .alignContent(Alignment.Center)

    // 标题
    Text(data.title)
      .fontSize(22).fontWeight(FontWeight.Bold)
      .fontColor('#2D3436')

    // 副标题
    Text(data.subtitle)
      .fontSize(13).fontColor('#5C6B72')
      .textAlign(TextAlign.Center)
      .padding({ left: 8, right: 8 })

    // 徽标
    if (data.badge !== undefined && data.badge.length > 0) {
      Row({ space: 4 }) {
        Text('✨').fontSize(12)
        Text(data.badge).fontSize(12).fontColor(data.accent ?? '#436444')
        Text('✨').fontSize(12)
      }
      .padding({ left: 12, right: 12, top: 4, bottom: 4 })
      .backgroundColor((data.accent ?? '#436444') + '15')
      .borderRadius(12)
    }

    Text('点击任意处关闭')
      .fontSize(10).fontColor('#9BA5AA').margin({ top: 6 })
  }
  .padding({ left: 28, right: 28, top: 32, bottom: 24 })
  .backgroundColor('#FFFFFF')
  .borderRadius(20).width(280)
  .alignItems(HorizontalAlign.Center)
  .shadow({ radius: 24, color: '#22000000', offsetX: 0, offsetY: 8 })
}

坑点 5@Builder 配合 openCustomDialog 使用必须是全局函数(不能写在 struct 内),且必须用 wrapBuilder 包装。写在 struct 内会运行时报错"@Builder not found"

5.2 用 ComponentContent + wrapBuilder 弹出
// service/DialogService.ets
import { ComponentContent, UIContext } from '@kit.ArkUI';
import { buildCelebrate, CelebrateData } from '../components/CelebrateDialog';

class DialogServiceImpl {
  /**
   * 显示庆祝弹窗,X 毫秒后自动关闭。
   */
  showCelebrate(ui: UIContext, data: CelebrateData, autoCloseMs: number = 2200): void {
    const node = new ComponentContent<CelebrateData>(
      ui,
      wrapBuilder<[CelebrateData]>(buildCelebrate),
      data,
    );
    const promptAction = ui.getPromptAction();

    promptAction.openCustomDialog(node, {
      autoCancel: true,                                    // 点空白处关闭
      alignment: DialogAlignment.Center,                   // 居中
      maskColor: '#33000000',                              // 半透明遮罩
      onWillDismiss: (action: DismissDialogAction) => {
        action.dismiss();                                  // 默认行为,可以拦截
      },
    }).catch((err: Error) => {
      console.error('openCustomDialog failed:', err.message);
    });

    // 自动关闭
    if (autoCloseMs > 0) {
      setTimeout(() => {
        promptAction.closeCustomDialog(node).catch(() => {
          // 已被用户手动关闭,忽略
        });
      }, autoCloseMs);
    }
  }
}

export const DialogService = new DialogServiceImpl();

业务调用方一行搞定:

// 在某个 onClick 里
DialogService.showCelebrate(this.getUIContext(), {
  emoji: '🎉',
  title: '连续打卡 5 天达成!',
  subtitle: '你又向「秀才」迈进一步 ✨',
  badge: '连续 5 天',
  accent: '#436444',
}, 2200);

坑点 6:必须保留 node = new ComponentContent(...) 引用!否则 closeCustomDialog(node) 找不到节点。Service 单例的好处就是把生命周期托管在 service 内部。

4.6 第 6 类:showTextPickerDialog 级联选择

「古诗学习宝」目前没用到,但选年级 / 选朝代 / 选诗派等场景非常合适:

import { promptAction } from '@kit.ArkUI';

async openGradePicker(): Promise<void> {
  const result = await this.getUIContext().getPromptAction().showTextPickerDialog({
    title: '选择年级',
    range: ['一年级', '二年级', '三年级', '四年级', '五年级', '六年级'],
    selected: 0,
    canLoop: false,                                      // 不允许循环滚动
  });
  if (result && result.index !== undefined) {
    const grade = result.index + 1;
    await Pref.setString(StorageKey.UserGrade, `${grade}`);
  }
}

或者用 CalendarPickerDialog 选学习开始日期:

import { CalendarPickerDialog } from '@kit.ArkUI';

CalendarPickerDialog.show({
  selected: new Date(),
  onAccept: (date: Date) => {
    console.log(`选择日期:${date.toISOString()}`);
  },
});

取舍showActionMenu vs showTextPickerDialog —— 6 个以内固定选项用 ActionMenu(一屏显示);6 个以上或动态范围用 TextPicker(滚轮显示)。

4.7 第 7 类:bindSheet 半模态上拉

「古诗学习宝」头像选择手写了底部浮层,其实可以改用系统的 bindSheet

// pages/views/ProfileView.ets
@Local showSheet: boolean = false;

build() {
  Column() {
    // ... 主页面内容
    Image(this.avatar)
      .onClick(() => { this.showSheet = true; })
      .bindSheet($$this.showSheet, this.SheetBuilder(), {
        height: SheetSize.MEDIUM,            // 中等高度
        dragBar: true,                       // 显示顶部抓手
        showClose: true,
        backgroundColor: '#FFFFFF',
        cornerRadius: 20,
        onWillDismiss: (action) => action.dismiss(),
      })
  }
}

@Builder
SheetBuilder() {
  Column() {
    Text('更换头像').fontSize(18).fontWeight(FontWeight.Bold)
    // ... 3 个动作 + 4 个卡通预设
  }
}

坑点 7bindSheet 第一个参数必须用 $$ 双向绑定布尔变量,否则点空白处关闭后 showSheet 不会变回 false。

4.8 第 7 类:bindContentCover 全屏覆盖

适合专注式全屏交互,比如"全屏背诵挑战"、“沉浸式诗词鉴赏”:

@Local showFullScreen: boolean = false;

Button('开始全屏背诵')
  .onClick(() => { this.showFullScreen = true; })
  .bindContentCover($$this.showFullScreen, this.FullScreenBuilder(), {
    modalTransition: ModalTransition.DEFAULT,            // 默认上拉转场
    backgroundColor: '#FAFAFC',
  })

@Builder
FullScreenBuilder() {
  Column() {
    // 全屏背诵 UI
    Text('生当作人杰,').fontSize(28)
    // ...
    Button('退出').onClick(() => { this.showFullScreen = false; })
  }
}

bindContentCover全屏bindSheet半模态——前者完全打断主流程,后者只是叠加。

4.9 第 8 类:OverlayManager 全局浮层

吊球、连击计数、悬浮入口等跨页面持续显示的浮层,用 OverlayManager

import { ComponentContent } from '@kit.ArkUI';

@Builder
function buildStreakBall(streak: number) {
  Stack() {
    Column().width(56).height(56).borderRadius(28)
      .backgroundColor('#FF6B35')
    Text(`🔥${streak}`).fontSize(14).fontColor(Color.White)
  }
}

// 打卡成功时显示
showStreakBall(streak: number): void {
  const ui = this.getUIContext();
  const node = new ComponentContent<number>(
    ui, wrapBuilder<[number]>(buildStreakBall), streak,
  );
  ui.getOverlayManager().addComponentContent(node);

  // 3 秒后移除
  setTimeout(() => {
    ui.getOverlayManager().removeComponentContent(node);
  }, 3000);
}

OverlayManager 浮层会跨页面持续显示(除非主动移除),适合"连击 5 天" 这种需要持续提醒的场景。

4.10 第 2、3 类:Popup 与 Menu

「古诗学习宝」目前没用到,但典型场景:

// Popup - 新手引导气泡
@Local showTip: boolean = false;

Image($r('app.media.ic_help'))
  .onClick(() => { this.showTip = true; })
  .bindPopup(this.showTip, {
    message: '点这里查看注释 / 译文 / 赏析',
    primaryButton: { value: '知道了', action: () => { this.showTip = false; } },
    placement: Placement.Bottom,
    backgroundColor: '#1F2937',
  })

// 长按上下文菜单 - 诗词卡片
PoemCard({ ... })
  .bindContextMenu(this.ContextMenuBuilder(), ResponseType.LongPress)

@Builder
ContextMenuBuilder() {
  Menu() {
    MenuItem({ content: '收藏',     startIcon: $r('app.media.ic_star') })
      .onClick(() => this.toggleFav())
    MenuItem({ content: '分享',     startIcon: $r('app.media.ic_share') })
      .onClick(() => this.share())
    MenuItem({ content: '加入计划', startIcon: $r('app.media.ic_plan') })
      .onClick(() => this.addToPlan())
  }
}

5、完整数据流分析

以「用户打卡成功 → 5 连续天数庆祝 → 状态同步」为例:

用户点首页"打卡"快捷入口
    │
    ▼
PlanService.checkInToday()
    ├─ 计算今天是否已打卡
    ├─ Preferences 写入今日打卡记录
    └─ 计算 streak(连续天数)

if (streak >= 5 && streak 是整数倍 OR 首次达 5 天)
    │
    ▼
DialogService.showCelebrate(ui, {
  emoji: '🎉', title: `连续打卡 ${streak} 天达成!`,
  subtitle: '你又向「秀才」迈进一步 ✨',
  badge: `连续 ${streak} 天`,
  accent: '#436444',
}, 2200)
    │
    ▼
ComponentContent 包装 → openCustomDialog → 居中弹起
    │
    ▼  2200ms 后
setTimeout → closeCustomDialog → 自动关闭
─────────────────────────────────────────────────────────────────
同时(与弹窗并行):

PlanService.checkInToday → favVersion++  
    │
    ▼  V2 reactive 触发
@Computed stat.streak: 4 → 5
    │
    ▼
所有依赖 favVersion 的组件重渲染
    ├─ HomeView 头部"连续 5 天"标签更新
    ├─ ProfileView 4 张统计卡之一更新
    ├─ PlanView 月历"今天"高亮
    └─ AchievementsPage 坚持篇"连续 5 天"成就达成
            │
            ▼ 若达成
        触发解锁成就的二次庆祝
        DialogService.showCelebrate(ui, {
          emoji: '🏅', title: '解锁成就「初露锋芒」',
          subtitle: '连续打卡 5 天 ✨',
          accent: '#FF6B35',
        }, 2500)
─────────────────────────────────────────────────────────────────
若开启了 OverlayManager 吊球(可选优化)

showStreakBall(5)
    └─ OverlayManager.addComponentContent
            ↓ 右下角悬浮 🔥5 吊球
            ↓ 用户切到任何页面都看得见
            ↓ 3 秒后自动移除

观察点:

  1. 庆祝弹窗与状态同步并行:CustomDialog 弹出和 @Computed 重算互不阻塞,视觉反馈和数据更新同时完成
  2. 链式庆祝:打卡成功庆祝 → 成就达成庆祝可以串起来(用 setTimeout 错峰),给用户连续正反馈
  3. 2200ms 自动关闭:用户来不及主动关时也不会"卡屏",Toast 的"轻量"和 Dialog 的"内容"结合
  4. OverlayManager 跨页持久:用户切到其他 tab 仍能看见连击吊球,强化成就感

6、代码分析与优化建议

6.1 现有实现的亮点

  • DialogService 单例封装:业务层不直接接触 ComponentContent,只关心数据
  • CustomDialog 用全局 @Builder + wrapBuilder:标准 ArkUI 6 推荐模式
  • autoCancel + onWillDismiss 双保险:用户主动关 + 系统关都正确处理
  • 2200ms 自动关闭:兼顾"用户主动关"和"懒用户被动关"
  • 左取消、右确定 + 颜色编码:跟 iOS 鸿蒙惯例对齐,认知零成本

6.2 可优化点

优化 1:showToast 全局封装

问题:散落各处的 promptAction.showToast,新人不知道默认配置。

改进:扩展 DialogService.toast:

// service/DialogService.ets
toast(msg: string, duration: number = 1500): void {
  this.getUIContext().getPromptAction().showToast({
    message: msg,
    duration: duration,
    bottom: 80,        // 默认避开 TabBar
  });
}

// 业务调用
DialogService.toast('已收藏');
优化 2:把 AvatarSheet 换成 bindSheet

问题:当前 AvatarSheet 是自己写的 Stack + 遮罩 + 圆角面板,约 90 行代码。

改进:用系统 bindSheet($$showSheet, builder, { height: SheetSize.MEDIUM })代码减半 + 自带拖拽手势 + 自带圆角阴影

优化 3:showDialog 改用更现代的 openCustomDialog

问题showDialog 样式不可改,强 iOS 风格在某些产品里"违和"。

改进:用 openCustomDialog 自定义二次确认 UI:

DialogService.showConfirm(ui, {
  title: '清空记录?',
  message: '该操作不可恢复',
  confirmText: '确定清空',
  confirmColor: '#DC2626',
  onConfirm: () => this.doClear(),
});
优化 4:错误反馈用 Popup 替代 Toast

问题:表单错误"邮箱格式不对"用 Toast 在底部弹,用户视线在输入框看不到。

改进:错误时用 bindPopup 把气泡定位在输入框旁边:

TextInput()
  .bindPopup(this.emailError, {
    message: '邮箱格式不正确',
    placement: Placement.Right,
    backgroundColor: '#FEE2E2',
  })
优化 5:连击 OverlayManager 吊球

问题:连续打卡 5 天等"长效正反馈"只在打卡瞬间显示 Dialog 一次。

改进:用 OverlayManager 显示右下角持久吊球,用户切到任何页面都能看见,强化成就感和留存

6.3 生产环境 Checklist

检查项 说明
promptAction.showToast 优先用 UIContext 路径 静态调用已 deprecated
Toast 设 bottom: 80 避开 TabBar 否则被遮挡
showActionMenu 取消必须 try-catch reject 触发 unhandled
危险动作 button 用红色 #DC2626 / #B35C5C 视觉权重区分
选项 6 个以内用 ActionMenu,超过用 Picker/Sheet 一屏显示原则
CustomDialog @Builder 必须是全局函数 写在 struct 内 runtime 报错
wrapBuilder 包装才能传给 ComponentContent 必须步骤
必须保留 ComponentContent 引用才能 close 否则关不掉
bindSheet / bindContentCover$$ 双向绑定 单向绑定关不上
bindPopup 用于定位某控件,不要全屏 与 Toast 互补
OverlayManager 添加后必须主动 remove 否则一直显示
业务调用走 DialogService 单例 不要散落原始 API

7、关键 API 速查

API 类别 作用
getUIContext().getPromptAction().showToast(opts) Toast 轻量提示
.bindPopup($$show, { message, placement }) Popup 控件旁气泡
.bindMenu([{value, action}]) Menu 点击菜单
.bindContextMenu(builder, ResponseType.LongPress) Menu 长按菜单
getUIContext().getPromptAction().showDialog({title, buttons}) Dialog 固定 二次确认
getUIContext().getPromptAction().showActionMenu({title, buttons}) Dialog 固定 6 以内单选
getUIContext().getPromptAction().showActionSheet({sheets}) Dialog 固定 滚动列表选择
getUIContext().getPromptAction().showAlertDialog({title, message}) Dialog 固定 单按钮提示
new ComponentContent(ui, wrapBuilder(fn), data) CustomDialog 自定义弹窗内容
promptAction.openCustomDialog(node, options) CustomDialog 弹出自定义 UI
promptAction.closeCustomDialog(node) CustomDialog 关闭自定义弹窗
getUIContext().getPromptAction().showTextPickerDialog({range}) Picker 选项选择
getUIContext().getPromptAction().showDatePickerDialog({}) Picker 日期选择
CalendarPickerDialog.show({}) Picker 日历日期选择
.bindSheet($$show, builder, { height: SheetSize.MEDIUM }) 模态 半模态上拉
.bindContentCover($$show, builder, opts) 模态 全屏覆盖
getUIContext().getOverlayManager().addComponentContent(node) Overlay 全局浮层
getUIContext().getOverlayManager().removeComponentContent(node) Overlay 移除浮层

8、总结

本文用「古诗学习宝」线上版本的 5 个真实弹窗 + 3 个扩展场景,把 HarmonyOS 6 七大类弹窗 API 一次串透:

  1. 决策树先行:纯反馈 → Toast;指向控件 → Popup/Menu;二次确认 → showDialog;列表选项 → ActionMenu/ActionSheet;自定义 UI → openCustomDialog;选项选择 → Picker;半模态 → bindSheet;全屏 → bindContentCover;全局吊球 → OverlayManager。8 种形态对应 8 种用户心智

  2. CustomDialog 是最强大武器new ComponentContent + wrapBuilder + openCustomDialog 三件套,让你用任意 ArkUI 组件做弹窗内容,配合 DialogService 单例业务层一行调用。

  3. **selectedKey + bind 是状态弹窗的标配 ∗ ∗ : b i n d S h e e t / b i n d C o n t e n t C o v e r / b i n d P o p u p 都用 ‘ 是状态弹窗的标配**:bindSheet / bindContentCover / bindPopup 都用 ` 是状态弹窗的标配bindSheet/bindContentCover/bindPopup都用show` 双向绑定 Boolean,单向绑定会关不上

  4. 8 个真坑写进 Checklist:showToast 用 UIContext 不用静态 / Toast bottom 80 避开 TabBar / ActionMenu cancel try-catch / 危险动作红色 / Builder 必须全局函数 / wrapBuilder 必步骤 / ComponentContent 引用必保留 / 双向绑定。

  5. 5 个优化方向:toast 全局封装 / AvatarSheet 换 bindSheet / showDialog 升 CustomDialog / 错误反馈用 Popup / 连击用 OverlayManager 吊球——直接对接生产级体验。

把这套「弹窗选型决策树 + DialogService 单例封装 + 8 类 API 速查」吃透,鸿蒙下任何弹窗场景都能 3 秒选准、5 分钟搞定。不再为"该用哪种弹窗"纠结


🎁 下载体验

**「古诗学习宝」**已上架华为应用市场,搜索 古诗学习宝 即可下载。本文 5 类真实弹窗在 App 里都能体验到:选每日目标 ActionMenu、点关于 showDialog、清空确认 Dialog、选头像 BottomSheet、打卡庆祝 CustomDialog。

📚 文中代码全部摘自上架版本,包含 DialogService 单例封装可直接复用。

👋 欢迎在评论区留言你遇到的弹窗选型纠结,一起讨论。

Logo

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

更多推荐