ArkUI 固定样式弹出框实战:五种弹窗的选型、技巧与避坑全解析

适用版本:HarmonyOS 5.0.0+ / API 12+(生命周期特性需 API 19+)
难度:初中级
预计阅读时间:30 分钟
写在前面
在鸿蒙应用开发中,弹窗是一个绕不开的话题。上一篇我们讲了自定义弹窗的完整体系,但很多时候你不需要那么"重"的方案——你只是想弹一个确认框、显示一个操作菜单、让用户选个日期。这时候,固定样式弹出框就是最高效的选择。
固定样式弹出框的核心设计理念是:布局由系统固定,开发者只管填内容。 你不需要关心弹窗的圆角、阴影、按钮排列、动画曲线——系统全帮你做好了。你只需要传入标题、内容和按钮文本,就能得到一个符合鸿蒙设计规范的弹窗。
这听起来简单,但实际开发中有不少门道。比如:在异步回调里怎么调用弹窗?非模态弹窗和模态弹窗到底有什么区别?CalendarPickerDialog 为什么在 Worker 线程里用不了?API 19 新增的生命周期回调该怎么用?
这篇文章会逐一讲清楚。
一、固定样式弹出框全景图
ArkUI 提供了以下几种固定样式弹出框:
| 弹窗类型 | 调用方式 | 核心用途 | 模态/非模态 |
|---|---|---|---|
| showActionMenu | PromptAction 对象 |
操作菜单,多按钮选择 | 支持切换 |
| showDialog | PromptAction 对象 |
对话框,标题+消息+按钮 | 支持切换 |
| ActionSheet | UIContext 对象 |
列表选择弹窗 | 支持切换 |
| AlertDialog | UIContext 对象 |
警告弹窗,操作确认 | 支持切换 |
| CalendarPickerDialog | 直接调用 show() |
日历选择器 | 模态 |
| DatePickerDialog | UIContext 对象 |
日期滑动选择器 | 模态 |
| TimePickerDialog | UIContext 对象 |
时间滑动选择器 | 模态 |
| TextPickerDialog | UIContext 对象 |
文本滑动选择器 | 模态 |
它们共享一个关键特征:布局格式固定,不支持自定义内容区的字体颜色、大小、换行等样式。 如果你需要完全自定义弹窗内容,应该使用全局自定义弹出框或基础自定义弹出框,而不是强行在固定样式弹窗里塞自定义内容。
二、使用约束:90% 的报错都源于调用姿势不对
固定样式弹窗最常见的报错不是参数写错,而是调用姿势不对。不同弹窗的调用入口不同,搞混了就会抛异常。
2.1 两条调用路径
ArkUI 的固定样式弹窗有两条调用路径:
路径一:通过 PromptAction 对象调用
适用于 showActionMenu 和 showDialog。需要先从 UIContext 获取 PromptAction 对象:
import { PromptAction } from '@kit.ArkUI';
// 在组件内部
const uiContext = this.getUIContext();
const promptAction: PromptAction = uiContext.getPromptAction();
// 然后通过 promptAction 调用
promptAction.showActionMenu({ /* 配置 */ });
promptAction showDialog({ /* 配置 */ });
路径二:通过 UIContext 对象直接调用
适用于 ActionSheet、AlertDialog、PickerDialog 系列。需要先获取 UIContext 实例:
// 方式 A:通过 ohos.window 获取(适用于非 UI 上下文)
import { window } from '@kit.ArkUI';
const win = await window.getLastWindow(context);
const uiContext = win.getUIContext();
// 方式 B:通过自定义组件内置方法获取(组件内推荐)
const uiContext = this.getUIContext();
// 然后直接调用
uiContext.showActionSheet({ /* 配置 */ });
uiContext.showAlertDialog({ /* 配置 */ });
uiContext.showDatePickerDialog({ /* 配置 */ });
路径三:CalendarPickerDialog 特殊处理
CalendarPickerDialog 是唯一一个不依赖 UIContext 的弹窗,直接调用 CalendarPickerDialog.show() 即可。但它也有一个硬约束:必须在 UI 执行上下文明确的地方使用。 也就是说,你不能在 Worker 线程、异步回调的深层嵌套里、或者应用还没完成初始化时调用它。
2.2 在异步回调中调用弹窗
这是开发者最容易踩的坑。你在 setTimeout、网络请求回调、Promise.then() 里直接调用弹窗接口,结果报错"UI 上下文不明确"。
原因: 异步回调执行时,当前的 UI 上下文可能已经发生了变化(比如用户已经切换了页面),系统无法确定弹窗应该挂载到哪个窗口上。
解决方案:提前捕获 UIContext。
@Entry
@Component
struct AsyncDialogPage {
// 提前保存 UIContext
private uiContext: UIContext | null = null;
aboutToAppear() {
// 在 UI 上下文明确时,提前获取
this.uiContext = this.getUIContext();
}
build() {
Column() {
Button('异步请求后弹窗')
.onClick(async () => {
// 网络请求是异步的,回调时 UI 上下文可能已变化
const result = await fetchDataFromNetwork();
// 使用提前保存的 UIContext,而不是重新获取
if (this.uiContext) {
this.uiContext.showAlertDialog({
title: '请求结果',
message: result.message,
buttons: [
{ text: '确定', color: '#007dff' }
]
});
}
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
关键原则: 在 UI 上下文明确的地方(组件生命周期内、事件回调内)获取 UIContext 并保存,在异步回调中使用保存的引用。
三、生命周期:API 19 带来的精细化控制
从 API version 19 开始,showDialog、ActionSheet、AlertDialog 支持四个生命周期回调。这是很多人忽略但非常实用的能力。
3.1 四个生命周期回调
弹窗触发
│
▼
onWillAppear ← 显示动效开始前触发
│
▼
[入场动画播放]
│
▼
onDidAppear ← 弹窗完全显示后触发
│
▼
[用户交互阶段]
│
▼
onWillDisappear ← 退出动效开始前触发
│
▼
[退场动画播放]
│
▼
onDidDisappear ← 弹窗完全消失后触发
3.2 生命周期的实际应用
这四个回调不是摆设,每个都有明确的用途:
import { PromptAction } from '@kit.ArkUI';
@Entry
@Component
struct LifecycleDialogPage {
@State isLoading: boolean = false;
@State dialogState: string = '未弹出';
build() {
Column({ space: 20 }) {
Text(`弹窗状态:${this.dialogState}`)
.fontSize(16)
Button('打开带生命周期的对话框')
.onClick(() => {
const promptAction: PromptAction = this.getUIContext().getPromptAction();
promptAction.showDialog({
title: '生命周期演示',
message: '观察控制台日志,查看生命周期回调顺序',
buttons: [
{ text: '确定', color: '#007dff' },
{ text: '取消', color: '#666666' }
],
// === 生命周期回调 ===
onWillAppear: () => {
this.dialogState = '即将出现';
console.info('Dialog: onWillAppear - 动效即将开始');
// 适合做:暂停背景动画、禁用页面交互
},
onDidAppear: () => {
this.dialogState = '已显示';
console.info('Dialog: onDidAppear - 弹窗已完全显示');
// 适合做:启动弹窗内计时器、开始埋点统计
},
onWillDisappear: () => {
this.dialogState = '即将消失';
console.info('Dialog: onWillDisappear - 退出动效即将开始');
// 适合做:保存用户输入、停止弹窗内计时器
},
onDidDisappear: () => {
this.dialogState = '已消失';
console.info('Dialog: onDidDisappear - 弹窗已完全消失');
// 适合做:恢复页面交互、跳转页面、触发后续逻辑
}
})
.then(data => {
console.info('用户点击了按钮索引:', data.index);
});
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
每个回调的最佳用途:
| 回调 | 典型用途 | 禁止做的事 |
|---|---|---|
onWillAppear |
暂停背景动画、禁用页面滚动 | 不要在这里做耗时操作,会卡住入场动画 |
onDidAppear |
启动弹窗内计时器、上报埋点 | 不要在这里再弹一个弹窗,会层级冲突 |
onWillDisappear |
保存用户输入、停止计时器 | 不要在这里取消弹窗关闭,会陷入死循环 |
onDidDisappear |
恢复页面交互、执行页面跳转 | 不要在这里访问弹窗内的组件引用,已销毁 |
四、操作菜单 showActionMenu:多选项的优雅方案
showActionMenu 用于在底部弹出操作菜单,适合提供 2-6 个操作选项的场景。相比 ActionSheet,它的样式更轻量,按钮排列更紧凑。
4.1 基础用法
import { PromptAction } from '@kit.ArkUI';
@Entry
@Component
struct ActionMenuPage {
build() {
Column({ space: 20 }) {
Button('分享内容')
.onClick(() => {
const promptAction: PromptAction = this.getUIContext().getPromptAction();
promptAction.showActionMenu({
title: '分享到',
buttons: [
{ text: '微信好友', color: '#07c160' },
{ text: '朋友圈', color: '#07c160' },
{ text: '复制链接', color: '#333333' },
{ text: '生成海报', color: '#333333' }
]
})
.then(data => {
console.info('选择了第', data.index, '项');
switch (data.index) {
case 0: this.shareToWechat(); break;
case 1: this.shareToMoments(); break;
case 2: this.copyLink(); break;
case 3: this.generatePoster(); break;
}
})
.catch((err: Error) => {
console.error('showActionMenu error:', err);
});
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
private shareToWechat(): void { /* ... */ }
private shareToMoments(): void { /* ... */ }
private copyLink(): void { /* ... */ }
private generatePoster(): void { /* ... */ }
}
4.2 使用技巧
技巧一:title 字体放大限制
title 字段的字体最大放大倍数为 2。这意味着如果用户在系统设置里开启了超大字体,标题最多放大到 2 倍,不会撑破弹窗布局。你不需要手动处理这个情况。
技巧二:buttons 数量建议
虽然 API 没有硬性限制 buttons 数量,但实际体验中:
- 2-4 个选项:最佳体验
- 5-6 个选项:可接受,需要滚动
- 超过 6 个:建议改用
ActionSheet,它支持标题+消息+列表的组合
技巧三:颜色语义化
buttons: [
{ text: '保存草稿', color: '#333333' },
{ text: '不保存', color: '#ff4444' }, // 危险操作用红色
{ text: '取消', color: '#666666' } // 次要操作用灰色
]
五、对话框 showDialog:最通用的信息弹窗
showDialog 是最基础的弹窗形式:一个标题、一段消息、若干按钮。它的通用性最强,但自定义程度也最低。
5.1 基础用法与回调两种模式
import { PromptAction } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
@Entry
@Component
struct ShowDialogPage {
build() {
Column({ space: 20 }) {
Button('模式一:Promise')
.onClick(() => {
const promptAction: PromptAction = this.getUIContext().getPromptAction();
promptAction.showDialog({
title: '同步完成',
message: '3 个文件已成功同步到云端',
buttons: [
{ text: '查看', color: '#007dff' },
{ text: '关闭', color: '#666666' }
]
})
.then(data => {
if (data.index === 0) {
this.navigateToFiles();
}
})
.catch((err: Error) => {
console.error('showDialog error:', err);
});
})
Button('模式二:回调函数')
.onClick(() => {
const promptAction: PromptAction = this.getUIContext().getPromptAction();
try {
promptAction.showDialog({
title: '确认操作',
message: '您确定要提交此表单吗?',
buttons: [
{ text: '提交', color: '#007dff' },
{ text: '取消', color: '#666666' }
]
}, (err, data) => {
if (err) {
console.error('showDialog error:', err);
return;
}
if (data.index === 0) {
this.submitForm();
}
});
} catch (error) {
const msg = (error as BusinessError).message;
const code = (error as BusinessError).code;
console.error(`showDialog args error: code=${code}, message=${msg}`);
}
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
private navigateToFiles(): void { /* ... */ }
private submitForm(): void { /* ... */ }
}
两种模式的区别:
| 模式 | 写法 | 适用场景 |
|---|---|---|
| Promise | .then(data => {...}) |
需要链式调用、async/await 场景 |
| 回调函数 | (err, data) => {...} |
简单场景、兼容旧代码 |
注意: 两种模式不要混用。选了 Promise 就别再传回调函数,选了回调就别再 .then()。
5.2 异步返回值的结构
无论哪种模式,返回的 data 对象都包含 index 字段,表示用户点击了 buttons 数组中的第几个按钮(从 0 开始)。
.then(data => {
// data.index: 0, 1, 2... 对应 buttons 数组的索引
console.info('用户选择了第', data.index, '个按钮');
})
六、列表选择弹窗 ActionSheet:带标题和消息的操作列表
ActionSheet 和 showActionMenu 的区别在于:ActionSheet 支持同时显示标题和消息文本,适合需要给用户更多上下文信息的场景。
6.1 基础用法
@Entry
@Component
struct ActionSheetPage {
build() {
Column({ space: 20 }) {
Button('删除文件')
.onClick(() => {
this.getUIContext().showActionSheet({
title: '删除文件',
message: '删除后文件将移入回收站,30 天后彻底清除。确定删除"年度报告.pdf"?',
autoCancel: true,
confirm: {
defaultFocus: true,
value: '确认删除',
fontColor: '#ff4444',
action: () => {
this.performDelete();
}
},
cancel: () => {
console.info('用户取消了删除操作');
},
// API 19+ 生命周期
onWillAppear: () => {
console.info('ActionSheet 即将出现');
},
onDidAppear: () => {
console.info('ActionSheet 已显示');
},
onWillDisappear: () => {
console.info('ActionSheet 即将消失');
},
onDidDisappear: () => {
console.info('ActionSheet 已消失');
}
});
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
private performDelete(): void {
// 执行删除
console.info('文件已删除');
}
}
6.2 自定义样式参数
ActionSheet 支持比 showDialog 更多的样式参数:
this.getUIContext().showActionSheet({
title: '选择操作',
message: '请选择您要执行的操作',
autoCancel: false,
// 自定义尺寸
width: 300,
height: 360,
// 自定义外观
cornerRadius: 20,
borderWidth: 1,
borderStyle: BorderStyle.Solid,
borderColor: '#e0e0e0',
// 蒙层颜色
maskColor: 'rgba(0,0,0,0.6)',
// 对齐方式
alignment: DialogAlignment.Center,
offset: { dx: 0, dy: -20 },
confirm: {
value: '确定',
fontColor: '#007dff',
action: () => {}
},
cancel: () => {},
sheets: [
{
title: '复制到剪贴板',
action: () => { this.copyToClipboard(); }
},
{
title: '移动到文件夹',
action: () => { this.moveToFolder(); }
},
{
title: '重命名',
action: () => { this.renameFile(); }
},
{
title: '删除',
action: () => { this.deleteFile(); }
}
]
});
七、警告弹窗 AlertDialog:不可逆操作的最后防线
AlertDialog 专门用于触发"将产生严重后果的不可逆操作"时的确认,如删除、重置、取消编辑。它在视觉上比 showDialog 更有警示感。
7.1 基础用法
@Entry
@Component
struct AlertDialogPage {
build() {
Column({ space: 20 }) {
Button('重置应用')
.onClick(() => {
this.getUIContext().showAlertDialog({
title: '重置应用',
message: '此操作将清除所有本地数据,包括笔记、收藏和设置。此操作不可撤销。',
autoCancel: false, // 禁止点击蒙层关闭
alignment: DialogAlignment.Center,
offset: { dx: 0, dy: -20 },
primaryButton: {
value: '重置',
fontColor: '#ff4444',
action: () => {
this.performReset();
}
},
secondaryButton: {
value: '取消',
fontColor: '#333333',
action: () => {}
},
// API 19+ 生命周期
onWillAppear: () => {
// 禁用页面的返回键,防止用户绕过确认
this.disableBackButton();
},
onDidDisappear: () => {
// 恢复返回键
this.enableBackButton();
}
});
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
private performReset(): void { /* ... */ }
private disableBackButton(): void { /* ... */ }
private enableBackButton(): void { /* ... */ }
}
7.2 三种按钮配置模式
AlertDialog 支持三种按钮配置,根据场景选择:
// 模式一:单按钮(仅确认)
this.getUIContext().showAlertDialog({
title: '提示',
message: '操作已完成',
confirm: {
value: '知道了',
action: () => {}
}
});
// 模式二:双按钮(确认 + 取消)
this.getUIContext().showAlertDialog({
title: '保存修改',
message: '您有未保存的修改,是否保存?',
primaryButton: {
value: '保存',
action: () => { this.saveChanges(); }
},
secondaryButton: {
value: '不保存',
action: () => { this.discardChanges(); }
}
});
// 模式三:三按钮(保存 + 不保存 + 取消)
this.getUIContext().showAlertDialog({
title: '退出编辑',
message: '是否保存修改后退出?',
primaryButton: {
value: '保存并退出',
action: () => { this.saveAndExit(); }
},
secondaryButton: {
value: '不保存退出',
action: () => { this.discardAndExit(); }
},
thirdButton: {
value: '取消',
action: () => {}
}
});
八、模态 vs 非模态:一个参数改变交互本质
showActionMenu、showDialog、ActionSheet、AlertDialog 都支持通过设置 isModal: false 变成非模态弹窗。这一个参数的切换,会彻底改变弹窗的交互行为。
8.1 模态 vs 非模态的对比
// 模态弹窗(默认):用户必须先处理弹窗,才能继续操作
this.getUIContext().showAlertDialog({
title: '网络错误',
message: '请检查网络连接',
isModal: true, // 默认值
buttons: [{ text: '重试', color: '#007dff' }]
});
// 非模态弹窗:弹窗显示同时用户可以继续操作页面
this.getUIContext().showAlertDialog({
title: '新版本可用',
message: 'v2.0.0 已发布,包含多项新功能',
isModal: false, // 非模态
buttons: [{ text: '稍后更新', color: '#007dff' }]
});
两种模式的行为差异:
| 行为 | 模态 (isModal: true) | 非模态 (isModal: false) |
|---|---|---|
| 蒙层遮罩 | 有,背景变暗 | 无,背景正常 |
| 背景可交互 | 否 | 是 |
| 用户操作流程 | 被中断 | 不中断 |
| 关闭方式 | 必须用户主动关闭 | 用户操作或自动关闭 |
8.2 非模态弹窗的适用场景
非模态弹窗适合"告知但不强制"的场景:
// 场景:后台同步完成通知
async function syncData(): Promise<void> {
await performSync();
// 同步完成后,非模态提示用户
this.getUIContext().showAlertDialog({
title: '同步完成',
message: '您的数据已同步到云端',
isModal: false, // 不打断用户当前操作
buttons: [{ text: '知道了', color: '#007dff' }]
});
}
// 场景:有新消息提醒
function notifyNewMessage(): void {
this.getUIContext().showAlertDialog({
title: '新消息',
message: '您有 3 条未读消息',
isModal: false,
primaryButton: {
value: '查看',
action: () => { router.pushUrl({ url: 'pages/Messages' }); }
},
secondaryButton: {
value: '忽略',
action: () => {}
}
});
}
九、选择器弹窗:日期、时间、文本的自定义样式
选择器弹窗系列(DatePickerDialog、TimePickerDialog、TextPickerDialog)都支持通过配置参数实现自定义文本和按钮样式。CalendarPickerDialog 则通过 acceptButtonStyle 和 cancelButtonStyle 实现按钮自定义。
9.1 CalendarPickerDialog:日历视图选择器
CalendarPickerDialog 提供完整的日历视图,适合需要精确选择某一天的场景。
@Entry
@Component
struct CalendarPickerPage {
@State selectedDate: Date = new Date('2026-07-23');
@State displayDate: string = '';
aboutToAppear() {
this.updateDisplayDate();
}
build() {
Column({ space: 20 }) {
Text(`当前选择日期:${this.displayDate}`)
.fontSize(16)
Button('选择日期')
.onClick(() => {
CalendarPickerDialog.show({
selected: this.selectedDate,
// 自定义确认按钮样式
acceptButtonStyle: {
fontColor: '#007dff',
fontSize: '16fp',
backgroundColor: '#f0f6ff',
borderRadius: 10,
fontWeight: FontWeight.Medium
},
// 自定义取消按钮样式
cancelButtonStyle: {
fontColor: Color.Red,
fontSize: '16fp',
backgroundColor: '#f7f7f7',
borderRadius: 10
},
onAccept: (date: Date) => {
this.selectedDate = date;
this.updateDisplayDate();
console.info('用户选择了:', date.toString());
},
onCancel: () => {
console.info('用户取消了选择');
}
});
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
private updateDisplayDate(): void {
const y = this.selectedDate.getFullYear();
const m = String(this.selectedDate.getMonth() + 1).padStart(2, '0');
const d = String(this.selectedDate.getDate()).padStart(2, '0');
this.displayDate = `${y}-${m}-${d}`;
}
}
CalendarPickerDialog 的使用约束:
-
必须在 UI 上下文明确的地方调用。 不能在 Worker 线程、深层异步回调、应用未完成初始化时调用。如果需要在异步回调中使用,提前在 UI 上下文明确时触发。
-
不支持通过 UIContext 调用。 它是唯一直接通过
CalendarPickerDialog.show()调用的弹窗。 -
acceptButtonStyle 和 cancelButtonStyle 的 primary 字段互斥。 两个按钮中最多只能有一个
primary设为true,表示哪个是主要按钮(视觉上更突出)。
9.2 DatePickerDialog:带农历切换的日期选择器
@Entry
@Component
struct DatePickerPage {
@State selectTime: Date = new Date('2026-07-23T08:30:00');
build() {
Column({ space: 20 }) {
Button('选择日期(含农历)')
.onClick(() => {
this.getUIContext().showDatePickerDialog({
start: new Date('2000-1-1'),
end: new Date('2100-12-31'),
selected: this.selectTime,
lunarSwitch: true, // 显示农历切换开关
showTime: true, // 显示时间选择
// 自定义文本样式
textStyle: {
color: '#999999',
font: { size: '14fp', weight: FontWeight.Normal }
},
selectedTextStyle: {
color: '#007dff',
font: { size: '18fp', weight: FontWeight.Medium }
},
// 自定义按钮样式
acceptButtonStyle: {
fontColor: '#007dff',
fontSize: '16fp',
backgroundColor: '#f0f6ff',
borderRadius: 10
},
cancelButtonStyle: {
fontColor: '#666666',
fontSize: '16fp',
backgroundColor: '#f7f7f7',
borderRadius: 10
},
onDateAccept: (value: Date) => {
this.selectTime = value;
console.info('选择的日期:', value.toString());
}
});
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
9.3 TextPickerDialog:级联文本选择器
@Entry
@Component
struct TextPickerPage {
private regions: TextCascadePickerRangeContent[] = [
{
text: '广东省',
children: [
{
text: '深圳市',
children: [
{ text: '南山区' },
{ text: '福田区' },
{ text: '宝安区' }
]
},
{
text: '广州市',
children: [
{ text: '天河区' },
{ text: '越秀区' },
{ text: '海珠区' }
]
}
]
},
{
text: '浙江省',
children: [
{
text: '杭州市',
children: [
{ text: '西湖区' },
{ text: '滨江区' },
{ text: '余杭区' }
]
}
]
}
];
private selectedIndex: number = 0;
build() {
Column({ space: 20 }) {
Button('选择地区')
.onClick(() => {
this.getUIContext().showTextPickerDialog({
range: this.regions,
selected: this.selectedIndex,
acceptButtonStyle: {
fontColor: '#007dff',
fontSize: '16fp',
backgroundColor: '#f0f6ff',
borderRadius: 10
},
cancelButtonStyle: {
fontColor: '#666666',
fontSize: '16fp',
backgroundColor: '#f7f7f7',
borderRadius: 10
},
onAccept: (value: TextPickerResult) => {
this.selectedIndex = value.index as number;
console.info('选择了:', JSON.stringify(value.value));
}
});
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
9.4 按钮自定义样式的完整参数
acceptButtonStyle 和 cancelButtonStyle 支持的完整参数:
interface PickerDialogButtonStyle {
fontColor: ResourceColor; // 文本颜色
fontSize: number | string; // 字号
fontWeight: FontWeight; // 字重
fontStyle: FontStyle; // 字体样式(正常/斜体)
fontFamily: string; // 字体族
backgroundColor: ResourceColor; // 背景色
borderRadius: number; // 圆角
primary: boolean; // 是否为主要按钮(视觉突出)
role: ButtonRole; // 按钮角色(正常/危险)
}
样式搭配建议:
// 方案一:确认为主色调,取消为次要色
acceptButtonStyle: {
fontColor: '#ffffff',
fontSize: '16fp',
backgroundColor: '#007dff',
borderRadius: 8,
primary: true
},
cancelButtonStyle: {
fontColor: '#666666',
fontSize: '16fp',
backgroundColor: '#f5f5f5',
borderRadius: 8,
primary: false
}
// 方案二:危险操作,确认按钮用红色
acceptButtonStyle: {
fontColor: '#ffffff',
fontSize: '16fp',
backgroundColor: '#ff4444',
borderRadius: 8,
primary: true,
role: ButtonRole.DESTRUCTIVE
},
cancelButtonStyle: {
fontColor: '#333333',
fontSize: '16fp',
backgroundColor: '#f5f5f5',
borderRadius: 8,
primary: true
}
十、实战整合:一个完整的表单提交流程
把所有固定样式弹窗串起来,模拟一个完整的用户操作流程。
import { PromptAction } from '@kit.ArkUI';
@Entry
@Component
struct FormSubmitFlow {
@State formData: FormData = {
title: '',
date: new Date(),
priority: 'normal'
};
build() {
Column({ space: 16 }) {
Text('新建任务')
.fontSize(24)
.fontWeight(FontWeight.Bold)
// 标题输入
TextInput({ placeholder: '输入任务标题' })
.width('90%')
.onChange((value) => { this.formData.title = value; })
// 日期选择(CalendarPickerDialog)
Button(`日期:${this.formatDate(this.formData.date)}`)
.width('90%')
.onClick(() => {
CalendarPickerDialog.show({
selected: this.formData.date,
acceptButtonStyle: {
fontColor: '#007dff',
fontSize: '16fp',
backgroundColor: '#f0f6ff',
borderRadius: 10
},
cancelButtonStyle: {
fontColor: '#666666',
fontSize: '16fp',
backgroundColor: '#f7f7f7',
borderRadius: 10
},
onAccept: (date: Date) => {
this.formData.date = date;
}
});
})
// 优先级选择(showActionMenu)
Button(`优先级:${this.formData.priority}`)
.width('90%')
.onClick(() => {
const promptAction: PromptAction = this.getUIContext().getPromptAction();
promptAction.showActionMenu({
title: '选择优先级',
buttons: [
{ text: '高', color: '#ff4444' },
{ text: '中', color: '#ff9800' },
{ text: '低', color: '#4caf50' }
]
}).then(data => {
const priorities = ['high', 'medium', 'low'];
this.formData.priority = priorities[data.index];
});
})
// 提交按钮
Button('提交任务')
.width('90%')
.backgroundColor('#007dff')
.fontColor(Color.White)
.onClick(() => {
this.confirmSubmit();
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
// 提交确认流程
private confirmSubmit(): void {
if (!this.formData.title.trim()) {
// 空标题:用 showDialog 提示
const promptAction: PromptAction = this.getUIContext().getPromptAction();
promptAction.showDialog({
title: '提示',
message: '请输入任务标题',
buttons: [{ text: '知道了', color: '#007dff' }]
});
return;
}
// 确认提交:用 AlertDialog
this.getUIContext().showAlertDialog({
title: '确认提交',
message: `标题:${this.formData.title}\n日期:${this.formatDate(this.formData.date)}\n优先级:${this.formData.priority}`,
autoCancel: true,
primaryButton: {
value: '提交',
fontColor: '#007dff',
action: () => { this.doSubmit(); }
},
secondaryButton: {
value: '取消',
fontColor: '#666666',
action: () => {}
},
onDidAppear: () => {
console.info('确认弹窗已显示');
},
onDidDisappear: () => {
console.info('确认弹窗已关闭');
}
});
}
// 执行提交
private async doSubmit(): Promise<void> {
try {
await this.submitToServer(this.formData);
// 成功:非模态提示
this.getUIContext().showAlertDialog({
title: '提交成功',
message: '任务已创建',
isModal: false,
buttons: [{ text: '知道了', color: '#007dff' }]
});
} catch (e) {
// 失败:ActionSheet 提供重试选项
this.getUIContext().showActionSheet({
title: '提交失败',
message: '网络异常,请选择操作',
confirm: {
value: '重试',
fontColor: '#007dff',
action: () => { this.doSubmit(); }
},
cancel: () => {},
sheets: [
{ title: '保存为草稿', action: () => { this.saveDraft(); } }
]
});
}
}
private formatDate(date: Date): string {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
}
private async submitToServer(data: FormData): Promise<void> { /* ... */ }
private saveDraft(): void { /* ... */ }
}
interface FormData {
title: string;
date: Date;
priority: string;
}
这个示例展示了如何在一个完整的业务流程中,根据不同场景选择不同的弹窗类型:
| 步骤 | 弹窗类型 | 选择原因 |
|---|---|---|
| 校验失败提示 | showDialog | 简单信息提示,不需要警告感 |
| 确认提交 | AlertDialog | 需要用户确认不可逆操作 |
| 提交成功 | AlertDialog(非模态) | 不打断用户,温和告知 |
| 提交失败 | ActionSheet | 需要提供多个操作选项(重试/保存草稿) |
| 日期选择 | CalendarPickerDialog | 日历视图最适合日期选择 |
| 优先级选择 | showActionMenu | 底部菜单,快速选择 |
十一、避坑速查表
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 异步回调里弹窗报错 | UI 上下文不明确 | 提前在组件内获取 UIContext 并保存 |
| CalendarPickerDialog 在 Worker 里用不了 | 依赖 UI 执行上下文 | 改用 DatePickerDialog 或回到主线程调用 |
| 弹窗内容文字无法自定义颜色 | 固定样式弹窗不支持 | 改用自定义弹出框 |
| 多个弹窗同时弹出层级混乱 | 后弹出的层级更高 | 用 onDidDisappear 串联弹窗,等前一个消失再弹下一个 |
| 非模态弹窗背景还能点击 | 这是预期行为 | 如果需要阻断交互,用模态模式 |
| title 太长被截断 | 字体最大放大倍数为 2 | 精简标题文字,详细内容放 message |
| showActionMenu 超过 6 个选项 | 体验下降 | 改用 ActionSheet 或自定义弹窗 |
| 生命周期回调不触发 | API 版本低于 19 | 升级到 API 19+ |
| acceptButtonStyle 的 primary 不生效 | 两个按钮的 primary 都设了 true | 只能有一个为 true |
十二、选型决策树
你需要弹什么?
├── 只是提示用户一句话?
│ └── showDialog(简单信息)
│ └── 需要警示感?→ AlertDialog
│
├── 给用户提供几个操作选项?
│ ├── 2-4 个选项 → showActionMenu(底部菜单)
│ ├── 需要标题+消息+操作 → ActionSheet(列表选择)
│ └── 操作有严重后果 → AlertDialog(警告确认)
│
├── 需要用户选择日期/时间/文本?
│ ├── 需要日历视图 → CalendarPickerDialog
│ ├── 需要滑动选择 → DatePickerDialog / TimePickerDialog / TextPickerDialog
│ └── 需要在异步回调中调用 → 用 UIContext 版本,不用 CalendarPickerDialog
│
├── 需要不打断用户操作?
│ └── isModal: false(showDialog / AlertDialog / ActionSheet / showActionMenu)
│
└── 需要完全自定义弹窗内容?
└── 固定样式弹窗做不到 → 使用全局自定义弹出框或基础自定义弹出框
总结
固定样式弹出框的设计理念是"够用就好"。它们牺牲了自定义自由度,换取了开发效率和视觉一致性。在 80% 的弹窗需求场景下,固定样式弹窗都是最优选择。
但当你需要自定义内容区的字体颜色、插入图片、嵌入复杂表单时,不要和固定样式弹窗较劲——它的设计边界就是"只填内容,不管布局"。这时候请果断切换到自定义弹出框方案。
记住一个原则:弹窗的类型选择,应该由内容决定,而不是由开发者的偏好决定。 需要警告的就 AlertDialog,需要选项的就 showActionMenu,需要日期的就 CalendarPickerDialog。选对了类型,你的弹窗就已经成功了一半。
如果这篇文章对你有帮助,建议收藏。弹窗是应用开发中高频使用的组件,选对类型、用对参数、避开陷阱,能显著提升开发效率。
更多推荐




所有评论(0)