HarmonyOS 鸿蒙Next中如何实现优雅的表单验证?

HarmonyOS 鸿蒙Next中如何实现优雅的表单验证?

  1. 如何实现优雅的表单验证?
  2. 如何处理各类用户输入?
4 回复

代码实现

/**
 * 表单验证工具类
 */
export class FormValidator {
  /**
   * 验证金额
   */
  public static validateAmount(amount: string): { valid: boolean; message: string } {
    if (!amount || amount.trim() === '') {
      return { valid: false, message: '请输入金额' };
    }

    const num = parseFloat(amount);
    if (isNaN(num)) {
      return { valid: false, message: '请输入有效的数字' };
    }

    if (num <= 0) {
      return { valid: false, message: '金额必须大于0' };
    }

    if (num > 999999) {
      return { valid: false, message: '金额不能超过999999' };
    }

    // 检查小数位数
    const decimalPart = amount.split('.')[1];
    if (decimalPart && decimalPart.length > 2) {
      return { valid: false, message: '最多支持2位小数' };
    }

    return { valid: true, message: '' };
  }

  /**
   * 验证姓名
   */
  public static validateName(name: string): { valid: boolean; message: string } {
    if (!name || name.trim() === '') {
      return { valid: false, message: '请输入姓名' };
    }

    if (name.trim().length < 2) {
      return { valid: false, message: '姓名至少2个字符' };
    }

    if (name.trim().length > 20) {
      return { valid: false, message: '姓名不能超过20个字符' };
    }

    // 只允许中文、英文、数字
    const namePattern = /^[\u4e00-\u9fa5a-zA-Z0-9\s]+$/;
    if (!namePattern.test(name.trim())) {
      return { valid: false, message: '姓名只能包含中文、英文、数字' };
    }

    return { valid: true, message: '' };
  }

  /**
   * 验证手机号
   */
  public static validatePhone(phone: string): { valid: boolean; message: string } {
    if (!phone || phone.trim() === '') {
      return { valid: true, message: '' }; // 手机号可选
    }

    const phonePattern = /^1[3-9]\d{9}$/;
    if (!phonePattern.test(phone.trim())) {
      return { valid: false, message: '请输入正确的手机号' };
    }

    return { valid: true, message: '' };
  }

  /**
   * 验证备注长度
   */
  public static validateRemark(remark: string): { valid: boolean; message: string } {
    if (remark && remark.length > 200) {
      return { valid: false, message: '备注不能超过200字' };
    }

    return { valid: true, message: '' };
  }

  /**
   * 格式化金额输入
   */
  public static formatAmountInput(input: string): string {
    // 只保留数字和小数点
    let formatted = input.replace(/[^\d.]/g, '');
    
    // 只保留第一个小数点
    const parts = formatted.split('.');
    if (parts.length > 2) {
      formatted = parts[0] + '.' + parts.slice(1).join('');
    }
    
    // 限制小数位数为2位
    if (parts.length === 2 && parts[1].length > 2) {
      formatted = parts[0] + '.' + parts[1].substring(0, 2);
    }
    
    return formatted;
  }

  /**
   * 格式化手机号输入
   */
  public static formatPhoneInput(input: string): string {
    // 只保留数字
    let formatted = input.replace(/\D/g, '');
    
    // 限制11位
    if (formatted.length > 11) {
      formatted = formatted.substring(0, 11);
    }
    
    return formatted;
  }
}

/**
 * 添加记录页面 - 完整表单验证示例
 */
@Entry
@Component
struct AddRecordPage {
  // 表单字段
  @State amount: string = '';
  @State personName: string = '';
  @State phone: string = '';
  @State location: string = '';
  @State remark: string = '';
  
  // 验证错误信息
  @State amountError: string = '';
  @State nameError: string = '';
  @State phoneError: string = '';
  @State remarkError: string = '';
  
  // 表单状态
  @State formValid: boolean = false;
  @State submitDisabled: boolean = true;
  @State loading: boolean = false;

