【HarmonyOS】RN_of_HarmonyOS实战项目_邮箱地址输入

【HarmonyOS】RN of HarmonyOS实战项目:邮箱地址输入与验证

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

项目概述:本文详细介绍在HarmonyOS平台上使用React Native实现智能邮箱地址输入组件,涵盖格式验证、域名检测、实时提示、多邮箱管理等完整功能,提供从基础实现到生产级应用的完整解决方案。


一、项目背景

邮箱地址输入是用户注册、登录、订阅等场景中的核心功能。在HarmonyOS平台上实现完善的邮箱输入功能需要考虑:

  • 格式验证:符合RFC 5322标准的邮箱格式验证
  • 域名检测:验证邮箱域名的MX记录
  • 智能提示:根据输入提供常用邮箱后缀建议
  • 多邮箱管理:支持添加、删除、切换多个邮箱
  • 用户体验:实时验证反馈、友好的错误提示

二、技术架构

2.1 邮箱验证流程

无效
有效
无效域名
有效域名
无MX记录
有MX记录
用户输入邮箱
格式检查
显示格式错误
域名检查
显示域名警告
MX记录查询
显示无法接收邮件
邮箱可用
显示成功提示

2.2 数据结构定义

typescript 复制代码
/**
 * 邮箱验证结果
 */
interface EmailValidationResult {
  email: string;
  isValid: boolean;
  isDeliverable: boolean;  // 是否可接收邮件
  hasMXRecord: boolean;    // 是否有MX记录
  domain: string;
  localPart: string;
  error?: EmailValidationError;
}

/**
 * 邮箱验证错误类型
 */
type EmailValidationError =
  | 'EMPTY'
  | 'INVALID_FORMAT'
  | 'INVALID_DOMAIN'
  | 'NO_MX_RECORD'
  | 'TEMPORARY_ERROR';

/**
 * 常用邮箱域名
 */
const COMMON_EMAIL_DOMAINS = [
  'qq.com',
  '163.com',
  '126.com',
  'gmail.com',
  'outlook.com',
  'hotmail.com',
  'yahoo.com',
  'sina.com',
  'sohu.com',
  'aliyun.com',
  'foxmail.com',
  'icloud.com',
];

三、核心实现代码

3.1 邮箱验证工具类

typescript 复制代码
/**
 * 邮箱验证工具类
 * 提供完整的邮箱格式验证和域名检测功能
 *
 * @platform HarmonyOS 2.0+
 * @react-native 0.72+
 */
export class EmailValidator {
  /**
   * 基础邮箱格式正则
   * 符合RFC 5322标准的简化版本
   */
  private static readonly EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;

