【HarmonyOS】RN_of_HarmonyOS实战项目_输入验证提示

【HarmonyOS】RN of HarmonyOS实战项目:TextInput输入验证提示完整实现

项目概述:本文详细介绍在HarmonyOS平台上使用React Native实现完整的TextInput输入验证提示系统,涵盖实时验证、错误提示、密码强度检测等生产级功能,提供从设计模式到工程实践的完整解决方案。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

一、项目背景

在现代移动应用开发中,表单输入验证是用户交互的核心环节。良好的验证机制不仅能提升数据质量,还能优化用户体验。在HarmonyOS平台上实现完整的验证系统需要考虑:

  • 平台适配差异:鸿蒙系统在输入法管理、焦点切换、键盘弹出等行为上的独特性
  • 性能优化需求:频繁的状态更新需要配合鸿蒙的高效渲染管道
  • 用户体验设计:实时反馈、清晰的错误提示、流畅的交互体验
  • 无障碍访问支持:符合HarmonyOS的无障碍标准

二、技术架构

2.1 验证状态管理设计

采用类型安全的状态管理模式,每个输入字段维护独立的验证状态:

typescript 复制代码
type ValidationState = {
  value: string;           // 当前输入值
  isValid: boolean;        // 验证结果
  errorMessage: string;    // 错误提示信息
  isTouched: boolean;      // 是否已交互(用于延迟显示错误)
};

2.2 验证流程架构



用户输入
onChangeText触发
标记isTouched=true
执行验证规则
验证通过?
显示成功提示
显示错误信息
更新isValid状态
触发UI重渲染
应用视觉反馈

三、核心实现代码

3.1 验证规则工厂类

typescript 复制代码
/**
 * 验证规则工具类
 * 提供常用的验证规则生成函数
 *
 * @platform HarmonyOS 2.0+
 * @react-native 0.72+
 */
export class ValidationRules {
  /**
   * 必填验证
   */
  static required(message: string = '此字段不能为空') {
    return (value: string) => {
      if (!value || !value.trim()) {
        return message;
      }
      return '';
    };
  }

  /**
   * 长度验证
   */
  static length(min: number, max: number) {
    return (value: string) => {
      if (value.length < min) {
        return `至少需要${min}个字符`;
      }
      if (value.length > max) {
        return `不能超过${max}个字符`;
      }
      return '';
    };
  }

  /**
   * 邮箱格式验证
   */
  static email(message: string = '请输入有效的邮箱地址') {
    return (value: string) => {
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      if (!emailRegex.test(value)) {
        return message;
      }
      return '';
    };
  }

  /**
   * 手机号验证(支持中国大陆)
   */
  static phone(message: string = '请输入有效的手机号码') {
    return (value: string) => {
      const phoneRegex = /^1[3-9]\d{9}$/;
      if (!phoneRegex.test(value)) {
        return message;
      }
      return '';
    };
  }

