HarmonyOS ArkUI Dialog 复用 用 @CustomDialog + enum type 做多类型统一管理的做法

鸿蒙 ArkUI 的 Dialog 系统提供两类:promptAction.showDialog 简单 + 系统外观;@CustomDialog 完全自定义。独立 app 一般两套都要------简单确认用前者,复杂业务(选择器 / 表单)用后者。做了几个 app 后发现 @CustomDialog 很容易写成 N 个独立组件 ,可复用度低。记录一下 用 enum type 做多类型统一管理 的 pattern,顺便分享给有需要的朋友。

一、@CustomDialog 的基本用法

typescript 复制代码
@CustomDialog
export struct ConfirmDialog {
  controller?: CustomDialogController
  @Prop title: string = ''
  @Prop message: string = ''
  onConfirm: () => void = () => {}

  build() {
    Column() {
      Text(this.title).fontSize(18).fontWeight(FontWeight.Bold)
      Text(this.message).fontSize(14).margin({ top: 12 })
      Row() {
        Button('取消').onClick(() => this.controller?.close())
        Button('确认').onClick(() => {
          this.onConfirm()
          this.controller?.close()
        })
      }
      .margin({ top: 16 })
    }
    .padding(20)
    .backgroundColor(Color.White)
    .borderRadius(16)
  }
}

// 使用
@Entry @Component
struct Page {
  private dialogController: CustomDialogController = new CustomDialogController({
    builder: ConfirmDialog({
      title: '删除确认',
      message: '确定吗?',
      onConfirm: () => this.doDelete(),
    }),
    alignment: DialogAlignment.Center,
  })

  build() {
    Button('删除').onClick(() => this.dialogController.open())
  }
}

几个点:

controller :dialog 自己通过 controller.close() 关闭。

@Prop 传入数据 :title / message / onConfirm。

CustomDialogController 在父组件声明 :open() 显示 dialog。

二、多个 dialog 的膨胀问题

随着 app 变复杂:

erlang 复制代码
ConfirmDialog.ets
DatePickerDialog.ets
CategoryPickerDialog.ets
InputDialog.ets
ActionSheetDialog.ets
InfoDialog.ets
...

每个 Dialog 一个组件 + 一个 controller + 一个 open 。页面里N 个 controller 字段 。代码膨胀。

问题:

  • 样式不统一(每个 Dialog 独立实现 border / padding)
  • state 分散(每个 controller 独立管)
  • 跨组件打开 Dialog 麻烦(EventBus 或 prop drilling)

三、统一 Dialog Manager pattern

思路 :一个 CustomDialog + enum type 区分内容。

typescript 复制代码
export enum DialogType {
  None = 'none',
  Confirm = 'confirm',
  Input = 'input',
  DatePicker = 'date',
  CategoryPicker = 'category',
  Info = 'info',
}

export interface DialogConfig {
  type: DialogType
  title: string
  message?: string
  defaultValue?: string
  options?: string[]
  onConfirm?: (value: string) => void
  onCancel?: () => void
}

AppStorage 存当前 dialog:

typescript 复制代码
// 全局
AppStorage.setOrCreate('dialog.current', null as DialogConfig | null)

// 任何地方触发
AppStorage.set('dialog.current', {
  type: DialogType.Confirm,
  title: '删除',
  message: '确定吗',
  onConfirm: () => doDelete(),
})

// 关闭
AppStorage.set('dialog.current', null)

四、统一的 UniversalDialog 组件

typescript 复制代码
@CustomDialog
export struct UniversalDialog {
  controller?: CustomDialogController
  @StorageLink('dialog.current') config: DialogConfig | null = null

  build() {
    Column() {
      if (this.config) {
        Text(this.config.title).fontSize(18).fontWeight(FontWeight.Bold)
        this.renderBody()
        this.renderActions()
      }
    }
    .padding(20)
    .backgroundColor(Color.White)
    .borderRadius(16)
    .width(300)
  }

  @Builder
  renderBody() {
    if (this.config?.type === DialogType.Confirm) {
      Text(this.config.message ?? '').fontSize(14)
    } else if (this.config?.type === DialogType.Input) {
      TextInput({ text: this.config.defaultValue ?? '' })
        .onChange((v) => { this.inputValue = v })
    } else if (this.config?.type === DialogType.CategoryPicker) {
      ForEach(this.config.options ?? [], (opt: string) => {
        Button(opt).onClick(() => {
          this.config?.onConfirm?.(opt)
          this.close()
        })
      })
    }
    // ... 其他类型
  }

  @Builder
  renderActions() {
    Row() {
      Button('取消').onClick(() => this.close())
      if (this.config?.type !== DialogType.CategoryPicker) {
        Button('确认').onClick(() => {
          this.config?.onConfirm?.(this.inputValue)
          this.close()
        })
      }
    }
  }

  @State private inputValue: string = ''

  private close(): void {
    this.controller?.close()
    AppStorage.set('dialog.current', null)
  }
}

所有类型 Dialog 用一个组件 。renderBody 根据 type 分发。

五、顶层挂一个 UniversalDialog

Index.ets:

typescript 复制代码
@Entry @Component
struct Index {
  @StorageLink('dialog.current') dialogConfig: DialogConfig | null = null
  private universalDialog: CustomDialogController = new CustomDialogController({
    builder: UniversalDialog(),
    alignment: DialogAlignment.Center,
    autoCancel: true,
  })