  /**
   * 更严格的邮箱验证正则
   */
  private static readonly STRICT_EMAIL_REGEX = /^[a-zA-Z0-9]+([._+-][a-zA-Z0-9]+)*@[a-zA-Z0-9]+([.-][a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;

  /**
   * 验证邮箱格式
   */
  static validateFormat(email: string): { isValid: boolean; error?: EmailValidationError } {
    // 空值检查
    if (!email || !email.trim()) {
      return { isValid: false, error: 'EMPTY' };
    }

    const trimmed = email.trim().toLowerCase();

    // 长度检查
    if (trimmed.length > 254) {
      return { isValid: false, error: 'INVALID_FORMAT' };
    }

    // 格式检查
    if (!this.STRICT_EMAIL_REGEX.test(trimmed)) {
      return { isValid: false, error: 'INVALID_FORMAT' };
    }

    // 检查是否有多个@
    if ((trimmed.match(/@/g) || []).length !== 1) {
      return { isValid: false, error: 'INVALID_FORMAT' };
    }

    // 分割本地部分和域名
    const [localPart, domain] = trimmed.split('@');

    // 本地部分检查
    if (localPart.length > 64) {
      return { isValid: false, error: 'INVALID_FORMAT' };
    }

    // 域名检查
    if (!this.isValidDomain(domain)) {
      return { isValid: false, error: 'INVALID_DOMAIN' };
    }

    return { isValid: true };
  }

  /**
   * 验证域名格式
   */
  private static isValidDomain(domain: string): boolean {
    // 域名长度检查
    if (domain.length > 253 || domain.length < 3) {
      return false;
    }

    // 域名格式检查
    const domainRegex = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$/i;
    if (!domainRegex.test(domain)) {
      return false;
    }

    // TLD检查
    const tld = domain.split('.').pop();
    if (!tld || tld.length < 2) {
      return false;
    }

    return true;
  }

  /**
   * 解析邮箱地址
   */
  static parse(email: string): { localPart: string; domain: string; full: string } | null {
    const validation = this.validateFormat(email);
    if (!validation.isValid) {
      return null;
    }

    const trimmed = email.trim().toLowerCase();
    const [localPart, domain] = trimmed.split('@');

    return {
      localPart,
      domain,
      full: trimmed,
    };
  }

  /**
   * 生成邮箱建议
   */
  static suggestDomains(localPart: string): string[] {
    return COMMON_EMAIL_DOMAINS.map(domain => `${localPart}@${domain}`);
  }

  /**
   * 自动补全邮箱
   */
  static autoComplete(partial: string): string | null {
    const trimmed = partial.trim().toLowerCase();

    // 如果已经包含@,尝试补全域名
    if (trimmed.includes('@')) {
      const [localPart, domainPart] = trimmed.split('@');

      if (!domainPart) {
        // 只有@符号,返回第一个常用域名
        return `${localPart}@${COMMON_EMAIL_DOMAINS[0]}`;
      }

      // 查找匹配的域名
      const matched = COMMON_EMAIL_DOMAINS.find(domain =>
        domain.startsWith(domainPart)
      );

      if (matched) {
        return `${localPart}@${matched}`;
      }
    } else {
      // 没有@符号,返回第一个建议
      return `${trimmed}@${COMMON_EMAIL_DOMAINS[0]}`;
    }

    return null;
  }

  /**
   * 检查是否为一次性邮箱
   */
  static isDisposableEmail(domain: string): boolean {
    const disposableDomains = [
      'tempmail.com',
      'guerrillamail.com',
      'mailinator.com',
      '10minutemail.com',
    ];
    return disposableDomains.some(d => domain.endsWith(d));
  }

  /**
   * 隐藏邮箱地址(用于显示)
   */
  static obscure(email: string): string {
    const parsed = this.parse(email);
    if (!parsed) return email;

    const { localPart, domain } = parsed;

    if (localPart.length <= 3) {
      return `${localPart[0]}***@${domain}`;
    }

    const visibleChars = Math.ceil(localPart.length / 3);
    const obscuredPart =
      localPart.substring(0, visibleChars) +
      '*'.repeat(localPart.length - visibleChars * 2) +
      localPart.substring(localPart.length - visibleChars);

    return `${obscuredPart}@${domain}`;
  }

  /**
   * 提取邮箱头像URL
   */
  static getAvatarURL(email: string, size: number = 200): string {
    const trimmed = email.trim().toLowerCase();
    const hash = this.md5(trimmed);
    return `https://www.gravatar.com/avatar/${hash}?s=${size}&d=identicon`;
  }

  /**
   * 简单的MD5实现(用于Gravatar)
   */
  private static md5(string: string): string {
    // 实际项目中应使用crypto-js等库
    // 这里返回一个模拟值
    return Array.from(string)
      .reduce((hash, char) => ((hash << 5) - hash + char.charCodeAt(0)) | 0, 0)
      .toString(16)
      .padStart(32, '0');
  }
}

3.2 邮箱输入组件

typescript 复制代码
/**
 * 智能邮箱输入组件
 * 支持实时验证、自动补全、域名建议等功能
 */
import React, { useState, useCallback, useRef, useEffect } from 'react';
import {
  View,
  TextInput,
  Text,
  TouchableOpacity,
  StyleSheet,
  ScrollView,
  ActivityIndicator,
  Image,
} from 'react-native';
import { EmailValidator, EmailValidationError } from './EmailValidator';

interface EmailInputProps {
  placeholder?: string;
  onEmailChange?: (email: string, isValid: boolean) => void;
  onEmailSubmit?: (email: string) => void;
  showSuggestions?: boolean;
  allowDisposableEmail?: boolean;
}

export const EmailInput: React.FC<EmailInputProps> = ({
  placeholder = '请输入邮箱地址',
  onEmailChange,
  onEmailSubmit,
  showSuggestions = true,
  allowDisposableEmail = false,
}) => {
  const [email, setEmail] = useState('');
  const [validation, setValidation] = useState<{
    isValid: boolean;
    error?: EmailValidationError;
  }>({ isValid: false });
  const [isValidating, setIsValidating] = useState(false);
  const [showDomainSuggestions, setShowDomainSuggestions] = useState(false);
  const [suggestions, setSuggestions] = useState<string[]>([]);

  const inputRef = useRef<TextInput>(null);
  const debounceTimer = useRef<NodeJS.Timeout>();

  /**
   * 获取错误提示信息
   */
  const getErrorMessage = (error?: EmailValidationError): string => {
    switch (error) {
      case 'EMPTY':
        return '邮箱地址不能为空';
      case 'INVALID_FORMAT':
        return '邮箱格式不正确';
      case 'INVALID_DOMAIN':
        return '域名格式不正确';
      case 'NO_MX_RECORD':
        return '该邮箱无法接收邮件';
      case 'TEMPORARY_ERROR':
        return '网络错误,请稍后重试';
      default:
        return '邮箱地址无效';
    }
  };

  /**
   * 验证邮箱
   */
  const validateEmail = useCallback((emailText: string) => {
    if (debounceTimer.current) {
      clearTimeout(debounceTimer.current);
    }

    if (!emailText) {
      setValidation({ isValid: false });
      setShowDomainSuggestions(false);
      onEmailChange?.('', false);
      return;
    }

    setIsValidating(true);

    debounceTimer.current = setTimeout(() => {
      const result = EmailValidator.validateFormat(emailText);

      setValidation(result);
      setIsValidating(false);

      onEmailChange?.(emailText, result.isValid);

      // 显示域名建议
      if (showSuggestions && emailText.includes('@')) {
        const [localPart, domainPart] = emailText.split('@');

        if (domainPart && domainPart.length > 0) {
          const matches = EmailValidator.suggestDomains(localPart).filter(email =>
            email.includes(domainPart)
          );
          setSuggestions(matches.slice(0, 3));
          setShowDomainSuggestions(matches.length > 1);
        } else {
          setSuggestions(EmailValidator.suggestDomains(localPart).slice(0, 3));
          setShowDomainSuggestions(true);
        }
      } else {
        setShowDomainSuggestions(false);
      }
    }, 300);
  }, [showSuggestions, onEmailChange]);

  /**
   * 处理输入变化
   */
  const handleChange = useCallback((text: string) => {
    setEmail(text);
    validateEmail(text);
  }, [validateEmail]);

  /**
   * 应用建议
   */
  const applySuggestion = useCallback((suggestedEmail: string) => {
    setEmail(suggestedEmail);
    setValidation({ isValid: true });
    setShowDomainSuggestions(false);
    onEmailChange?.(suggestedEmail, true);
    inputRef.current?.blur();
  }, [onEmailChange]);

  /**
   * 清空输入
   */
  const handleClear = useCallback(() => {
    setEmail('');
    setValidation({ isValid: false });
    setShowDomainSuggestions(false);
    onEmailChange?.('', false);
    inputRef.current?.focus();
  }, [onEmailChange]);

  /**
   * 提交邮箱
   */
  const handleSubmit = useCallback(() => {
    if (validation.isValid) {
      onEmailSubmit?.(email);
    }
  }, [validation.isValid, email, onEmailSubmit]);

  const parsed = EmailValidator.parse(email);

  return (
    <View style={styles.container}>
      {/* 输入框 */}
      <View style={styles.inputContainer}>
        <TextInput
          ref={inputRef}
          style={styles.input}
          value={email}
          onChangeText={handleChange}
          onSubmitEditing={handleSubmit}
          placeholder={placeholder}
          placeholderTextColor="#999"
          keyboardType="email-address"
          autoCapitalize="none"
          autoCorrect={false}
          textContentType="emailAddress"
          returnKeyType="done"
        />

        {email.length > 0 && (
          <TouchableOpacity onPress={handleClear} style={styles.clearButton}>
            <Text style={styles.clearIcon}>×</Text>
          </TouchableOpacity>
        )}

        {isValidating && (
          <ActivityIndicator size="small" color="#007AFF" style={styles.spinner} />
        )}
      </View>

      {/* 验证状态指示器 */}
      {email.length > 0 && !isValidating && (
        <View style={styles.statusContainer}>
          {validation.isValid ? (
            <View style={styles.statusSuccess}>
              <Text style={styles.statusIcon}>✓</Text>
              <Text style={styles.statusText}>邮箱格式正确</Text>
            </View>
          ) : (
            <View style={styles.statusError}>
              <Text style={styles.statusIcon}>!</Text>
              <Text style={styles.statusText}>{getErrorMessage(validation.error)}</Text>
            </View>
          )}
        </View>
      )}

      {/* 域名建议 */}
      {showDomainSuggestions && suggestions.length > 0 && (
        <View style={styles.suggestionsContainer}>
          <Text style={styles.suggestionsTitle}>选择邮箱域名</Text>
          <ScrollView
            horizontal
            showsHorizontalScrollIndicator={false}
            contentContainerStyle={styles.suggestionsList}
          >
            {suggestions.map((suggestedEmail, index) => (
              <TouchableOpacity
                key={index}
                style={styles.suggestionItem}
                onPress={() => applySuggestion(suggestedEmail)}
              >
                <Text style={styles.suggestionText}>{suggestedEmail}</Text>
              </TouchableOpacity>
            ))}
          </ScrollView>
        </View>
      )}

      {/* 邮箱头像预览 */}
      {validation.isValid && parsed && (
        <View style={styles.previewContainer}>
          <Image
            source={{ uri: EmailValidator.getAvatarURL(email, 80) }}
            style={styles.avatar}
          />
          <View style={styles.previewInfo}>
            <Text style={styles.previewEmail}>{email}</Text>
            <Text style={styles.previewDomain}>@{parsed.domain}</Text>
          </View>
        </View>
      )}
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    width: '100%',
  },
  inputContainer: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#fff',
    borderWidth: 1,
    borderColor: '#E5E5E5',
    borderRadius: 12,
    paddingHorizontal: 16,
  },
  input: {
    flex: 1,
    height: 50,
    fontSize: 16,
    color: '#333',
  },
  clearButton: {
    width: 28,
    height: 28,
    borderRadius: 14,
    backgroundColor: '#E0E0E0',
    justifyContent: 'center',
    alignItems: 'center',
    marginRight: 8,
  },
  clearIcon: {
    fontSize: 18,
    color: '#666',
    fontWeight: 'bold',
  },
  spinner: {
    marginRight: 8,
  },
  statusContainer: {
    marginTop: 10,
  },
  statusSuccess: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#E8F5E9',
    paddingHorizontal: 12,
    paddingVertical: 8,
    borderRadius: 8,
  },
  statusError: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#FFEBEE',
    paddingHorizontal: 12,
    paddingVertical: 8,
    borderRadius: 8,
  },
  statusIcon: {
    fontSize: 14,
    marginRight: 6,
  },
  statusText: {
    fontSize: 13,
  },
  suggestionsContainer: {
    marginTop: 12,
  },
  suggestionsTitle: {
    fontSize: 13,
    color: '#999',
    marginBottom: 8,
  },
  suggestionsList: {
    gap: 8,
  },
  suggestionItem: {
    backgroundColor: '#F5F5F5',
    paddingHorizontal: 16,
    paddingVertical: 10,
    borderRadius: 8,
  },
  suggestionText: {
    fontSize: 14,
    color: '#333',
  },
  previewContainer: {
    marginTop: 16,
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#F5F5F5',
    padding: 12,
    borderRadius: 12,
  },
  avatar: {
    width: 50,
    height: 50,
    borderRadius: 25,
    marginRight: 12,
  },
  previewInfo: {
    flex: 1,
  },
  previewEmail: {
    fontSize: 15,
    fontWeight: '600',
    color: '#333',
    marginBottom: 2,
  },
  previewDomain: {
    fontSize: 13,
    color: '#999',
  },
});

