前言

Formik 作为 React 生态中久经考验的表单管理库,能够大幅简化状态管理,减少约 60% 的样板代码。但在 OpenHarmony 平台使用时,需特别注意以下关键问题:
以下是整理后的表格形式:

问题 影响
输入法事件差异 验证时机不准确
键盘管理机制 布局遮挡输入框
无障碍支持 错误提示无法播报

快速开始

关键配置

import { Formik } from 'formik';
import * as yup from 'yup';

// Yup 验证模式
const schema = yup.object().shape({
  email: yup.string().email('请输入有效的邮箱').required('邮箱不能为空'),
  password: yup.string()
    .min(8, '密码至少8个字符')
    .matches(/[a-z]/, '必须包含小写字母')
    .matches(/[A-Z]/, '必须包含大写字母')
    .matches(/[0-9]/, '必须包含数字')
    .required('密码不能为空'),
  confirmPassword: yup.string()
    .oneOf([yup.ref('password')], '两次密码不一致')
    .required('请确认密码'),
});

// Formik 组件使用
<Formik
  initialValues={{ email: '', password: '', confirmPassword: '' }}
  validationSchema={schema}
  validateOnBlur={true}      // OpenHarmony 推荐:失焦验证
  validateOnChange={false}   // 减少频繁验证
  onSubmit={handleSubmit}
>
  {({ handleChange, handleBlur, values, errors, touched }) => (
    // 表单内容
  )}
</Formik>

为什么使用 validateOnBlur?

  1. 提升用户体验
  • 避免用户在输入过程中频繁看到错误提示
  • 减少界面干扰,让用户专注于当前输入
  1. 性能优化
  • 减少实时校验带来的计算开销
  • 只在必要时触发校验逻辑
  1. 逻辑合理性
  • 用户完成输入后才进行校验更符合实际场景
  • 避免未完成输入就被判定为错误的情况
  1. 移动端适配
  • 在移动设备上减少虚拟键盘与校验提示的冲突
  • 避免因实时校验导致的输入卡顿
  1. 错误处理
  • 集中处理错误信息,便于用户一次性修正
  • 减少重复错误提示带来的困扰

完整实现

表单组件代码

/**
 * Formik 表单验证 - OpenHarmony 适配版
 *
 * @platform OpenHarmony 6.0.0 (API 20)
 * @react-native 0.72.5
 * @typescript 5.0+
 */

import React, { useCallback, useMemo } from 'react';
import {
  View,
  Text,
  StyleSheet,
  TouchableOpacity,
  ScrollView,
  TextInput,
  KeyboardAvoidingView,
  Platform,
  ActivityIndicator,
} from 'react-native';

// ==================== 类型定义 ====================

interface FormValues {
  email: string;
  password: string;
  confirmPassword: string;
  agreeTerms: boolean;
}

interface FormErrors {
  email?: string;
  password?: string;
  confirmPassword?: string;
  agreeTerms?: string;
}

interface FormTouched {
  email?: boolean;
  password?: boolean;
  confirmPassword?: boolean;
  agreeTerms?: boolean;
}

interface Props {
  onSubmit?: (values: FormValues) => Promise<void>;
  onBack?: () => void;
}

// ==================== 验证规则 ====================

/**
 * 邮箱验证
 */
const validateEmail = (email: string): string | undefined => {
  if (!email) return '邮箱不能为空';
  const emailRegex = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
  if (!emailRegex.test(email)) return '请输入有效的邮箱地址';
};

/**
 * 密码强度等级
 */
enum PasswordStrength {
  None = 0,
  Weak = 1,
  Fair = 2,
  Good = 3,
  Strong = 4,
}

/**
 * 计算密码强度
 */
const getPasswordStrength = (password: string): PasswordStrength => {
  let score = 0;
  if (password.length >= 8) score++;
  if (password.length >= 12) score++;
  if (/[a-z]/.test(password) && /[A-Z]/.test(password)) score++;
  if (/[0-9]/.test(password)) score++;
  if (/[^a-zA-Z0-9]/.test(password)) score++;
  return Math.min(score, 4) as PasswordStrength;
};

