問題描述

應用中需要頻繁使用確認對話框、選擇器對話框等,如何封裝通用的 Dialog 組件避免重複代碼?如何實現優雅的回調處理?

關鍵字: CustomDialog、對話框封裝、組件複用、回調處理、UI 組件

解決方案

完整代碼

/**
 * 確認對話框
 */
@CustomDialog
export struct ConfirmDialog {
  controller: CustomDialogController;
  title: string = '提示';
  message: string = '';
  confirmText: string = '確定';
  cancelText: string = '取消';
  onConfirm?: () => void;
  onCancel?: () => void;
  
  build() {
    Column({ space: 16 }) {
      // 標題
      Text(this.title)
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .width('100%')
      
      // 消息內容
      Text(this.message)
        .fontSize(14)
        .fontColor('#666')
        .width('100%')
        .margin({ top: 8, bottom: 16 })
      
      // 按鈕組
      Row({ space: 12 }) {
        Button(this.cancelText)
          .fontSize(16)
          .backgroundColor('#f5f5f5')
          .fontColor('#333')
          .layoutWeight(1)
          .onClick(() => {
            this.controller.close();
            if (this.onCancel) {
              this.onCancel();
            }
          })
        
        Button(this.confirmText)
          .fontSize(16)
          .backgroundColor('#ff6b6b')
          .fontColor('#fff')
          .layoutWeight(1)
          .onClick(() => {
            this.controller.close();
            if (this.onConfirm) {
              this.onConfirm();
            }
          })
      }
      .width('100%')
    }
    .padding(20)
    .backgroundColor(Color.White)
    .borderRadius(12)
  }
}
​
/**
 * 單選對話框
 */
@CustomDialog
export struct SelectDialog {
  controller: CustomDialogController;
  title: string = '請選擇';
  options: string[] = [];
  selectedIndex: number = 0;
  onSelect?: (index: number, value: string) => void;
  
  @State currentIndex: number = 0;
  
  aboutToAppear() {
    this.currentIndex = this.selectedIndex;
  }
  
  build() {
    Column({ space: 12 }) {
      // 標題
      Text(this.title)
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .width('100%')
        .padding({ bottom: 12 })
      
      // 選項列表
      List({ space: 0 }) {
        ForEach(this.options, (option: string, index: number) => {
          ListItem() {
            Row() {
              Text(option)
                .fontSize(16)
                .fontColor(this.currentIndex === index ? '#ff6b6b' : '#333')
                .layoutWeight(1)
              
              if (this.currentIndex === index) {
                Text('✓')
                  .fontSize(18)
                  .fontColor('#ff6b6b')
              }
            }
            .width('100%')
            .padding(12)
            .backgroundColor(this.currentIndex === index ? '#fff5f5' : Color.White)
            .borderRadius(8)
            .onClick(() => {
              this.currentIndex = index;
            })
          }
        })
      }
      .height(Math.min(this.options.length * 48, 300))
      
      // 確定按鈕
      Button('確定')
        .width('100%')
        .backgroundColor('#ff6b6b')
        .fontColor('#fff')
        .margin({ top: 12 })
        .onClick(() => {
          this.controller.close();
          if (this.onSelect) {
            this.onSelect(this.currentIndex, this.options[this.currentIndex]);
          }
        })
    }
    .padding(20)
    .backgroundColor(Color.White)
    .borderRadius(12)
    .width('80%')
  }
}
​
/**
 * 輸入對話框
 */
@CustomDialog
export struct InputDialog {
  controller: CustomDialogController;
  title: string = '輸入';
  placeholder: string = '請輸入';
  defaultValue: string = '';
  inputType: InputType = InputType.Normal;
  maxLength: number = 50;
  onConfirm?: (value: string) => void;
  
  @State inputValue: string = '';
  
  aboutToAppear() {
    this.inputValue = this.defaultValue;
  }
  
  build() {
    Column({ space: 16 }) {
      // 標題
      Text(this.title)
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .width('100%')
      
      // 輸入框
      TextInput({ text: this.inputValue, placeholder: this.placeholder })
        .type(this.inputType)
        .maxLength(this.maxLength)
        .onChange((value: string) => {
          this.inputValue = value;
        })
        .width('100%')
        .padding(12)
        .borderRadius(8)
        .backgroundColor('#f5f5f5')
      
      // 按鈕組
      Row({ space: 12 }) {
        Button('取消')
          .fontSize(16)
          .backgroundColor('#f5f5f5')
          .fontColor('#333')
          .layoutWeight(1)
          .onClick(() => {
            this.controller.close();
          })
        
        Button('確定')
          .fontSize(16)
          .backgroundColor('#ff6b6b')
          .fontColor('#fff')
          .layoutWeight(1)
          .onClick(() => {
            this.controller.close();
            if (this.onConfirm) {
              this.onConfirm(this.inputValue);
            }
          })
      }
      .width('100%')
    }
    .padding(20)
    .backgroundColor(Color.White)
    .borderRadius(12)
  }
}
​
/**
 * 加載對話框
 */