  @Watch('onDialogConfigChange')
  dialogConfigWatch: boolean = false

  private onDialogConfigChange(): void {
    if (this.dialogConfig !== null) {
      this.universalDialog.open()
    } else {
      this.universalDialog.close()
    }
  }

  build() {
    // 整个 app 的 UI
    // UniversalDialog 自动打开 / 关闭
  }
}

流程:

  1. 任何地方 set AppStorage 的 dialog.current
  2. @Watch 响应 → open universalDialog
  3. Dialog 内部根据 type render
  4. 用户确认 / 取消 → set null
  5. @Watch 响应 → close universalDialog

六、各处调用的简化 API

typescript 复制代码
export class DialogService {
  static confirm(title: string, message: string, onOk: () => void): void {
    AppStorage.set('dialog.current', {
      type: DialogType.Confirm,
      title,
      message,
      onConfirm: () => onOk(),
    })
  }

  static input(title: string, defaultValue: string, onInput: (v: string) => void): void {
    AppStorage.set('dialog.current', {
      type: DialogType.Input,
      title,
      defaultValue,
      onConfirm: (v) => onInput(v),
    })
  }

  static categoryPicker(title: string, options: string[], onPick: (v: string) => void): void {
    AppStorage.set('dialog.current', {
      type: DialogType.CategoryPicker,
      title,
      options,
      onConfirm: (v) => onPick(v),
    })
  }
}

// 使用
DialogService.confirm('删除', '确定吗?', () => this.doDelete())
DialogService.input('修改名字', this.name, (v) => this.name = v)
DialogService.categoryPicker('选分类', ['工作', '生活', '娱乐'], (v) => this.category = v)

调用方的代码 :一行。

不用 :在页面组件声明 CustomDialogController。

七、Dialog 复用 vs 独立的权衡

复用(本案):

  • 优点:调用简单,样式统一,state 集中
  • 缺点:UniversalDialog 组件复杂(switch 多个 type)

独立:

  • 优点:每个 Dialog 自己 clean
  • 缺点:多处调用 麻烦 ,样式各做各的

独立开发者的建议:

  • 5 种以内 dialog → 独立
  • 5-15 种 → 复用(本案)
  • 15+ 种 → 可能你的 dialog 太多了,考虑专门的页面代替

八、Dialog 的 onDismiss 和 autoCancel

typescript 复制代码
new CustomDialogController({
  builder: UniversalDialog(),
  autoCancel: true,            // 点外部关闭
  onCancel: () => {
    AppStorage.set('dialog.current', null)
  },
  cornerRadius: 16,
  alignment: DialogAlignment.Center,
})

autoCancel: true :点 dialog 外部关闭。

  • 用户不想强行限制操作
  • 配合onCancel 清 AppStorage

autoCancel: false :只能通过按钮关闭。

  • 重要确认 (如"真的要删除所有数据吗")用
  • 防止误触

独立开发者的建议 :95% 的 Dialog 用 autoCancel: true 。强制交互只在高危操作用。

踩坑清单

  • 简单确认用 promptAction.showDialog(系统外观)
  • 复杂业务用 @CustomDialog
  • 多类型 Dialog 用 enum + 统一组件
  • AppStorage 存当前 dialog config
  • 顶层挂一个 UniversalDialog
  • 调用方用 DialogService.confirm/input/picker(一行)
  • autoCancel: true 大部分场景
  • autoCancel: false 强制确认场景
  • renderBody / renderActions 分开 @Builder

尾声

Dialog 统一管理 是独立 app 的工程化基础设施 。早期不管,后期 N 个 controller 难受。

独立开发者的建议:

  • 项目早期定好 Dialog pattern
  • 5+ 种 Dialog 果断上统一管理
  • 调用方只能看到 DialogService.xxx(禁用裸 CustomDialogController)

对比:

  • React Native:react-native-modal + context
  • iOS:UIAlertController + 自定义 modal
  • Flutter:showDialog + custom builder
  • 鸿蒙:本案的 enum + AppStorage pattern

UI 组件复用 是独立开发者的生产力 。把 Dialog 做好了,一个 app 的调用方全部一行,心智清爽。

相关推荐
AiMan28 分钟前
豆包爱学还是不可以导出左侧所有对话,只能单容导出,方法不对吗
前端
AiMan1 小时前
Grok生成的内容能否下载吗?
前端
徐小夕1 小时前
JitArchives,一款基于JitWord协同AI文档打造的企业级电子档案系统
前端·vue.js·github
陈随易1 小时前
Node.js 2026上半年都有哪些性能提升?
前端·后端·程序员
墨林陌2 小时前
AI 热点日报(2026-11-09):苹果代理式Siri曝光,Anthropic推进IPO,编码智能体实测翻车
前端
vipxieliang2 小时前
JavaScript 迭代器与生成器:控制异步与惰性求值
前端·javascript
Hilaku2 小时前
Event Loop 这篇你看完就不用再看别的了!
前端·javascript·程序员
恋猫de小郭3 小时前
Flutter 下 AOT 一个你怎么都想不出来的 Bug ,JIT 正常的计算,在 AOT 居然算错了
android·前端·flutter