#

适用版本: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 对象调用

适用于 showActionMenushowDialog。需要先从 UIContext 获取 PromptAction 对象:

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

// 在组件内部
const uiContext = this.getUIContext();
const promptAction: PromptAction = uiContext.getPromptAction();

// 然后通过 promptAction 调用
promptAction.showActionMenu({ /* 配置 */ });
promptAction showDialog({ /* 配置 */ });

路径二:通过 UIContext 对象直接调用

适用于 ActionSheetAlertDialogPickerDialog 系列。需要先获取 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 开始,showDialogActionSheetAlertDialog 支持四个生命周期回调。这是很多人忽略但非常实用的能力。

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:带标题和消息的操作列表

ActionSheetshowActionMenu 的区别在于: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 非模态:一个参数改变交互本质

showActionMenushowDialogActionSheetAlertDialog 都支持通过设置 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 则通过 acceptButtonStylecancelButtonStyle 实现按钮自定义。

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 的使用约束:

  1. 必须在 UI 上下文明确的地方调用。 不能在 Worker 线程、深层异步回调、应用未完成初始化时调用。如果需要在异步回调中使用,提前在 UI 上下文明确时触发。

  2. 不支持通过 UIContext 调用。 它是唯一直接通过 CalendarPickerDialog.show() 调用的弹窗。

  3. 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 按钮自定义样式的完整参数

acceptButtonStylecancelButtonStyle 支持的完整参数:

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。选对了类型,你的弹窗就已经成功了一半。


如果这篇文章对你有帮助,建议收藏。弹窗是应用开发中高频使用的组件,选对类型、用对参数、避开陷阱,能显著提升开发效率。

Logo

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

更多推荐