[鸿蒙2025领航者闯关] 表单验证与用户输入处理最佳实践

问题描述

在鸿蒙应用开发中,如何实现优雅的表单验证?如何处理各类用户输入?本文以添加记录页面为例,讲解完整的表单处理方案。

技术要点

  • TextInput 输入控制
  • 实时验证与提示
  • 正则表达式验证
  • 输入格式化
  • 表单状态管理

完整实现代码

复制代码
/**
 * 表单验证工具类
 */
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')

总结

完整的表单验证方案:

  • ✅ 实时验证与提示
  • ✅ 输入格式化
  • ✅ 正则表达式验证
  • ✅ 表单状态管理
  • ✅ 用户体验优化

相关资源

相关推荐
高频交易dragon2 小时前
5分钟和30分钟联立进行缠论信号分析
开发语言·python
ULTRA??2 小时前
C/C++函数指针
c语言·开发语言·c++
0思必得02 小时前
[Web自动化] 开发者工具应用(Application)面板
运维·前端·python·自动化·web自动化·开发者工具
m0_740043732 小时前
Vue Router中获取路由参数d两种方式:$route.query和$route.params
前端·javascript·vue.js
还没想好取啥名2 小时前
C++11新特性(一)——自动类型推导
开发语言·c++·stl
风止何安啊2 小时前
Event Loop 教你高效 “划水”:JS 单线程的“摸鱼”指南
前端·javascript·面试
xiaozi41202 小时前
Ruey S. Tsay《时间序列分析》Python实现笔记:综合与应用
开发语言·笔记·python·机器学习
@菜菜_达2 小时前
goldenLayout布局
前端·javascript
小飞侠在吗2 小时前
vue 生命周期
前端·javascript·vue.js