/**
 * 密码强度配置
 */
const STRENGTH_CONFIG = {
  [PasswordStrength.None]: { label: '无', color: '#d1d5db', percent: 0 },
  [PasswordStrength.Weak]: { label: '弱', color: '#ef4444', percent: 25 },
  [PasswordStrength.Fair]: { label: '一般', color: '#f59e0b', percent: 50 },
  [PasswordStrength.Good]: { label: '良好', color: '#10b981', percent: 75 },
  [PasswordStrength.Strong]: { label: '强', color: '#3b82f6', percent: 100 },
};

/**
 * 密码验证
 */
const validatePassword = (password: string): string | undefined => {
  if (!password) return '密码不能为空';
  if (password.length < 8) return '密码至少8个字符';
  if (!/[a-z]/.test(password)) return '必须包含小写字母';
  if (!/[A-Z]/.test(password)) return '必须包含大写字母';
  if (!/[0-9]/.test(password)) return '必须包含数字';
};

/**
 * 确认密码验证
 */
const validateConfirmPassword = (
  confirmPassword: string,
  password: string
): string | undefined => {
  if (!confirmPassword) return '请确认密码';
  if (confirmPassword !== password) return '两次输入的密码不一致';
};

// ==================== 自定义 Hooks ====================

/**
 * 表单状态管理 Hook
 * 封装 Formik 核心逻辑
 */
const useFormikState = (initialValues: FormValues) => {
  const [values, setValues] = React.useState<FormValues>(initialValues);
  const [errors, setErrors] = React.useState<FormErrors>({});
  const [touched, setTouched] = React.useState<FormTouched>({});
  const [isSubmitting, setIsSubmitting] = React.useState(false);
  const [submitCount, setSubmitCount] = React.useState(0);

  /**
   * 计算表单是否有效
   */
  const isValid = useMemo(() => {
    return !!(
      values.email &&
      !validateEmail(values.email) &&
      values.password &&
      !validatePassword(values.password) &&
      values.confirmPassword === values.password &&
      values.agreeTerms
    );
  }, [values]);

  /**
   * 获取密码强度
   */
  const passwordStrength = useMemo(() => {
    return STRENGTH_CONFIG[getPasswordStrength(values.password)];
  }, [values.password]);

  /**
   * 处理值变更
   */
  const handleChange = useCallback(
    (field: keyof FormValues) => (value: string | boolean) => {
      setValues(prev => ({ ...prev, [field]: value }));

      // 仅在字段已被触摸时验证
      if (touched[field]) {
        let error: string | undefined;

        if (field === 'email') error = validateEmail(value as string);
        else if (field === 'password') error = validatePassword(value as string);
        else if (field === 'confirmPassword')
          error = validateConfirmPassword(value as string, values.password);
        else if (field === 'agreeTerms' && !value)
          error = '请同意服务条款';

        setErrors(prev => ({ ...prev, [field]: error }));
      }
    },
    [touched, values.password]
  );

  /**
   * 处理失焦
   */
  const handleBlur = useCallback(
    (field: keyof FormValues) => () => {
      setTouched(prev => ({ ...prev, [field]: true }));

      let error: string | undefined;
      if (field === 'email') error = validateEmail(values.email);
      else if (field === 'password') error = validatePassword(values.password);
      else if (field === 'confirmPassword')
        error = validateConfirmPassword(values.confirmPassword, values.password);

      setErrors(prev => ({ ...prev, [field]: error }));
    },
    [values]
  );

  /**
   * 处理条款切换
   */
  const handleToggleTerms = useCallback(() => {
    const newValue = !values.agreeTerms;
    handleChange('agreeTerms')(newValue);
    setTouched(prev => ({ ...prev, agreeTerms: true }));
  }, [values.agreeTerms, handleChange]);

  /**
   * 提交表单
   */
  const handleSubmit = useCallback(
    async (onSubmit?: (values: FormValues) => Promise<void>) => {
      setIsSubmitting(true);
      setSubmitCount(prev => prev + 1);

      // 标记所有字段为已触摸
      setTouched({
        email: true,
        password: true,
        confirmPassword: true,
        agreeTerms: true,
      });

      // 执行所有验证
      const newErrors: FormErrors = {
        email: validateEmail(values.email),
        password: validatePassword(values.password),
        confirmPassword: validateConfirmPassword(values.confirmPassword, values.password),
        agreeTerms: values.agreeTerms ? undefined : '请同意服务条款',
      };

      setErrors(newErrors);

      const hasErrors = Object.values(newErrors).some(e => e !== undefined);

      if (hasErrors) {
        setIsSubmitting(false);
        return false;
      }

      try {
        if (onSubmit) {
          await onSubmit(values);
        }
        return true;
      } finally {
        setIsSubmitting(false);
      }
    },
    [values]
  );

  /**
   * 重置表单
   */
  const reset = useCallback(() => {
    setValues(initialValues);
    setErrors({});
    setTouched({});
    setSubmitCount(0);
  }, [initialValues]);

  return {
    // 状态
    values,
    errors,
    touched,
    isSubmitting,
    submitCount,
    isValid,
    passwordStrength,

    // 操作
    handleChange,
    handleBlur,
    handleToggleTerms,
    handleSubmit,
    reset,
  };
};