  /**
   * 密码强度验证
   */
  static passwordStrength(minStrength: number = 3) {
    return (value: string) => {
      const checks = {
        hasUpperCase: /[A-Z]/.test(value),
        hasLowerCase: /[a-z]/.test(value),
        hasNumbers: /\d/.test(value),
        hasSpecialChar: /[!@#$%^&*(),.?":{}|<>]/.test(value),
      };

      const strength = Object.values(checks).filter(Boolean).length;

      if (strength < minStrength) {
        return `密码强度不足(当前:${strength}/4)`;
      }
      return '';
    };
  }

  /**
   * 组合多个验证规则
   */
  static compose(...rules: ((value: string) => string)[]) {
    return (value: string) => {
      for (const rule of rules) {
        const error = rule(value);
        if (error) {
          return error;
        }
      }
      return '';
    };
  }

  /**
   * 自定义正则验证
   */
  static pattern(regex: RegExp, message: string) {
    return (value: string) => {
      if (!regex.test(value)) {
        return message;
      }
      return '';
    };
  }
}

3.2 表单验证Hook

typescript 复制代码
/**
 * 表单验证Hook
 * 提供完整的表单状态管理和验证功能
 */
import { useState, useCallback, useMemo } from 'react';

interface FieldValidationState {
  value: string;
  isValid: boolean;
  errorMessage: string;
  isTouched: boolean;
}

type FormValidator<T extends Record<string, any>> = {
  [K in keyof T]: (value: string, formData: T) => string;
};

export function useFormValidation<T extends Record<string, any>>(
  initialValues: { [K in keyof T]?: string },
  validators: FormValidator<T>
) {
  // 初始化表单状态
  const [formData, setFormData] = useState<Record<string, FieldValidationState>>(
    () => {
      const initial: Record<string, FieldValidationState> = {};
      Object.keys(initialValues).forEach(key => {
        initial[key] = {
          value: initialValues[key as keyof T] || '',
          isValid: true,
          errorMessage: '',
          isTouched: false,
        };
      });
      return initial;
    }
  );

  /**
   * 处理输入变化
   */
  const handleChange = useCallback(
    (field: keyof T, value: string) => {
      setFormData(prev => {
        const newState = { ...prev };
        const currentData = Object.fromEntries(
          Object.entries(newState).map(([k, v]) => [k, v.value])
        ) as T;

        // 执行验证
        const errorMessage = validators[field]?.(value, currentData) || '';

        newState[field as string] = {
          value,
          isValid: !errorMessage,
          errorMessage,
          isTouched: true,
        };

        return newState;
      });
    },
    [validators]
  );

  /**
   * 验证单个字段
   */
  const validateField = useCallback(
    (field: keyof T) => {
      setFormData(prev => {
        const newState = { ...prev };
        const fieldState = newState[field as string];
        const currentData = Object.fromEntries(
          Object.entries(newState).map(([k, v]) => [k, v.value])
        ) as T;

        const errorMessage = validators[field]?.(fieldState.value, currentData) || '';

        newState[field as string] = {
          ...fieldState,
          isValid: !errorMessage,
          errorMessage,
          isTouched: true,
        };

        return newState;
      });
    },
    [validators]
  );

  /**
   * 验证所有字段
   */
  const validateAll = useCallback((): boolean => {
    let allValid = true;

    setFormData(prev => {
      const newState = { ...prev };
      const currentData = Object.fromEntries(
        Object.entries(newState).map(([k, v]) => [k, v.value])
      ) as T;

      Object.keys(validators).forEach(key => {
        const field = key as keyof T;
        const errorMessage = validators[field]?.(
          newState[key].value,
          currentData
        ) || '';

        newState[key] = {
          ...newState[key],
          isValid: !errorMessage,
          errorMessage,
          isTouched: true,
        };

        if (errorMessage) {
          allValid = false;
        }
      });

      return newState;
    });

    return allValid;
  }, [validators]);

  /**
   * 重置表单
   */
  const resetForm = useCallback(() => {
    setFormData(
      Object.fromEntries(
        Object.keys(initialValues).map(key => [
          key,
          {
            value: initialValues[key as keyof T] || '',
            isValid: true,
            errorMessage: '',
            isTouched: false,
          },
        ])
      )
    );
  }, [initialValues]);

  /**
   * 获取表单数据
   */
  const getValues = useCallback((): T => {
    return Object.fromEntries(
      Object.entries(formData).map(([k, v]) => [k, v.value])
    ) as T;
  }, [formData]);

  /**
   * 表单是否有效
   */
  const isFormValid = useMemo(() => {
    return Object.values(formData).every(
      field => field.isValid && field.value.trim().length > 0
    );
  }, [formData]);

  return {
    formData,
    handleChange,
    validateField,
    validateAll,
    resetForm,
    getValues,
    isFormValid,
  };
}

3.3 验证输入组件

typescript 复制代码
/**
 * 验证输入组件
 * 集成验证状态和错误提示显示
 */
import React, { forwardRef } from 'react';
import {
  View,
  TextInput,
  Text,
  StyleSheet,
  TextInputProps,
} from 'react-native';

interface ValidatedInputProps extends TextInputProps {
  label?: string;
  error?: string;
  successMessage?: string;
  showSuccess?: boolean;
  required?: boolean;
}

export const ValidatedInput = forwardRef<TextInput, ValidatedInputProps>(
  (
    {
      label,
      error,
      successMessage,
      showSuccess = false,
      required = false,
      style,
      ...props
    },
    ref
  ) => {
    const hasError = Boolean(error);
    const hasSuccess = showSuccess && !hasError && successMessage;

    return (
      <View style={styles.container}>
        {label && (
          <Text style={styles.label}>
            {label}
            {required && <Text style={styles.required}> *</Text>}
          </Text>
        )}
        <TextInput
          ref={ref}
          style={[
            styles.input,
            hasError && styles.inputError,
            hasSuccess && styles.inputSuccess,
            style,
          ]}
          placeholderTextColor="#999"
          {...props}
        />
        {hasError && <Text style={styles.errorText}>{error}</Text>}
        {hasSuccess && <Text style={styles.successText}>{successMessage}</Text>}
      </View>
    );
  }
);

const styles = StyleSheet.create({
  container: {
    marginBottom: 16,
  },
  label: {
    fontSize: 14,
    fontWeight: '600',
    color: '#333',
    marginBottom: 6,
  },
  required: {
    color: '#FF3B30',
  },
  input: {
    backgroundColor: '#fff',
    borderWidth: 1,
    borderColor: '#E5E5E5',
    borderRadius: 8,
    paddingHorizontal: 12,
    paddingVertical: 10,
    fontSize: 15,
    color: '#333',
  },
  inputError: {
    borderColor: '#FF3B30',
    backgroundColor: '#FFF5F5',
  },
  inputSuccess: {
    borderColor: '#34C759',
    backgroundColor: '#F5FFF9',
  },
  errorText: {
    fontSize: 12,
    color: '#FF3B30',
    marginTop: 4,
    marginLeft: 4,
  },
  successText: {
    fontSize: 12,
    color: '#34C759',
    marginTop: 4,
    marginLeft: 4,
  },
});

3.4 密码强度指示器组件

typescript 复制代码
/**
 * 密码强度指示器组件
 * 实时显示密码强度和改进建议
 */
import React, { useMemo } from 'react';
import { View, Text, StyleSheet } from 'react-native';

interface PasswordStrengthProps {
  password: string;
  showLabel?: boolean;
}

interface PasswordStrengthResult {
  score: number;
  label: string;
  color: string;
  suggestions: string[];
}

export const PasswordStrengthIndicator: React.FC<PasswordStrengthProps> = ({
  password,
  showLabel = true,
}) => {
  const strength = useMemo((): PasswordStrengthResult => {
    if (!password) {
      return {
        score: 0,
        label: '请输入密码',
        color: '#999',
        suggestions: [],
      };
    }

    const suggestions: string[] = [];
    let score = 0;

    // 长度检查
    if (password.length >= 8) score += 1;
    else suggestions.push('至少8个字符');

    if (password.length >= 12) score += 1;

    // 包含大写字母
    if (/[A-Z]/.test(password)) score += 1;
    else suggestions.push('包含大写字母');

    // 包含小写字母
    if (/[a-z]/.test(password)) score += 1;
    else suggestions.push('包含小写字母');

    // 包含数字
    if (/\d/.test(password)) score += 1;
    else suggestions.push('包含数字');

    // 包含特殊字符
    if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) score += 1;
    else suggestions.push('包含特殊字符');

    const levels = [
      { score: 0, label: '非常弱', color: '#FF3B30' },
      { score: 2, label: '弱', color: '#FF9500' },
      { score: 3, label: '中等', color: '#FFCC00' },
      { score: 4, label: '强', color: '#34C759' },
      { score: 5, label: '非常强', color: '#00C7BE' },
    ];

    const level = levels.reduce((prev, curr) =>
      score >= curr.score ? curr : prev
    );

    return {
      score,
      ...level,
      suggestions: suggestions.slice(0, 3),
    };
  }, [password]);