export default EmailInput;

3.3 多邮箱管理组件

typescript 复制代码
/**
 * 多邮箱管理组件
 * 支持添加、删除、切换多个邮箱地址
 */
import React, { useState, useCallback } from 'react';
import {
  View,
  Text,
  TouchableOpacity,
  StyleSheet,
  FlatList,
  Alert,
} from 'react-native';
import { EmailValidator } from './EmailValidator';

interface StoredEmail {
  id: string;
  email: string;
  isPrimary: boolean;
  isVerified: boolean;
  addedAt: Date;
}

interface EmailManagerProps {
  emails: StoredEmail[];
  onAddEmail: (email: string) => void;
  onRemoveEmail: (id: string) => void;
  onSetPrimary: (id: string) => void;
  onResendVerification: (id: string) => void;
}

export const EmailManager: React.FC<EmailManagerProps> = ({
  emails,
  onAddEmail,
  onRemoveEmail,
  onSetPrimary,
  onResendVerification,
}) => {
  const [showAddForm, setShowAddForm] = useState(false);

  /**
   * 渲染邮箱项
   */
  const renderEmailItem = useCallback(({ item }: { item: StoredEmail }) => {
    const parsed = EmailValidator.parse(item.email);
    const obscured = EmailValidator.obscure(item.email);

    return (
      <View style={managerStyles.emailItem}>
        <View style={managerStyles.emailInfo}>
          <View style={managerStyles.emailHeader}>
            <Text style={managerStyles.emailText}>{obscured}</Text>
            {item.isPrimary && (
              <View style={managerStyles.primaryBadge}>
                <Text style={managerStyles.primaryBadgeText}>主要</Text>
              </View>
            )}
            {!item.isVerified && (
              <View style={managerStyles.unverifiedBadge}>
                <Text style={managerStyles.unverifiedBadgeText}>未验证</Text>
              </View>
            )}
          </View>
          {parsed && (
            <Text style={managerStyles.domainText}>@{parsed.domain}</Text>
          )}
        </View>

        <View style={managerStyles.actions}>
          {!item.isPrimary && (
            <TouchableOpacity
              style={managerStyles.actionButton}
              onPress={() => onSetPrimary(item.id)}
            >
              <Text style={managerStyles.actionButtonText}>设为主要</Text>
            </TouchableOpacity>
          )}

          {!item.isVerified && (
            <TouchableOpacity
              style={[managerStyles.actionButton, managerStyles.resendButton]}
              onPress={() => onResendVerification(item.id)}
            >
              <Text style={managerStyles.actionButtonText}>重发验证</Text>
            </TouchableOpacity>
          )}

          {emails.length > 1 && (
            <TouchableOpacity
              style={[managerStyles.actionButton, managerStyles.removeButton]}
              onPress={() => {
                Alert.alert(
                  '删除邮箱',
                  '确定要删除此邮箱地址吗?',
                  [
                    { text: '取消', style: 'cancel' },
                    {
                      text: '删除',
                      style: 'destructive',
                      onPress: () => onRemoveEmail(item.id),
                    },
                  ]
                );
              }}
            >
              <Text style={[managerStyles.actionButtonText, managerStyles.removeButtonText]}>
                删除
              </Text>
            </TouchableOpacity>
          )}
        </View>
      </View>
    );
  }, [emails, onSetPrimary, onResendVerification, onRemoveEmail]);

  return (
    <View style={managerStyles.container}>
      <View style={managerStyles.header}>
        <Text style={managerStyles.title}>邮箱地址管理</Text>
        <Text style={managerStyles.subtitle}>已添加 {emails.length} 个邮箱</Text>
      </View>

      <FlatList
        data={emails}
        renderItem={renderEmailItem}
        keyExtractor={(item) => item.id}
        contentContainerStyle={managerStyles.list}
        ItemSeparatorComponent={() => <View style={managerStyles.separator} />}
      />

      <TouchableOpacity
        style={managerStyles.addButton}
        onPress={() => setShowAddForm(true)}
      >
        <Text style={managerStyles.addButtonText}>+ 添加新邮箱</Text>
      </TouchableOpacity>
    </View>
  );
};