// ==================== 表单组件 ====================

const FormikFormScreen: React.FC<Props> = ({ onSubmit, onBack }) => {
  const initialValues: FormValues = {
    email: '',
    password: '',
    confirmPassword: '',
    agreeTerms: false,
  };

  const form = useFormikState(initialValues);

  /**
   * 处理提交
   */
  const handleSubmit = useCallback(async () => {
    const success = await form.handleSubmit(onSubmit);
    if (success) {
      alert('注册成功!');
      form.reset();
    }
  }, [form, onSubmit]);

  return (
    <KeyboardAvoidingView
      behavior={Platform.OS === 'ios' ? 'padding' : undefined}
      style={styles.container}
    >
      <ScrollView
        style={styles.scrollView}
        keyboardShouldPersistTaps="handled"
        contentContainerStyle={styles.scrollContent}
      >
        {/* 头部 */}
        <View style={styles.header}>
          {onBack && (
            <TouchableOpacity onPress={onBack} style={styles.backButton}>
              <Text style={styles.backButtonText}>← 返回</Text>
            </TouchableOpacity>
          )}
          <View style={styles.headerContent}>
            <Text style={styles.headerTitle}>用户注册</Text>
            <Text style={styles.headerSubtitle}>Formik + Yup 表单验证</Text>
          </View>
        </View>

        {/* 平台信息 */}
        <View style={styles.platformBar}>
          <Text style={styles.platformText}>
            {Platform.OS.toUpperCase()} • OpenHarmony 6.0.0
          </Text>
        </View>

        {/* 表单卡片 */}
        <View style={styles.formCard}>
          <Text style={styles.formTitle}>创建账号</Text>

          {/* 邮箱 */}
          <FormField
            label="邮箱地址"
            placeholder="请输入邮箱地址"
            value={form.values.email}
            error={form.errors.email}
            touched={form.touched.email}
            onChangeText={form.handleChange('email')}
            onBlur={form.handleBlur('email')}
            keyboardType="email-address"
            autoCapitalize="none"
            disabled={form.isSubmitting}
            icon="📧"
          />

          {/* 密码 */}
          <PasswordField
            label="密码"
            placeholder="请输入密码"
            value={form.values.password}
            error={form.errors.password}
            touched={form.touched.password}
            onChangeText={form.handleChange('password')}
            onBlur={form.handleBlur('password')}
            disabled={form.isSubmitting}
            strength={form.passwordStrength}
          />

          {/* 确认密码 */}
          <FormField
            label="确认密码"
            placeholder="请再次输入密码"
            value={form.values.confirmPassword}
            error={form.errors.confirmPassword}
            touched={form.touched.confirmPassword}
            onChangeText={form.handleChange('confirmPassword')}
            onBlur={form.handleBlur('confirmPassword')}
            secureTextEntry
            disabled={form.isSubmitting}
            icon="🔒"
          />

          {/* 服务条款 */}
          <View style={styles.termsContainer}>
            <TouchableOpacity
              style={styles.checkbox}
              onPress={form.handleToggleTerms}
              disabled={form.isSubmitting}
              activeOpacity={0.7}
            >
              <View style={[styles.checkboxBox, form.values.agreeTerms && styles.checkboxChecked]}>
                {form.values.agreeTerms && <Text style={styles.checkmark}></Text>}
              </View>
              <Text style={styles.termsText}>
                我已阅读并同意
                <Text style={styles.termsLink}>《服务条款》</Text><Text style={styles.termsLink}>《隐私政策》</Text>
              </Text>
            </TouchableOpacity>
            {form.errors.agreeTerms && form.touched.agreeTerms && (
              <Text style={styles.errorText}>{form.errors.agreeTerms}</Text>
            )}
          </View>

          {/* 提交按钮 */}
          <TouchableOpacity
            style={[styles.submitButton, !form.isValid && styles.submitButtonDisabled]}
            onPress={handleSubmit}
            disabled={!form.isValid || form.isSubmitting}
            activeOpacity={0.8}
          >
            {form.isSubmitting ? (
              <ActivityIndicator color="#fff" />
            ) : (
              <Text style={styles.submitButtonText}>立即注册</Text>
            )}
          </TouchableOpacity>

          {/* 表单状态 */}
          <View style={styles.formStatus}>
            <StatusBadge label="提交次数" value={form.submitCount} />
            <StatusBadge
              label="表单状态"
              value={form.isValid ? '有效' : '无效'}
              status={form.isValid ? 'success' : 'error'}
            />
          </View>
        </View>

        {/* 验证规则说明 */}
        <View style={styles.rulesCard}>
          <Text style={styles.rulesTitle}>密码要求</Text>
          <RuleItem icon="📏" text="至少 8 个字符" />
          <RuleItem icon="🔡" text="包含小写字母" />
          <RuleItem icon="🔠" text="包含大写字母" />
          <RuleItem icon="🔢" text="包含数字" />
        </View>

        {/* OpenHarmony 适配提示 */}
        <View style={styles.tipsCard}>
          <Text style={styles.tipsTitle}>OpenHarmony 适配要点</Text>
          <TipItem
            icon="validateOnBlur"
            title="失焦验证模式"
            desc="使用 validateOnBlur 避免输入法频繁触发验证"
          />
          <TipItem
            icon="KeyboardAvoidingView"
            title="键盘避让处理"
            desc="OpenHarmony 设备需调整 KeyboardAvoidingView 的 behavior"
          />
          <TipItem
            icon="accessibilityLiveRegion"
            title="无障碍支持"
            desc="错误信息需添加 accessibilityLiveRegion 属性"
          />
        </View>
      </ScrollView>
    </KeyboardAvoidingView>
  );
};

