HarmonyOS6 弹窗实战:七类弹窗 API 在真实项目里的选型决策树#星光计划4.0#20260517
本文介绍了鸿蒙应用开发中的弹窗选型策略,基于「古诗学习宝」实际案例总结出7大类弹窗使用场景。主要内容包括: 弹窗分类决策树:从轻量提示Toast到全屏模态覆盖,提供清晰的选型路径 实际应用场景: ActionMenu用于5档学习目标选择 自定义BottomSheet实现头像选择 CustomDialog展示庆祝动画 showDialog处理关于页面 Toast提供操作反馈 核心特点: 每类弹窗对应
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 档


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

「更换头像」从底部上拉的浮层: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,
});
坑点 1:
promptAction.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) {
// 用户取消
}
}
坑点 3:
showActionMenu选项最多 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()}`);
},
});
取舍:
showActionMenuvsshowTextPickerDialog—— 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 个卡通预设
}
}
坑点 7:
bindSheet第一个参数必须用$$双向绑定布尔变量,否则点空白处关闭后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 秒后自动移除
观察点:
- 庆祝弹窗与状态同步并行:CustomDialog 弹出和 @Computed 重算互不阻塞,视觉反馈和数据更新同时完成。
- 链式庆祝:打卡成功庆祝 → 成就达成庆祝可以串起来(用 setTimeout 错峰),给用户连续正反馈。
- 2200ms 自动关闭:用户来不及主动关时也不会"卡屏",Toast 的"轻量"和 Dialog 的"内容"结合。
- 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 一次串透:
-
决策树先行:纯反馈 → Toast;指向控件 → Popup/Menu;二次确认 → showDialog;列表选项 → ActionMenu/ActionSheet;自定义 UI → openCustomDialog;选项选择 → Picker;半模态 → bindSheet;全屏 → bindContentCover;全局吊球 → OverlayManager。8 种形态对应 8 种用户心智。
-
CustomDialog 是最强大武器:
new ComponentContent + wrapBuilder + openCustomDialog三件套,让你用任意 ArkUI 组件做弹窗内容,配合 DialogService 单例业务层一行调用。 -
**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,单向绑定会关不上。
-
8 个真坑写进 Checklist:showToast 用 UIContext 不用静态 / Toast bottom 80 避开 TabBar / ActionMenu cancel try-catch / 危险动作红色 / Builder 必须全局函数 / wrapBuilder 必步骤 / ComponentContent 引用必保留 / 双向绑定。
-
5 个优化方向:toast 全局封装 / AvatarSheet 换 bindSheet / showDialog 升 CustomDialog / 错误反馈用 Popup / 连击用 OverlayManager 吊球——直接对接生产级体验。
把这套「弹窗选型决策树 + DialogService 单例封装 + 8 类 API 速查」吃透,鸿蒙下任何弹窗场景都能 3 秒选准、5 分钟搞定。不再为"该用哪种弹窗"纠结。
🎁 下载体验
**「古诗学习宝」**已上架华为应用市场,搜索 古诗学习宝 即可下载。本文 5 类真实弹窗在 App 里都能体验到:选每日目标 ActionMenu、点关于 showDialog、清空确认 Dialog、选头像 BottomSheet、打卡庆祝 CustomDialog。
📚 文中代码全部摘自上架版本,包含 DialogService 单例封装可直接复用。
👋 欢迎在评论区留言你遇到的弹窗选型纠结,一起讨论。
更多推荐




所有评论(0)