問題描述
在鴻蒙應用開發中,如何實現優雅的表單驗證?如何處理各類用户輸入?本文以添加記錄頁面為例,講解完整的表單處理方案。
技術要點
- 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')
總結
完整的表單驗證方案:
- ✅ 實時驗證與提示
- ✅ 輸入格式化
- ✅ 正則表達式驗證
- ✅ 表單狀態管理
- ✅ 用户體驗優化
相關資源
- 鴻蒙學習資源