// ==================== 子组件 ====================

interface FormFieldProps {
  label: string;
  placeholder: string;
  value: string;
  error?: string;
  touched?: boolean;
  onChangeText: (text: string) => void;
  onBlur: () => void;
  keyboardType?: 'email-address' | 'default';
  autoCapitalize?: 'none' | 'sentences';
  secureTextEntry?: boolean;
  disabled?: boolean;
  icon?: string;
}

const FormField: React.FC<FormFieldProps> = React.memo(({
  label,
  placeholder,
  value,
  error,
  touched,
  onChangeText,
  onBlur,
  keyboardType = 'default',
  autoCapitalize = 'none',
  secureTextEntry = false,
  disabled = false,
  icon,
}) => {
  const showError = touched && error;

  return (
    <View style={styles.fieldContainer}>
      <Text style={styles.fieldLabel}>
        {icon && <Text style={styles.fieldIcon}>{icon}</Text>}
        {label}
      </Text>
      <TextInput
        style={[styles.fieldInput, showError && styles.fieldInputError]}
        placeholder={placeholder}
        value={value}
        onChangeText={onChangeText}
        onBlur={onBlur}
        keyboardType={keyboardType}
        autoCapitalize={autoCapitalize}
        secureTextEntry={secureTextEntry}
        editable={!disabled}
        placeholderTextColor="#9ca3af"
        accessibilityLabel={label}
        accessibilityLiveRegion={showError ? 'polite' : 'none'}
        accessibilityHint={showError ? error : undefined}
      />
      {showError && (
        <Text
          style={styles.errorText}
          accessibilityLiveRegion="polite"
          accessibilityLabel={`错误: ${error}`}
        >
          {error}
        </Text>
      )}
    </View>
  );
});