const managerStyles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
  },
  header: {
    padding: 20,
    borderBottomWidth: 1,
    borderBottomColor: '#E5E5E5',
  },
  title: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#333',
    marginBottom: 4,
  },
  subtitle: {
    fontSize: 14,
    color: '#999',
  },
  list: {
    paddingVertical: 8,
  },
  emailItem: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingHorizontal: 20,
    paddingVertical: 12,
  },
  emailInfo: {
    flex: 1,
  },
  emailHeader: {
    flexDirection: 'row',
    alignItems: 'center',
    gap: 8,
    marginBottom: 4,
  },
  emailText: {
    fontSize: 15,
    color: '#333',
  },
  primaryBadge: {
    backgroundColor: '#E3F2FD',
    paddingHorizontal: 8,
    paddingVertical: 2,
    borderRadius: 4,
  },
  primaryBadgeText: {
    fontSize: 11,
    color: '#1976D2',
    fontWeight: '600',
  },
  unverifiedBadge: {
    backgroundColor: '#FFF3E0',
    paddingHorizontal: 8,
    paddingVertical: 2,
    borderRadius: 4,
  },
  unverifiedBadgeText: {
    fontSize: 11,
    color: '#F57C00',
    fontWeight: '600',
  },
  domainText: {
    fontSize: 13,
    color: '#999',
  },
  actions: {
    flexDirection: 'row',
    gap: 8,
  },
  actionButton: {
    paddingHorizontal: 12,
    paddingVertical: 6,
    borderRadius: 6,
    backgroundColor: '#F5F5F5',
  },
  actionButtonText: {
    fontSize: 13,
    color: '#666',
    fontWeight: '500',
  },
  resendButton: {
    backgroundColor: '#FFF3E0',
  },
  removeButton: {
    backgroundColor: '#FFEBEE',
  },
  removeButtonText: {
    color: '#C62828',
  },
  separator: {
    height: 1,
    backgroundColor: '#F5F5F5',
    marginLeft: 20,
  },
  addButton: {
    margin: 20,
    backgroundColor: '#007AFF',
    paddingVertical: 14,
    borderRadius: 10,
    alignItems: 'center',
  },
  addButtonText: {
    color: '#fff',
    fontSize: 16,
    fontWeight: '600',
  },
});