  return (
    <View style={styles.container}>
      {showLabel && (
        <Text style={styles.label}>密码强度</Text>
      )}

      <View style={styles.barContainer}>
        {[1, 2, 3, 4, 5].map(level => (
          <View
            key={level}
            style={[
              styles.bar,
              {
                backgroundColor:
                  level <= strength.score
                    ? strength.color
                    : '#E5E5E5',
              },
            ]}
          />
        ))}
      </View>

      <View style={styles.infoRow}>
        <Text style={[styles.label, { color: strength.color }]}>
          {strength.label}
        </Text>
        {password.length > 0 && (
          <Text style={styles.length}>{password.length} 字符</Text>
        )}
      </View>

      {strength.suggestions.length > 0 && (
        <View style={styles.suggestions}>
          <Text style={styles.suggestionTitle}>改进建议:</Text>
          {strength.suggestions.map((suggestion, index) => (
            <Text key={index} style={styles.suggestion}>
              • {suggestion}
            </Text>
          ))}
        </View>
      )}
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    paddingVertical: 8,
  },
  label: {
    fontSize: 13,
    fontWeight: '600',
    marginBottom: 6,
  },
  barContainer: {
    flexDirection: 'row',
    gap: 4,
    height: 6,
    marginBottom: 8,
  },
  bar: {
    flex: 1,
    borderRadius: 3,
  },
  infoRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
  },
  length: {
    fontSize: 12,
    color: '#999',
  },
  suggestions: {
    marginTop: 8,
    backgroundColor: '#F5F5F5',
    padding: 10,
    borderRadius: 6,
  },
  suggestionTitle: {
    fontSize: 12,
    fontWeight: '600',
    color: '#666',
    marginBottom: 4,
  },
  suggestion: {
    fontSize: 12,
    color: '#666',
    lineHeight: 18,
  },
});