interface PasswordFieldProps extends Omit<FormFieldProps, 'icon'> {
  strength: { label: string; color: string; percent: number };
}

const PasswordField: React.FC<PasswordFieldProps> = React.memo((props) => {
  const { strength, ...fieldProps } = props;
  const showError = props.touched && props.error;

  return (
    <View style={styles.fieldContainer}>
      <Text style={styles.fieldLabel}>
        <Text style={styles.fieldIcon}>🔐</Text>
        {props.label}
      </Text>
      <TextInput
        style={[styles.fieldInput, showError && styles.fieldInputError]}
        placeholder={props.placeholder}
        value={props.value}
        onChangeText={props.onChangeText}
        onBlur={props.onBlur}
        secureTextEntry
        editable={!props.disabled}
        placeholderTextColor="#9ca3af"
      />
      {showError && (
        <Text style={styles.errorText} accessibilityLiveRegion="polite">
          {props.error}
        </Text>
      )}
      {/* 密码强度指示器 */}
      {props.value.length > 0 && !showError && (
        <View style={styles.strengthContainer}>
          <View style={styles.strengthBar}>
            <View
              style={[
                styles.strengthFill,
                { backgroundColor: strength.color, width: `${strength.percent}%` },
              ]}
            />
          </View>
          <Text style={[styles.strengthText, { color: strength.color }]}>
            强度: {strength.label}
          </Text>
        </View>
      )}
    </View>
  );
});

interface StatusBadgeProps {
  label: string;
  value: string | number;
  status?: 'default' | 'success' | 'error';
}

const StatusBadge: React.FC<StatusBadgeProps> = ({ label, value, status = 'default' }) => {
  const statusColors = {
    default: '#6b7280',
    success: '#10b981',
    error: '#ef4444',
  };

  return (
    <View style={styles.statusBadge}>
      <Text style={styles.statusLabel}>{label}</Text>
      <Text style={[styles.statusValue, { color: statusColors[status] }]}>
        {value}
      </Text>
    </View>
  );
};

interface RuleItemProps {
  icon: string;
  text: string;
}

const RuleItem: React.FC<RuleItemProps> = ({ icon, text }) => (
  <View style={styles.ruleItem}>
    <Text style={styles.ruleIcon}>{icon}</Text>
    <Text style={styles.ruleText}>{text}</Text>
  </View>
);

interface TipItemProps {
  icon: string;
  title: string;
  desc: string;
}

const TipItem: React.FC<TipItemProps> = ({ icon, title, desc }) => (
  <View style={styles.tipItem}>
    <View style={styles.tipIconBox}>
      <Text style={styles.tipIcon}>{icon}</Text>
    </View>
    <View style={styles.tipContent}>
      <Text style={styles.tipTitle}>{title}</Text>
      <Text style={styles.tipDesc}>{desc}</Text>
    </View>
  </View>
);