3.4 使用示例

typescript 复制代码
/**
 * 邮箱输入组件使用示例
 */
import React, { useState } from 'react';
import {
  View,
  StyleSheet,
  ScrollView,
  Alert,
  SafeAreaView,
} from 'react-native';
import { EmailInput } from './EmailInput';
import { EmailManager, StoredEmail } from './EmailManager';

const EmailInputDemo: React.FC = () => {
  const [emails, setEmails] = useState<StoredEmail[]>([
    {
      id: '1',
      email: 'user@example.com',
      isPrimary: true,
      isVerified: true,
      addedAt: new Date(),
    },
  ]);

  const handleEmailChange = (email: string, isValid: boolean) => {
    console.log('Email changed:', email, 'Valid:', isValid);
  };

  const handleEmailSubmit = (email: string) => {
    Alert.alert('提交成功', `邮箱地址: ${email}`);
  };

  const handleAddEmail = (email: string) => {
    const newEmail: StoredEmail = {
      id: Date.now().toString(),
      email,
      isPrimary: false,
      isVerified: false,
      addedAt: new Date(),
    };
    setEmails([...emails, newEmail]);
  };

  const handleRemoveEmail = (id: string) => {
    setEmails(emails.filter(e => e.id !== id));
  };

  const handleSetPrimary = (id: string) => {
    setEmails(
      emails.map(e => ({
        ...e,
        isPrimary: e.id === id,
      }))
    );
  };

  const handleResendVerification = (id: string) => {
    Alert.alert('验证邮件已发送', '请检查您的邮箱');
  };

  return (
    <SafeAreaView style={demoStyles.container}>
      <ScrollView contentContainerStyle={demoStyles.content}>
        <Text style={demoStyles.title}>邮箱地址输入</Text>

        <EmailInput
          placeholder="请输入邮箱地址"
          onEmailChange={handleEmailChange}
          onEmailSubmit={handleEmailSubmit}
          showSuggestions
        />

        <View style={demoStyles.section}>
          <EmailManager
            emails={emails}
            onAddEmail={handleAddEmail}
            onRemoveEmail={handleRemoveEmail}
            onSetPrimary={handleSetPrimary}
            onResendVerification={handleResendVerification}
          />
        </View>
      </ScrollView>
    </SafeAreaView>
  );
};