3.5 完整注册表单示例

typescript 复制代码
/**
 * 用户注册表单示例
 * 演示完整的输入验证功能
 */
import React, { useCallback } from 'react';
import {
  View,
  Text,
  StyleSheet,
  ScrollView,
  SafeAreaView,
  TouchableOpacity,
  ActivityIndicator,
} from 'react-native';
import { ValidatedInput } from './ValidatedInput';
import { PasswordStrengthIndicator } from './PasswordStrengthIndicator';
import { useFormValidation } from './useFormValidation';
import { ValidationRules } from './ValidationRules';

interface RegisterFormData {
  username: string;
  email: string;
  password: string;
  confirmPassword: string;
  phone: string;
}

const RegisterForm: React.FC = () => {
  const {
    formData,
    handleChange,
    validateAll,
    getValues,
    isFormValid,
  } = useFormValidation<RegisterFormData>(
    {
      username: '',
      email: '',
      password: '',
      confirmPassword: '',
      phone: '',
    },
    {
      username: ValidationRules.compose(
        ValidationRules.required('用户名不能为空'),
        ValidationRules.length(3, 20),
        ValidationRules.pattern(
          /^[a-zA-Z0-9_]+$/,
          '只能包含字母、数字和下划线'
        )
      ),
      email: ValidationRules.compose(
        ValidationRules.required('邮箱不能为空'),
        ValidationRules.email()
      ),
      password: ValidationRules.compose(
        ValidationRules.required('密码不能为空'),
        ValidationRules.length(8, 32),
        ValidationRules.passwordStrength(3)
      ),
      confirmPassword: (value, formData) => {
        if (!value) return '请确认密码';
        if (value !== formData.password) return '两次输入的密码不一致';
        return '';
      },
      phone: ValidationRules.compose(
        ValidationRules.required('手机号不能为空'),
        ValidationRules.phone()
      ),
    }
  );

  const [isSubmitting, setIsSubmitting] = React.useState(false);

  const handleSubmit = useCallback(async () => {
    const isValid = validateAll();

    if (!isValid) {
      return;
    }

    setIsSubmitting(true);

    try {
      const values = getValues();

      // 模拟API调用
      await new Promise(resolve => setTimeout(resolve, 1500));

      console.log('注册成功:', values);
      // 实际项目中这里会调用注册API
    } catch (error) {
      console.error('注册失败:', error);
    } finally {
      setIsSubmitting(false);
    }
  }, [validateAll, getValues]);

  return (
    <SafeAreaView style={styles.container}>
      <ScrollView contentContainerStyle={styles.scrollContent}>
        <Text style={styles.title}>创建账户</Text>

        <ValidatedInput
          label="用户名"
          value={formData.username.value}
          onChangeText={text => handleChange('username', text)}
          error={
            formData.username.isTouched ? formData.username.errorMessage : ''
          }
          successMessage={
            formData.username.isValid && formData.username.value
              ? '✓ 用户名可用'
              : ''
          }
          showSuccess={formData.username.isValid}
          required
          autoCapitalize="none"
          autoCorrect={false}
          placeholder="3-20个字符"
        />

        <ValidatedInput
          label="邮箱地址"
          value={formData.email.value}
          onChangeText={text => handleChange('email', text)}
          error={
            formData.email.isTouched ? formData.email.errorMessage : ''
          }
          successMessage={
            formData.email.isValid && formData.email.value
              ? '✓ 邮箱格式正确'
              : ''
          }
          showSuccess={formData.email.isValid}
          required
          keyboardType="email-address"
          autoCapitalize="none"
          autoCorrect={false}
          placeholder="example@domain.com"
        />

        <ValidatedInput
          label="手机号码"
          value={formData.phone.value}
          onChangeText={text => handleChange('phone', text)}
          error={
            formData.phone.isTouched ? formData.phone.errorMessage : ''
          }
          successMessage={
            formData.phone.isValid && formData.phone.value
              ? '✓ 手机号有效'
              : ''
          }
          showSuccess={formData.phone.isValid}
          required
          keyboardType="phone-pad"
          placeholder="11位手机号码"
        />

        <View style={styles.passwordSection}>
          <Text style={styles.label}>密码 *</Text>
          <ValidatedInput
            value={formData.password.value}
            onChangeText={text => handleChange('password', text)}
            error={
              formData.password.isTouched ? formData.password.errorMessage : ''
            }
            secureTextEntry
            required
            placeholder="至少8位字符"
          />
          <PasswordStrengthIndicator password={formData.password.value} />
        </View>

        <ValidatedInput
          label="确认密码"
          value={formData.confirmPassword.value}
          onChangeText={text => handleChange('confirmPassword', text)}
          error={
            formData.confirmPassword.isTouched
              ? formData.confirmPassword.errorMessage
              : ''
          }
          successMessage={
            formData.confirmPassword.isValid && formData.confirmPassword.value
              ? '✓ 密码匹配'
              : ''
          }
          showSuccess={formData.confirmPassword.isValid}
          required
          secureTextEntry
          placeholder="再次输入密码"
        />

        <TouchableOpacity
          style={[
            styles.submitButton,
            (!isFormValid || isSubmitting) && styles.submitButtonDisabled,
          ]}
          onPress={handleSubmit}
          disabled={!isFormValid || isSubmitting}
          activeOpacity={0.8}
        >
          {isSubmitting ? (
            <ActivityIndicator color="#fff" />
          ) : (
            <Text style={styles.submitButtonText}>注册</Text>
          )}
        </TouchableOpacity>

        <Text style={styles.hint}>
          带 * 的字段为必填项,所有验证会在输入时实时进行
        </Text>
      </ScrollView>
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F5F5F5',
  },
  scrollContent: {
    padding: 20,
    paddingBottom: 40,
  },
  title: {
    fontSize: 28,
    fontWeight: 'bold',
    color: '#333',
    marginBottom: 30,
    textAlign: 'center',
  },
  passwordSection: {
    marginBottom: 16,
  },
  label: {
    fontSize: 14,
    fontWeight: '600',
    color: '#333',
    marginBottom: 6,
  },
  submitButton: {
    backgroundColor: '#007AFF',
    borderRadius: 12,
    paddingVertical: 16,
    alignItems: 'center',
    marginTop: 10,
    marginBottom: 20,
  },
  submitButtonDisabled: {
    backgroundColor: '#B4D4FF',
  },
  submitButtonText: {
    color: '#fff',
    fontSize: 18,
    fontWeight: '600',
  },
  hint: {
    fontSize: 13,
    color: '#999',
    textAlign: 'center',
    lineHeight: 18,
  },
});