// ==================== 样式定义 ====================

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f9fafb',
  },
  scrollView: {
    flex: 1,
  },
  scrollContent: {
    paddingBottom: 24,
  },

  // 头部
  header: {
    flexDirection: 'row',
    alignItems: 'center',
    padding: 16,
    backgroundColor: '#7c3aed',
  },
  backButton: {
    padding: 8,
    marginRight: 8,
  },
  backButtonText: {
    color: '#fff',
    fontSize: 15,
  },
  headerContent: {
    flex: 1,
  },
  headerTitle: {
    fontSize: 18,
    fontWeight: '700',
    color: '#fff',
  },
  headerSubtitle: {
    fontSize: 12,
    color: 'rgba(255,255,255,0.8)',
    marginTop: 2,
  },

  // 平台信息
  platformBar: {
    paddingVertical: 10,
    paddingHorizontal: 16,
    backgroundColor: '#ede9fe',
    alignItems: 'center',
  },
  platformText: {
    fontSize: 12,
    color: '#6d28d9',
    fontWeight: '500',
  },

  // 表单卡片
  formCard: {
    margin: 16,
    backgroundColor: '#fff',
    borderRadius: 16,
    padding: 20,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.08,
    shadowRadius: 8,
    elevation: 3,
  },
  formTitle: {
    fontSize: 20,
    fontWeight: '700',
    color: '#111827',
    marginBottom: 20,
  },

  // 表单字段
  fieldContainer: {
    marginBottom: 16,
  },
  fieldLabel: {
    fontSize: 14,
    fontWeight: '600',
    color: '#374151',
    marginBottom: 8,
  },
  fieldIcon: {
    marginRight: 6,
  },
  fieldInput: {
    borderWidth: 1.5,
    borderColor: '#e5e7eb',
    borderRadius: 12,
    padding: 14,
    fontSize: 15,
    backgroundColor: '#f9fafb',
    color: '#111827',
  },
  fieldInputError: {
    borderColor: '#ef4444',
    backgroundColor: '#fef2f2',
  },
  errorText: {
    color: '#ef4444',
    fontSize: 12,
    marginTop: 6,
    marginLeft: 4,
  },

  // 密码强度
  strengthContainer: {
    marginTop: 8,
  },
  strengthBar: {
    height: 4,
    backgroundColor: '#e5e7eb',
    borderRadius: 2,
    overflow: 'hidden',
  },
  strengthFill: {
    height: '100%',
    borderRadius: 2,
  },
  strengthText: {
    fontSize: 11,
    marginTop: 4,
    fontWeight: '500',
  },

  // 服务条款
  termsContainer: {
    marginBottom: 16,
  },
  checkbox: {
    flexDirection: 'row',
    alignItems: 'flex-start',
  },
  checkboxBox: {
    width: 22,
    height: 22,
    borderRadius: 6,
    borderWidth: 2,
    borderColor: '#d1d5db',
    alignItems: 'center',
    justifyContent: 'center',
    marginRight: 10,
    marginTop: 1,
  },
  checkboxChecked: {
    backgroundColor: '#7c3aed',
    borderColor: '#7c3aed',
  },
  checkmark: {
    color: '#fff',
    fontSize: 12,
    fontWeight: '700',
  },
  termsText: {
    flex: 1,
    fontSize: 13,
    color: '#4b5563',
    lineHeight: 20,
  },
  termsLink: {
    color: '#7c3aed',
    fontWeight: '500',
  },

  // 提交按钮
  submitButton: {
    backgroundColor: '#7c3aed',
    borderRadius: 12,
    padding: 16,
    alignItems: 'center',
    marginTop: 8,
    minHeight: 52,
    justifyContent: 'center',
  },
  submitButtonDisabled: {
    backgroundColor: '#d1d5db',
  },
  submitButtonText: {
    color: '#fff',
    fontSize: 16,
    fontWeight: '600',
  },

  // 表单状态
  formStatus: {
    flexDirection: 'row',
    gap: 10,
    marginTop: 16,
    paddingTop: 16,
    borderTopWidth: 1,
    borderTopColor: '#f3f4f6',
  },
  statusBadge: {
    flex: 1,
    backgroundColor: '#f3f4f6',
    borderRadius: 8,
    padding: 10,
    alignItems: 'center',
  },
  statusLabel: {
    fontSize: 11,
    color: '#6b7280',
    marginBottom: 2,
  },
  statusValue: {
    fontSize: 14,
    fontWeight: '600',
  },

  // 规则卡片
  rulesCard: {
    margin: 16,
    marginTop: 0,
    backgroundColor: '#fff',
    borderRadius: 12,
    padding: 16,
  },
  rulesTitle: {
    fontSize: 14,
    fontWeight: '600',
    color: '#111827',
    marginBottom: 12,
  },
  ruleItem: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 8,
  },
  ruleIcon: {
    fontSize: 16,
    marginRight: 10,
  },
  ruleText: {
    fontSize: 13,
    color: '#4b5563',
  },

  // 提示卡片
  tipsCard: {
    margin: 16,
    marginTop: 0,
    backgroundColor: '#fef3c7',
    borderRadius: 12,
    padding: 16,
    borderLeftWidth: 4,
    borderLeftColor: '#f59e0b',
  },
  tipsTitle: {
    fontSize: 14,
    fontWeight: '600',
    color: '#92400e',
    marginBottom: 12,
  },
  tipItem: {
    flexDirection: 'row',
    marginBottom: 12,
  },
  tipItem: {
    flexDirection: 'row',
    marginBottom: 12,
  },
  tipIconBox: {
    width: 32,
    height: 32,
    borderRadius: 8,
    backgroundColor: '#fde68a',
    alignItems: 'center',
    justifyContent: 'center',
    marginRight: 12,
  },
  tipIcon: {
    fontSize: 12,
    fontWeight: '600',
    color: '#92400e',
  },
  tipContent: {
    flex: 1,
  },
  tipTitle: {
    fontSize: 13,
    fontWeight: '600',
    color: '#78350f',
    marginBottom: 2,
  },
  tipDesc: {
    fontSize: 12,
    color: '#92400e',
    lineHeight: 18,
  },
});