  /**
   * 金额输入变化
   */
  private onAmountChange(value: string) {
    // 格式化输入
    const formatted = FormValidator.formatAmountInput(value);
    this.amount = formatted;
    
    // 实时验证
    const result = FormValidator.validateAmount(formatted);
    this.amountError = result.message;
    
    // 更新表单状态
    this.updateFormValidity();
  }

  /**
   * 姓名输入变化
   */
  private onNameChange(value: string) {
    this.personName = value;
    
    // 实时验证
    const result = FormValidator.validateName(value);
    this.nameError = result.message;
    
    this.updateFormValidity();
  }

  /**
   * 手机号输入变化
   */
  private onPhoneChange(value: string) {
    // 格式化输入
    const formatted = FormValidator.formatPhoneInput(value);
    this.phone = formatted;
    
    // 实时验证
    const result = FormValidator.validatePhone(formatted);
    this.phoneError = result.message;
    
    this.updateFormValidity();
  }

  /**
   * 备注输入变化
   */
  private onRemarkChange(value: string) {
    this.remark = value;
    
    // 实时验证
    const result = FormValidator.validateRemark(value);
    this.remarkError = result.message;
    
    this.updateFormValidity();
  }

  /**
   * 更新表单有效性
   */
  private updateFormValidity() {
    const amountValid = FormValidator.validateAmount(this.amount).valid;
    const nameValid = FormValidator.validateName(this.personName).valid;
    const phoneValid = FormValidator.validatePhone(this.phone).valid;
    const remarkValid = FormValidator.validateRemark(this.remark).valid;
    
    this.formValid = amountValid && nameValid && phoneValid && remarkValid;
    this.submitDisabled = !this.formValid;
  }

  /**
   * 提交表单
   */
  private async submitForm() {
    // 最终验证
    if (!this.validateForm()) {
      promptAction.showToast({
        message: '请检查表单填写',
        duration: 2000
      });
      return;
    }

    try {
      this.loading = true;
      
      // 保存数据逻辑
      await this.saveRecord();
      
      promptAction.showToast({
        message: '保存成功',
        duration: 2000
      });
      
      setTimeout(() => {
        router.back();
      }, 1000);
    } catch (error) {
      promptAction.showToast({
        message: '保存失败',
        duration: 2000
      });
    } finally {
      this.loading = false;
    }
  }

  /**
   * 验证整个表单
   */
  private validateForm(): boolean {
    // 验证金额
    const amountResult = FormValidator.validateAmount(this.amount);
    if (!amountResult.valid) {
      this.amountError = amountResult.message;
      return false;
    }

    // 验证姓名
    const nameResult = FormValidator.validateName(this.personName);
    if (!nameResult.valid) {
      this.nameError = nameResult.message;
      return false;
    }

    // 验证手机号
    const phoneResult = FormValidator.validatePhone(this.phone);
    if (!phoneResult.valid) {
      this.phoneError = phoneResult.message;
      return false;
    }

    // 验证备注
    const remarkResult = FormValidator.validateRemark(this.remark);
    if (!remarkResult.valid) {
      this.remarkError = remarkResult.message;
      return false;
    }

    return true;
  }

  private async saveRecord() {
    // 保存逻辑
  }