export default RegisterForm;

四、HarmonyOS平台优化

4.1 触觉反馈工具

typescript 复制代码
import { Platform } from 'react-native';

/**
 * HarmonyOS触觉反馈工具
 */
export class HapticFeedback {
  /**
   * 验证失败时的震动反馈
   */
  static validationError() {
    if (Platform.OS === 'harmony') {
      // 需要在module.json5中申请ohos.permission.VIBRATE权限
      console.warn('验证失败');
      // 实际实现会调用原生震动API
    }
  }

  /**
   * 验证成功时的轻微反馈
   */
  static validationSuccess() {
    if (Platform.OS === 'harmony') {
      // 轻微震动反馈
    }
  }

  /**
   * 输入时的触觉反馈
   */
  static keystroke() {
    if (Platform.OS === 'harmony') {
      // 轻微按键反馈
    }
  }
}

4.2 性能优化Hook

typescript 复制代码
import { useCallback, useEffect, useRef } from 'react';

/**
 * 防抖Hook
 * 用于优化频繁触发的验证
 */
export function useDebounce<T extends (...args: any[]) => any>(
  callback: T,
  delay: number
): T {
  const timeoutRef = useRef<NodeJS.Timeout>();
  const callbackRef = useRef(callback);

  useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);

  const debouncedCallback = useCallback(
    (...args: Parameters<T>) => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }

      timeoutRef.current = setTimeout(() => {
        callbackRef.current(...args);
      }, delay);
    },
    [delay]
  ) as T;

  useEffect(() => {
    return () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
    };
  }, []);

  return debouncedCallback;
}