export default FormikFormScreen;

代码优化说明
以下是改进后的表格形式:

改进点 原文描述 改进方案
类型定义 分散在组件内 统一提取,使用 TypeScript 5.0+
验证逻辑 硬编码在函数中 独立验证函数,便于单元测试
状态管理 多个 useState 封装为 useFormikState Hook
组件复用 内联渲染 拆分为 FormField、PasswordField 等组件
无障碍 添加 accessibilityLiveRegion 支持
密码强度 简单判断 枚举 + 配置化,易于扩展

OpenHarmony 系统适配指南

  1. 输入事件处理
// 推荐:失焦验证
validateOnBlur={true}
validateOnChange={false}

// 避免:即时验证(OpenHarmony 输入法会频繁触发)
validateOnChange={true}

  1. 键盘避让
<KeyboardAvoidingView
  behavior={Platform.OS === 'ios' ? 'padding' : undefined}
  // OpenHarmony 不需要 behavior,使用默认值即可
>

  1. 无障碍支持
<Text
  accessibilityLiveRegion="polite"
  accessibilityLabel={`错误: ${error}`}
  accessibilityHint={showError ? error : undefined}
>
  {error}
</Text>

  1. 权限配置
// harmony/entry/src/main/module.json5
{
  "module": {
    "requestPermissions": [
      { "name": "ohos.permission.INTERNET" }
    ]
  }
}

验证规则速查指南

Yup 方法 用途 示例
required() 必填验证 yup.string().required(‘不能为空’)
email() 邮箱格式验证 yup.string().email(‘邮箱格式错误’)
min() 最小长度限制 yup.string().min(8, ‘至少8个字符’)
matches() 正则表达式验证 yup.string().matches(/[a-z]/, ‘需包含小写字母’)
oneOf() 值匹配验证 yup.string().oneOf([yup.ref(‘password’)], ‘密码不一致’)Yup 方法 用途 示例

总结

优化后的版本:

验证策略:采用 validateOnBlur 机制,有效减少输入法触发的验证频率
状态管理:通过自定义 Hook 封装,实现逻辑的高效复用
无障碍支持:集成 accessibilityLiveRegion 属性,完善屏幕阅读器兼容性
组件设计:将 FormField 和 PasswordField 等拆分为独立组件,提升可维护性
类型系统:全面支持 TypeScript 类型检查,确保代码安全性

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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