  build() {
    Column() {
      // 导航栏
      this.buildHeader()

      // 表单内容
      Scroll() {
        Column() {
          // 金额输入
          this.buildAmountInput()

          // 姓名输入
          this.buildNameInput()

          // 手机号输入
          this.buildPhoneInput()

          // 地点输入
          this.buildLocationInput()

          // 备注输入
          this.buildRemarkInput()
        }
        .padding(16)
      }
      .layoutWeight(1)

      // 提交按钮
      this.buildSubmitButton()
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  @Builder
  buildHeader() {
    Row() {
      Image($r('app.media.ic_back'))
        .width(24)
        .height(24)
        .onClick(() => router.back())

      Text('添加记录')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .margin({ left: 16 })
    }
    .width('100%')
    .height(60)
    .padding({ left: 20, right: 20 })
    .backgroundColor('#FA8C16')
  }

  /**
   * 金额输入框
   */
  @Builder
  buildAmountInput() {
    Column() {
      Row() {
        Text('金额')
          .fontSize(16)
          .fontColor('#262626')
        
        Text('*')
          .fontSize(16)
          .fontColor('#FF4D4F')
          .margin({ left: 4 })
      }
      .margin({ bottom: 8 })

      TextInput({ text: this.amount, placeholder: '请输入金额' })
        .type(InputType.Number)
        .fontSize(16)
        .placeholderColor('#BFBFBF')
        .backgroundColor('#FFFFFF')
        .borderRadius(8)
        .padding({ left: 12, right: 12 })
        .height(48)
        .onChange((value: string) => {
          this.onAmountChange(value);
        })
        .onSubmit(() => {
          // 回车时验证
          const result = FormValidator.validateAmount(this.amount);
          this.amountError = result.message;
        })

      // 错误提示
      if (this.amountError) {
        Text(this.amountError)
          .fontSize(12)
          .fontColor('#FF4D4F')
          .margin({ top: 4 })
      }

      // 快捷金额
      this.buildQuickAmounts()
    }
    .width('100%')
    .alignItems(HorizontalAlign.Start)
    .margin({ bottom: 16 })
  }

  /**
   * 快捷金额选择
   */
  @Builder
  buildQuickAmounts() {
    Row() {
      ForEach([100, 200, 500, 1000], (amount: number) => {
        Button(amount.toString())
          .fontSize(14)
          .fontColor('#595959')
          .backgroundColor('#F5F5F5')
          .borderRadius(16)
          .padding({ left: 16, right: 16, top: 6, bottom: 6 })
          .onClick(() => {
            this.amount = amount.toString();
            this.onAmountChange(this.amount);
          })
      })
    }
    .width('100%')
    .justifyContent(FlexAlign.SpaceBetween)
    .margin({ top: 12 })
  }

  /**
   * 姓名输入框
   */
  @Builder
  buildNameInput() {
    Column() {
      Row() {
        Text('姓名')
          .fontSize(16)
          .fontColor('#262626')
        
        Text('*')
          .fontSize(16)
          .fontColor('#FF4D4F')
          .margin({ left: 4 })
      }
      .margin({ bottom: 8 })

      TextInput({ text: this.personName, placeholder: '请输入姓名' })
        .fontSize(16)
        .placeholderColor('#BFBFBF')
        .backgroundColor('#FFFFFF')
        .borderRadius(8)
        .padding({ left: 12, right: 12 })
        .height(48)
        .maxLength(20)
        .onChange((value: string) => {
          this.onNameChange(value);
        })

      if (this.nameError) {
        Text(this.nameError)
          .fontSize(12)
          .fontColor('#FF4D4F')
          .margin({ top: 4 })
      }
    }
    .width('100%')
    .alignItems(HorizontalAlign.Start)
    .margin({ bottom: 16 })
  }

  /**
   * 手机号输入框
   */
  @Builder
  buildPhoneInput() {
    Column() {
      Text('手机号')
        .fontSize(16)
        .fontColor('#262626')
        .margin({ bottom: 8 })

      TextInput({ text: this.phone, placeholder: '请输入手机号(可选)' })
        .type(InputType.PhoneNumber)
        .fontSize(16)
        .placeholderColor('#BFBFBF')
        .backgroundColor('#FFFFFF')
        .borderRadius(8)
        .padding({ left: 12, right: 12 })
        .height(48)
        .maxLength(11)
        .onChange((value: string) => {
          this.onPhoneChange(value);
        })

      if (this.phoneError) {
        Text(this.phoneError)
          .fontSize(12)
          .fontColor('#FF4D4F')
          .margin({ top: 4 })
      }
    }
    .width('100%')
    .alignItems(HorizontalAlign.Start)
    .margin({ bottom: 16 })
  }

  /**
   * 地点输入框
   */
  @Builder
  buildLocationInput() {
    Column() {
      Text('地点')
        .fontSize(16)
        .fontColor('#262626')
        .margin({ bottom: 8 })

      TextInput({ text: this.location, placeholder: '请输入地点(可选)' })
        .fontSize(16)
        .placeholderColor('#BFBFBF')
        .backgroundColor('#FFFFFF')
        .borderRadius(8)
        .padding({ left: 12, right: 12 })
        .height(48)
        .onChange((value: string) => {
          this.location = value;
        })
    }
    .width('100%')
    .alignItems(HorizontalAlign.Start)
    .margin({ bottom: 16 })
  }

  /**
   * 备注输入框
   */
  @Builder
  buildRemarkInput() {
    Column() {
      Row() {
        Text('备注')
          .fontSize(16)
          .fontColor('#262626')
        
        Text(`${this.remark.length}/200`)
          .fontSize(12)
          .fontColor('#8C8C8C')
          .margin({ left: 8 })
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)
      .margin({ bottom: 8 })

      TextArea({ text: this.remark, placeholder: '请输入备注(可选)' })
        .fontSize(16)
        .placeholderColor('#BFBFBF')
        .backgroundColor('#FFFFFF')
        .borderRadius(8)
        .padding(12)
        .height(100)
        .maxLength(200)
        .onChange((value: string) => {
          this.onRemarkChange(value);
        })

      if (this.remarkError) {
        Text(this.remarkError)
          .fontSize(12)
          .fontColor('#FF4D4F')
          .margin({ top: 4 })
      }
    }
    .width('100%')
    .alignItems(HorizontalAlign.Start)
    .margin({ bottom: 16 })
  }

  /**
   * 提交按钮
   */
  @Builder
  buildSubmitButton() {
    Button(this.loading ? '保存中...' : '保存')
      .width('90%')
      .height(48)
      .fontSize(16)
      .fontColor('#FFFFFF')
      .backgroundColor(this.submitDisabled ? '#D9D9D9' : '#FA8C16')
      .borderRadius(24)
      .margin({ bottom: 16 })
      .enabled(!this.submitDisabled && !this.loading)
      .onClick(() => {
        this.submitForm();
      })
  }
}

核心技术点

1. TextInput类型

.type(InputType.Number)     // 数字键盘
.type(InputType.PhoneNumber) // 电话键盘
.type(InputType.Email)       // 邮箱键盘

2. 输入限制

.maxLength(20)          // 最大长度
.onChange((value) => {}) // 实时监听
.onSubmit(() => {})     // 回车提交

3. 正则表达式验证

// 手机号
/^1[3-9]\d{9}$/

// 姓名(中英文数字)
/^[\u4e00-\u9fa5a-zA-Z0-9\s]+$/

// 金额(最多2位小数)
/^\d+(\.\d{1,2})?$/

最佳实践

1. 实时验证 + 提交验证

// 实时验证: 输入时提示
.onChange((value) => {
  this.validate(value);
})

// 提交验证: 最终检查
submitForm() {
  if (!this.validateForm()) {
    return;
  }
}

2. 输入格式化

// 自动格式化,提升用户体验
formatAmountInput(input: string): string {
  return input.replace(/[^\d.]/g, '');
}

3. 错误提示

if (this.amountError) {
  Text(this.amountError)
    .fontSize(12)
    .fontColor('#FF4D4F')
}

4. 按钮状态控制

.enabled(!this.submitDisabled && !this.loading)
.backgroundColor(this.submitDisabled ? '#D9D9D9' : '#FA8C16')

更多关于HarmonyOS 鸿蒙Next中如何实现优雅的表单验证?的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


如何将 HTML 转换为 Markdown

什么是 HTML 到 Markdown 转换器?

HTML 到 Markdown 转换器是一种工具,可将 HTML(超文本标记语言)代码转换为 Markdown 格式。Markdown 是一种轻量级标记语言,使用纯文本格式编写文档,然后可转换为 HTML。这种转换对于希望以更易读、更易写的格式呈现 HTML 内容的用户特别有用。

为什么使用 HTML 到 Markdown 转换器?

使用 HTML 到 Markdown 转换器有几个好处:

  1. 简化内容创建:Markdown 语法简单直观,使编写和格式化文档变得容易,无需复杂的 HTML 标签。
  2. 提高可读性:Markdown 文件是纯文本,易于阅读和编辑,即使在没有渲染的情况下也是如此。
  3. 跨平台兼容性:Markdown 被许多平台支持,包括 GitHub、Reddit 和各种博客平台,使其成为创建可跨不同环境移植的内容的理想选择。
  4. 高效转换:转换器可以快速准确地将现有的 HTML 内容转换为 Markdown,节省手动重写的时间和精力。

如何使用 HTML 到 Markdown 转换器?

使用 HTML 到 Markdown 转换器通常很简单:

  1. 输入 HTML 代码:将要转换的 HTML 代码粘贴到转换器的输入区域。
  2. 开始转换:点击转换按钮,工具将处理 HTML 代码并生成相应的 Markdown。
  3. 复制输出:转换完成后,将 Markdown 输出复制到剪贴板或直接下载。

转换示例

以下是一个简单的 HTML 代码片段及其转换后的 Markdown 示例:

HTML 代码:

<h1>欢迎来到我的博客</h1>
<p>这是我的第一篇 <strong>Markdown</strong> 文章。</p>
<ul>
    <li>项目一</li>
    <li>项目二</li>
</ul>

转换后的 Markdown:

欢迎来到我的博客

这是我的第一篇 Markdown 文章。

  • 项目一
  • 项目二

## 结论

HTML 到 Markdown 转换器是简化内容创建和格式化的宝贵工具。通过将 HTML 转换为 Markdown,用户可以享受更简单、更易读的语法,同时保持跨各种平台的内容兼容性。无论您是博主、开发人员还是内容创建者,使用此工具都可以增强您的工作流程并提高生产力。

在鸿蒙Next中,可通过ArkTS声明式UI和状态管理实现表单验证。使用@State@Prop等装饰器管理表单数据与验证状态,结合条件渲染动态显示错误信息。推荐利用内置校验库或自定义校验函数,通过正则表达式或逻辑判断验证输入。表单提交时,统一检查所有字段的验证状态,确保数据合规。

在HarmonyOS Next中,实现优雅的表单验证可以充分利用其声明式UI和状态管理能力,核心在于将验证逻辑与UI组件解耦,并实时反馈。

1. 核心思路:状态驱动与响应式 将每个表单字段的“值”和“错误信息”定义为状态变量(使用@State@Link装饰器)。当值变化时,触发验证函数更新错误状态,UI自动刷新。

2. 实现方案示例:

  • 定义状态:
    @State username: string = ''
    @State usernameError: string = ''
    
  • 绑定与验证: 在文本输入框的onChange事件中,同步值并调用验证器。
    TextInput({ placeholder: '请输入用户名' })
        .value(this.username)
        .onChange((value: string) => {
            this.username = value
            this.validateUsername(value) // 验证函数
        })
    
    下方显示错误信息:
    if (this.usernameError) {
        Text(this.usernameError)
            .fontColor(Color.Red)
    }
    
  • 验证函数: 集中管理校验逻辑,返回错误提示。
    validateUsername(value: string): void {
        if (!value) {
            this.usernameError = '用户名不能为空'
        } else if (value.length < 3) {
            this.usernameError = '用户名至少3位'
        } else {
            this.usernameError = '' // 验证通过,清空错误
        }
    }
    

3. 处理各类输入:

  • 文本/密码: 使用TextInput,通过onChangeonEditChange捕获输入。
  • 选择器(单选/多选): 使用RadioCheckboxSelect组件,绑定其选中状态到变量。
  • 日期/时间: 使用DatePickerTimePicker,监听其onChange事件获取值。
  • 表单提交: 在提交按钮的onClick事件中,遍历执行所有字段的最终验证,只有所有error状态均为空时才提交数据。

4. 进阶优化:

  • 封装验证器: 将正则校验、必填校验等抽离为纯函数工具类。
  • 防抖验证: 对频繁触发的输入(如搜索框),使用debounce函数避免过度验证。
  • ArkUI组件组合: 可以将输入框、错误提示封装为一个自定义的可复用组件。

这种方式逻辑清晰,响应迅速,符合HarmonyOS Next的开发范式。

回到顶部