@CustomDialog
export struct LoadingDialog {
  controller: CustomDialogController;
  message: string = '加載中...';
  
  build() {
    Column({ space: 16 }) {
      LoadingProgress()
        .width(50)
        .height(50)
        .color('#ff6b6b')
      
      Text(this.message)
        .fontSize(14)
        .fontColor('#666')
    }
    .padding(30)
    .backgroundColor(Color.White)
    .borderRadius(12)
  }
}

使用示例

@Entry
@Component
struct DemoPage {
  private confirmDialogController: CustomDialogController | null = null;
  private selectDialogController: CustomDialogController | null = null;
  private inputDialogController: CustomDialogController | null = null;
  private loadingDialogController: CustomDialogController | null = null;
  
  // 顯示確認對話框
  showConfirmDialog() {
    this.confirmDialogController = new CustomDialogController({
      builder: ConfirmDialog({
        title: '刪除確認',
        message: '確定要刪除這條記錄嗎?此操作不可恢復。',
        confirmText: '刪除',
        cancelText: '取消',
        onConfirm: () => {
          console.log('用户點擊了刪除');
          this.deleteRecord();
        },
        onCancel: () => {
          console.log('用户取消了刪除');
        }
      }),
      autoCancel: true,
      alignment: DialogAlignment.Center
    });
    this.confirmDialogController.open();
  }
  
  // 顯示選擇對話框
  showSelectDialog() {
    this.selectDialogController = new CustomDialogController({
      builder: SelectDialog({
        title: '選擇關係',
        options: ['朋友', '同事', '親戚', '同學', '其他'],
        selectedIndex: 0,
        onSelect: (index: number, value: string) => {
          console.log(`選擇了: ${value}`);
        }
      }),
      autoCancel: true,
      alignment: DialogAlignment.Center
    });
    this.selectDialogController.open();
  }
  
  // 顯示輸入對話框
  showInputDialog() {
    this.inputDialogController = new CustomDialogController({
      builder: InputDialog({
        title: '添加備註',
        placeholder: '請輸入備註內容',
        defaultValue: '',
        maxLength: 100,
        onConfirm: (value: string) => {
          console.log(`輸入內容: ${value}`);
        }
      }),
      autoCancel: true,
      alignment: DialogAlignment.Center
    });
    this.inputDialogController.open();
  }
  
  // 顯示加載對話框
  async showLoadingDialog() {
    this.loadingDialogController = new CustomDialogController({
      builder: LoadingDialog({
        message: '正在保存...'
      }),
      autoCancel: false,
      alignment: DialogAlignment.Center
    });
    this.loadingDialogController.open();
    
    // 模擬異步操作
    await this.saveData();
    
    // 關閉加載對話框
    this.loadingDialogController.close();
  }
  
  async deleteRecord() {
    // 刪除邏輯
  }
  
  async saveData() {
    // 保存邏輯
  }
  
  build() {
    Column({ space: 16 }) {
      Button('確認對話框').onClick(() => this.showConfirmDialog())
      Button('選擇對話框').onClick(() => this.showSelectDialog())
      Button('輸入對話框').onClick(() => this.showInputDialog())
      Button('加載對話框').onClick(() => this.showLoadingDialog())
    }
    .padding(20)
  }
}

原理解析

1. @CustomDialog 裝飾器

@CustomDialog
export struct ConfirmDialog {
  controller: CustomDialogController;
}
  • 標記為自定義對話框組件
  • 必須包含 controller 屬性
  • 通過 controller 控制顯示/隱藏

2. 回調函數傳遞

onConfirm?: () => void;
  • 使用可選屬性定義回調
  • 調用前檢查是否存在
  • 支持傳遞參數

3. @State 狀態管理

@State currentIndex: number = 0;
  • 對話框內部狀態
  • 響應用户交互
  • 觸發 UI 更新

最佳實踐

  1. 統一風格: 所有對話框使用相同的樣式和動畫
  2. 回調處理: 使用可選回調,調用前檢查
  3. 自動關閉: 設置 autoCancel: true 支持點擊外部關閉
  4. 內存管理: 對話框關閉後 controller 置 null
  5. 異步操作: 加載對話框配合 async/await 使用

避坑指南

  1. 忘記 close: 必須手動調用 controller.close()
  2. 重複打開: 打開前檢查 controller 是否已存在
  3. 內存泄漏: 組件銷燬時關閉所有對話框
  4. 回調丟失: 箭頭函數保持 this 指向
  5. 樣式覆蓋: 使用 width/height 限制對話框大小

效果展示

  • 確認對話框:標題 + 消息 + 雙按鈕
  • 選擇對話框:標題 + 列表 + 確定按鈕
  • 輸入對話框:標題 + 輸入框 + 雙按鈕
  • 加載對話框:加載動畫 + 提示文字

相關資源

  • 鴻蒙學習資源