五、最佳实践与性能优化

优化策略 实现方式 效果
防抖验证 使用useDebounce包装onChangeText 减少不必要的验证计算
延迟错误显示 通过isTouched状态控制 避免用户未输入就显示错误
React.memo 包装ValidatedInput组件 防止父组件更新导致重渲染
useCallback 稳定化回调函数引用 减少子组件不必要的更新
useMemo 缓存计算结果 避免重复的复杂计算

六、总结

本文详细介绍了在HarmonyOS平台上实现完整TextInput输入验证系统的方案。核心要点:

  1. 模块化设计:验证规则、Hook、组件分离,便于复用
  2. 类型安全:完整的TypeScript类型定义
  3. 用户体验:实时反馈、清晰的错误提示、密码强度可视化
  4. 性能优化:防抖、记忆化、条件渲染
  5. 平台适配:针对HarmonyOS的触觉反馈、样式优化

相关推荐
_waylau1 小时前
鸿蒙架构师修炼之道-架构师设计思维特点
华为·架构·架构师·harmonyos·鸿蒙·鸿蒙系统
阿林来了2 小时前
Flutter三方库适配OpenHarmony【flutter_speech】— MethodChannel 双向通信实现
flutter·harmonyos
阿林来了2 小时前
Flutter三方库适配OpenHarmony【flutter_speech】— 单元测试与集成测试
flutter·单元测试·集成测试·harmonyos
松叶似针3 小时前
Flutter三方库适配OpenHarmony【secure_application】— 性能影响与优化策略
flutter·harmonyos
星空22234 小时前
【HarmonyOS】React Native 实战项目与 Redux Toolkit 状态管理实践
react native·华为·harmonyos
lbb 小魔仙4 小时前
【HarmonyOS】RN_of_HarmonyOS实战项目_电话号码输入
华为·harmonyos
果粒蹬i5 小时前
【HarmonyOS】RN_of_HarmonyOS实战项目_搜索框样式
华为·harmonyos
松叶似针5 小时前
Flutter三方库适配OpenHarmony【secure_application】— 测试策略与用例设计
flutter·harmonyos
lbb 小魔仙5 小时前
【HarmonyOS】RN_of_HarmonyOS实战项目_密码显示隐藏
华为·harmonyos