const demoStyles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F5F5F5',
  },
  content: {
    padding: 16,
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#333',
    marginBottom: 20,
    textAlign: 'center',
  },
  section: {
    marginTop: 30,
    borderRadius: 12,
    overflow: 'hidden',
    height: 400,
  },
});

export default EmailInputDemo;

四、最佳实践

功能 实现方式 效果
实时验证 防抖300ms 减少不必要的验证
域名建议 显示常用邮箱域名 提升输入效率
邮箱隐藏 本地部分部分隐藏 保护用户隐私
头像集成 Gravatar API 显示邮箱头像
多邮箱管理 添加、删除、切换 灵活的邮箱管理

五、总结

本文详细介绍了在HarmonyOS平台上实现邮箱地址输入的完整方案,涵盖:

  1. 格式验证:RFC标准的邮箱格式验证
  2. 智能功能:域名建议、自动补全
  3. 用户体验:实时验证、错误提示
  4. 高级功能:多邮箱管理、头像显示

完整项目代码AtomGitDemos

相关推荐
星空22232 小时前
【HarmonyOS】React Native 实战:原生手势交互开发
react native·交互·harmonyos
阿林来了2 小时前
Flutter三方库适配OpenHarmony【flutter_speech】— 错误处理与异常恢复
flutter·harmonyos
爱华晨宇2 小时前
快速清理C盘,释放10GB空间!
harmonyos
阿林来了3 小时前
Flutter三方库适配OpenHarmony【flutter_speech】— 调试技巧与日志分析
flutter·harmonyos
平安的平安3 小时前
【OpenHarmony】React Native鸿蒙实战:Camera 相机组件详解
数码相机·react native·harmonyos
松叶似针3 小时前
Flutter三方库适配OpenHarmony【secure_application】— 调试与问题排查实战
flutter·harmonyos
阿林来了3 小时前
Flutter三方库适配OpenHarmony【flutter_speech】— pubspec.yaml 多平台配置
flutter·harmonyos
funnycoffee1231 天前
Cisco ,H3C,华为配置端口聚合命令(lacp mode)
网络·华为·聚合
ITUnicorn1 天前
【HarmonyOS 6】进度组件实战:打造精美的数据可视化
华为·harmonyos·arkts·鸿蒙·harmonyos6