【HarmonyOS】RN_of_HarmonyOS实战项目_电话号码输入

【HarmonyOS】RN of HarmonyOS实战项目:电话号码输入与验证


🌸你好呀!我是 lbb小魔仙
🌟 感谢陪伴~ 小白博主在线求友
🌿 跟着小白学Linux/Java/Python
📖 专栏汇总:
《Linux》专栏 | 《Java》专栏 | 《Python》专栏

  • [【HarmonyOS】RN of HarmonyOS实战项目:电话号码输入与验证](#【HarmonyOS】RN of HarmonyOS实战项目:电话号码输入与验证)
    • 一、项目背景
    • 二、技术架构
      • [2.1 手机号验证流程](#2.1 手机号验证流程)
      • [2.2 数据结构定义](#2.2 数据结构定义)
    • 三、核心实现代码
      • [3.1 手机号验证工具类](#3.1 手机号验证工具类)
      • [3.2 国家/地区区号选择器](#3.2 国家/地区区号选择器)
      • [3.3 手机号输入组件](#3.3 手机号输入组件)
      • [3.4 验证码输入组件](#3.4 验证码输入组件)
      • [3.5 使用示例](#3.5 使用示例)
    • 四、最佳实践
    • 五、总结

项目概述:本文详细介绍在HarmonyOS平台上使用React Native实现智能电话号码输入组件,涵盖格式验证、国际区号处理、运营商检测、验证码发送等完整功能,提供从基础实现到生产级应用的完整解决方案。


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

一、项目背景

电话号码输入是用户注册、登录、身份验证等场景中的核心功能。在中国市场,电话号码验证尤为重要。在HarmonyOS平台上实现完善的电话号码输入功能需要考虑:

  • 格式验证:支持中国大陆11位手机号验证
  • 国际区号:支持多个国家/地区的区号选择
  • 运营商检测:识别移动、联通、电信等运营商
  • 验证码集成:与短信验证码服务集成
  • 用户体验:实时格式化、空格分隔、错误提示

二、技术架构

2.1 手机号验证流程

无效
有效
中国大陆
其他地区
有效
有效
无效
无效
用户输入手机号
格式检查
显示格式错误
区号检查
11位检查
对应格式检查
运营商识别
显示位数错误
显示运营商信息
启用获取验证码按钮

2.2 数据结构定义

typescript 复制代码
/**
 * 国家/地区区号配置
 */
interface CountryCode {
  code: string;           // 区号,如 +86
  country: string;        // 国家/地区名称
  flag: string;          // 国旗emoji
  iso: string;           // ISO代码
  maxLength: number;     // 最大长度
  pattern: RegExp;      // 验证正则
}

/**
 * 运营商信息
 */
type CarrierType = 'china_mobile' | 'china_unicom' | 'china_telecom' | 'unknown';

interface CarrierInfo {
  type: CarrierType;
  name: string;
  color: string;
}

/**
 * 手机号验证结果
 */
interface PhoneNumberValidationResult {
  phoneNumber: string;    // 标准化的手机号
  countryCode: string;   // 国际区号
  carrier?: CarrierInfo; // 运营商信息
  isValid: boolean;      // 是否有效
  error?: string;       // 错误信息
}

三、核心实现代码

3.1 手机号验证工具类

typescript 复制代码
/**
 * 手机号验证工具类
 * 提供完整的手机号格式验证、运营商识别等功能
 *
 * @platform HarmonyOS 2.0+
 * @react-native 0.72+
 */
export class PhoneValidator {
  /**
   * 中国大陆手机号段
   */
  private static readonly CHINA_MOBILE_PREFIXES = [
    '134', '135', '136', '137', '138', '139',
    '147', '150', '151', '152', '157', '158', '159',
    '172', '178', '182', '183', '184', '187', '188',
    '195', '197', '198',
  ];

  private static readonly CHINA_UNICOM_PREFIXES = [
    '130', '131', '132', '145', '155', '156',
    '166', '171', '175', '176', '185', '186',
    '196',
  ];

  private static readonly CHINA_TELECOM_PREFIXES = [
    '133', '149', '153', '173', '174', '177',
    '180', '181', '189', '190', '191', '193', '199',
  ];

  /**
   * 中国大陆手机号验证正则
   */
  private static readonly CHINA_PHONE_REGEX = /^1[3-9]\d{9}$/;

  /**
   * 验证中国大陆手机号
   */
  static validateChinaPhone(phone: string): {
    isValid: boolean;
    error?: string;
    carrier?: CarrierInfo;
  } {
    // 去除空格和分隔符
    const cleaned = phone.replace(/[\s-]/g, '');

    // 长度检查
    if (cleaned.length !== 11) {
      return {
        isValid: false,
        error: cleaned.length < 11 ? '手机号位数不足' : '手机号位数过多',
      };
    }

    // 格式检查
    if (!this.CHINA_PHONE_REGEX.test(cleaned)) {
      return {
        isValid: false,
        error: '手机号格式不正确',
      };
    }

    // 识别运营商
    const prefix = cleaned.substring(0, 3);
    let carrier: CarrierInfo | undefined;

    if (this.CHINA_MOBILE_PREFIXES.includes(prefix)) {
      carrier = {
        type: 'china_mobile',
        name: '中国移动',
        color: '#1E88E5',
      };
    } else if (this.CHINA_UNICOM_PREFIXES.includes(prefix)) {
      carrier = {
        type: 'china_unicom',
        name: '中国联通',
        color: '#E53935',
      };
    } else if (this.CHINA_TELECOM_PREFIXES.includes(prefix)) {
      carrier = {
        type: 'china_telecom',
        name: '中国电信',
        color: '#FB8C00',
      };
    }

    return {
      isValid: true,
      phoneNumber: cleaned,
      carrier,
    };
  }

  /**
   * 格式化手机号显示(添加空格)
   */
  static formatPhoneNumber(phone: string, countryCode: string = '+86'): string {
    const cleaned = phone.replace(/\D/g, '');

    if (countryCode === '+86' && cleaned.length === 11) {
      // 中国大陆手机号:3-4-4格式
      return `${cleaned.substring(0, 3)} ${cleaned.substring(3, 7)} ${cleaned.substring(7)}`;
    }

    // 默认格式:每3-4位分隔
    const groups: string[] = [];
    for (let i = 0; i < cleaned.length; i += 3) {
      const group = cleaned.substring(i, i + 3);
      if (group) {
        groups.push(group);
      }
    }
    return groups.join(' ');
  }

  /**
   * 隐藏手机号中间四位
   */
  static obscurePhone(phone: string): string {
    if (phone.length === 11) {
      return `${phone.substring(0, 3)} **** ${phone.substring(7)}`;
    }
    return phone;
  }

  /**
   * 生成验证码倒计时文本
   */
  static getCountdownText(seconds: number): string {
    if (seconds <= 0) {
      return '获取验证码';
    }
    return `${seconds}秒后重试`;
  }

  /**
   * 验证验证码格式
   */
  static validateCode(code: string, length: number = 6): boolean {
    return /^\d+$/.test(code) && code.length === length;
  }
}

3.2 国家/地区区号选择器

typescript 复制代码
/**
 * 常用国家/地区区号数据
 */
export const COUNTRY_CODES: CountryCode[] = [
  { code: '+86', country: '中国', flag: '🇨🇳', iso: 'CN', maxLength: 11, pattern: /^1[3-9]\d{9}$/ },
  { code: '+1', country: '美国/加拿大', flag: '🇺🇸', iso: 'US', maxLength: 10, pattern: /^\d{10}$/ },
  { code: '+44', country: '英国', flag: '🇬🇧', iso: 'GB', maxLength: 10, pattern: /^7\d{9}$/ },
  { code: '+81', country: '日本', flag: '🇯🇵', iso: 'JP', maxLength: 11, pattern: /^[789]\d{9}$/ },
  { code: '+82', country: '韩国', flag: '🇰🇷', iso: 'KR', maxLength: 10, pattern: /^1[016789]\d{7,8}$/ },
  { code: '+852', country: '中国香港', flag: '🇭🇰', iso: 'HK', maxLength: 8, pattern: /^[5689]\d{7}$/ },
  { code: '+853', country: '中国澳门', flag: '🇲🇴', iso: 'MO', maxLength: 8, pattern: /^6\d{7}$/ },
  { code: '+886', country: '中国台湾', flag: '🇹🇼', iso: 'TW', maxLength: 10, pattern: /^9\d{8}$/ },
  { code: '+61', country: '澳大利亚', flag: '🇦🇺', iso: 'AU', maxLength: 9, pattern: /^4\d{8}$/ },
  { code: '+49', country: '德国', flag: '🇩🇪', iso: 'DE', maxLength: 11, pattern: /^1[5-7]\d{8,9}$/ },
  { code: '+33', country: '法国', flag: '🇫🇷', iso: 'FR', maxLength: 9, pattern: /^[67]\d{8}$/ },
  { code: '+65', country: '新加坡', flag: '🇸🇬', iso: 'SG', maxLength: 8, pattern: /^[89]\d{7}$/ },
];

/**
 * 国家/地区区号选择器组件
 */
import React, { useState, useCallback } from 'react';
import {
  View,
  Text,
  TouchableOpacity,
  StyleSheet,
  Modal,
  FlatList,
  TextInput,
  SafeAreaView,
} from 'react-native';

interface CountryCodePickerProps {
  visible: boolean;
  selected: CountryCode;
  onSelect: (country: CountryCode) => void;
  onClose: () => void;
}

export const CountryCodePicker: React.FC<CountryCodePickerProps> = ({
  visible,
  selected,
  onSelect,
  onClose,
}) => {
  const [search, setSearch] = useState('');

  const filteredCountries = COUNTRY_CODES.filter(
    country =>
      country.country.includes(search) ||
      country.code.includes(search) ||
      country.iso.toLowerCase().includes(search.toLowerCase())
  );

  const renderCountry = useCallback(({ item }: { item: CountryCode }) => (
    <TouchableOpacity
      style={[
        pickerStyles.countryItem,
        selected.code === item.code && pickerStyles.countryItemActive,
      ]}
      onPress={() => {
        onSelect(item);
        onClose();
      }}
    >
      <Text style={pickerStyles.flag}>{item.flag}</Text>
      <Text style={pickerStyles.countryName}>{item.country}</Text>
      <Text style={pickerStyles.countryCode}>{item.code}</Text>
    </TouchableOpacity>
  ), [selected, onSelect, onClose]);

  return (
    <Modal visible={visible} animationType="slide" transparent>
      <SafeAreaView style={pickerStyles.container}>
        <View style={pickerStyles.content}>
          {/* 头部 */}
          <View style={pickerStyles.header}>
            <Text style={pickerStyles.title}>选择国家/地区</Text>
            <TouchableOpacity onPress={onClose}>
              <Text style={pickerStyles.closeButton}>完成</Text>
            </TouchableOpacity>
          </View>

          {/* 搜索框 */}
          <View style={pickerStyles.searchContainer}>
            <TextInput
              style={pickerStyles.searchInput}
              value={search}
              onChangeText={setSearch}
              placeholder="搜索国家或区号"
              placeholderTextColor="#999"
            />
          </View>

          {/* 国家列表 */}
          <FlatList
            data={filteredCountries}
            renderItem={renderCountry}
            keyExtractor={(item) => item.code}
            ItemSeparatorComponent={() => <View style={pickerStyles.separator} />}
          />
        </View>
      </SafeAreaView>
    </Modal>
  );
};

const pickerStyles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: 'rgba(0,0,0,0.5)',
  },
  content: {
    flex: 1,
    marginTop: 100,
    backgroundColor: '#fff',
    borderTopLeftRadius: 20,
    borderTopRightRadius: 20,
  },
  header: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    padding: 16,
    borderBottomWidth: 1,
    borderBottomColor: '#E5E5E5',
  },
  title: {
    fontSize: 18,
    fontWeight: 'bold',
    color: '#333',
  },
  closeButton: {
    fontSize: 16,
    color: '#007AFF',
  },
  searchContainer: {
    padding: 16,
    borderBottomWidth: 1,
    borderBottomColor: '#E5E5E5',
  },
  searchInput: {
    backgroundColor: '#F5F5F5',
    borderRadius: 10,
    paddingHorizontal: 16,
    paddingVertical: 12,
    fontSize: 15,
  },
  countryItem: {
    flexDirection: 'row',
    alignItems: 'center',
    padding: 16,
  },
  countryItemActive: {
    backgroundColor: '#E3F2FD',
  },
  flag: {
    fontSize: 24,
    marginRight: 12,
  },
  countryName: {
    flex: 1,
    fontSize: 15,
    color: '#333',
  },
  countryCode: {
    fontSize: 15,
    color: '#666',
    fontWeight: '500',
  },
  separator: {
    height: 1,
    backgroundColor: '#F5F5F5',
    marginLeft: 52,
  },
});

3.3 手机号输入组件

typescript 复制代码
/**
 * 手机号输入组件
 * 支持区号选择、实时验证、运营商识别、验证码发送等功能
 */
import React, { useState, useCallback, useRef, useEffect } from 'react';
import {
  View,
  TextInput,
  Text,
  TouchableOpacity,
  StyleSheet,
  ActivityIndicator,
  Alert,
} from 'react-native';
import { PhoneValidator } from './PhoneValidator';
import { CountryCodePicker, COUNTRY_CODES } from './CountryCodePicker';

interface PhoneInputProps {
  placeholder?: string;
  onPhoneChange?: (phone: string, isValid: boolean) => void;
  onPhoneSubmit?: (phone: string) => void;
  enableVerificationCode?: boolean;
  onSendCode?: (phone: string) => void;
}

export const PhoneInput: React.FC<PhoneInputProps> = ({
  placeholder = '请输入手机号',
  onPhoneChange,
  onPhoneSubmit,
  enableVerificationCode = true,
  onSendCode,
}) => {
  const [selectedCountry, setSelectedCountry] = useState(COUNTRY_CODES[0]);
  const [phoneNumber, setPhoneNumber] = useState('');
  const [validation, setValidation] = useState<{
    isValid: boolean;
    error?: string;
    carrier?: any;
  }>({ isValid: false });

  const [showCountryPicker, setShowCountryPicker] = useState(false);
  const [countdown, setCountdown] = useState(0);
  const [sendingCode, setSendingCode] = useState(false);

  const countdownRef = useRef<NodeJS.Timeout>();

  /**
   * 验证手机号
   */
  const validatePhone = useCallback((phone: string) => {
    if (!phone) {
      setValidation({ isValid: false });
      onPhoneChange?.('', false);
      return;
    }

    if (selectedCountry.code === '+86') {
      const result = PhoneValidator.validateChinaPhone(phone);
      setValidation(result);
      onPhoneChange?.(phone, result.isValid);
    } else {
      // 其他国家/地区的验证
      const cleaned = phone.replace(/\D/g, '');
      const isValid = selectedCountry.pattern.test(cleaned);
      setValidation({
        isValid,
        error: isValid ? undefined : '手机号格式不正确',
      });
      onPhoneChange?.(phone, isValid);
    }
  }, [selectedCountry, onPhoneChange]);

  /**
   * 处理手机号输入
   */
  const handlePhoneChange = useCallback((text: string) => {
    // 只允许输入数字
    const cleaned = text.replace(/\D/g, '');

    // 限制最大长度
    const maxLength = selectedCountry.maxLength;
    const truncated = cleaned.substring(0, maxLength);

    setPhoneNumber(truncated);
    validatePhone(truncated);
  }, [selectedCountry.maxLength, validatePhone]);

  /**
   * 发送验证码
   */
  const handleSendCode = useCallback(async () => {
    if (!validation.isValid) {
      Alert.alert('提示', '请输入正确的手机号');
      return;
    }

    setSendingCode(true);

    try {
      // 调用发送验证码接口
      await onSendCode?.(phoneNumber);

      // 开始倒计时
      setCountdown(60);
      countdownRef.current = setInterval(() => {
        setCountdown(prev => {
          if (prev <= 1) {
            clearInterval(countdownRef.current);
            return 0;
          }
          return prev - 1;
        });
      }, 1000);

      Alert.alert('验证码已发送', `验证码已发送至 ${phoneNumber}`);
    } catch (error) {
      Alert.alert('发送失败', '验证码发送失败,请稍后重试');
    } finally {
      setSendingCode(false);
    }
  }, [phoneNumber, validation.isValid, onSendCode]);

  /**
   * 清理定时器
   */
  useEffect(() => {
    return () => {
      if (countdownRef.current) {
        clearInterval(countdownRef.current);
      }
    };
  }, []);

  /**
   * 提交手机号
   */
  const handleSubmit = useCallback(() => {
    if (validation.isValid) {
      onPhoneSubmit?.(phoneNumber);
    }
  }, [validation.isValid, phoneNumber, onPhoneSubmit]);

  return (
    <View style={styles.container}>
      <View style={styles.inputContainer}>
        {/* 区号选择 */}
        <TouchableOpacity
          style={styles.countrySelector}
          onPress={() => setShowCountryPicker(true)}
        >
          <Text style={styles.flag}>{selectedCountry.flag}</Text>
          <Text style={styles.countryCode}>{selectedCountry.code}</Text>
          <Text style={styles.dropdownIcon}>▼</Text>
        </TouchableOpacity>

        {/* 手机号输入 */}
        <TextInput
          style={styles.input}
          value={PhoneValidator.formatPhoneNumber(phoneNumber, selectedCountry.code)}
          onChangeText={handlePhoneChange}
          onSubmitEditing={handleSubmit}
          placeholder={placeholder}
          placeholderTextColor="#999"
          keyboardType="phone-pad"
          maxLength={selectedCountry.maxLength + 2} // 考虑空格
          returnKeyType="done"
        />

        {/* 验证码按钮 */}
        {enableVerificationCode && (
          <TouchableOpacity
            style={[
              styles.codeButton,
              !validation.isValid && countdown === 0 && styles.codeButtonDisabled,
            ]}
            onPress={handleSendCode}
            disabled={!validation.isValid || countdown > 0 || sendingCode}
          >
            {sendingCode ? (
              <ActivityIndicator size="small" color="#007AFF" />
            ) : (
              <Text
                style={[
                  styles.codeButtonText,
                  (!validation.isValid || countdown > 0) && styles.codeButtonTextDisabled,
                ]}
              >
                {PhoneValidator.getCountdownText(countdown)}
              </Text>
            )}
          </TouchableOpacity>
        )}
      </View>

      {/* 验证状态 */}
      {phoneNumber.length > 0 && (
        <View style={styles.statusContainer}>
          {validation.isValid ? (
            <View style={styles.statusSuccess}>
              <Text style={styles.statusIcon}>✓</Text>
              <Text style={styles.statusText}>手机号格式正确</Text>
              {validation.carrier && (
                <View
                  style={[
                    styles.carrierBadge,
                    { backgroundColor: validation.carrier.color + '20' },
                  ]}
                >
                  <Text
                    style={[styles.carrierText, { color: validation.carrier.color }]}
                  >
                    {validation.carrier.name}
                  </Text>
                </View>
              )}
            </View>
          ) : (
            <View style={styles.statusError}>
              <Text style={styles.statusIcon}>!</Text>
              <Text style={styles.statusText}>{validation.error}</Text>
            </View>
          )}
        </View>
      )}

      {/* 区号选择器 */}
      <CountryCodePicker
        visible={showCountryPicker}
        selected={selectedCountry}
        onSelect={setSelectedCountry}
        onClose={() => setShowCountryPicker(false)}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    width: '100%',
  },
  inputContainer: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#fff',
    borderWidth: 1,
    borderColor: '#E5E5E5',
    borderRadius: 12,
    paddingHorizontal: 12,
  },
  countrySelector: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingVertical: 12,
    paddingHorizontal: 8,
    borderRightWidth: 1,
    borderRightColor: '#E5E5E5',
  },
  flag: {
    fontSize: 20,
    marginRight: 6,
  },
  countryCode: {
    fontSize: 15,
    color: '#333',
    marginRight: 4,
  },
  dropdownIcon: {
    fontSize: 10,
    color: '#999',
  },
  input: {
    flex: 1,
    height: 50,
    fontSize: 16,
    color: '#333',
    paddingHorizontal: 12,
  },
  codeButton: {
    paddingHorizontal: 14,
    paddingVertical: 8,
    marginLeft: 8,
    borderRadius: 8,
    backgroundColor: '#E3F2FD',
  },
  codeButtonDisabled: {
    backgroundColor: '#F5F5F5',
  },
  codeButtonText: {
    fontSize: 13,
    color: '#007AFF',
    fontWeight: '600',
  },
  codeButtonTextDisabled: {
    color: '#999',
  },
  statusContainer: {
    marginTop: 10,
  },
  statusSuccess: {
    flexDirection: 'row',
    alignItems: 'center',
    flexWrap: 'wrap',
  },
  statusError: {
    flexDirection: 'row',
    alignItems: 'center',
  },
  statusIcon: {
    fontSize: 14,
    marginRight: 6,
  },
  statusText: {
    fontSize: 13,
  },
  carrierBadge: {
    paddingHorizontal: 10,
    paddingVertical: 4,
    borderRadius: 6,
    marginLeft: 8,
  },
  carrierText: {
    fontSize: 12,
    fontWeight: '600',
  },
});

export default PhoneInput;

3.4 验证码输入组件

typescript 复制代码
/**
 * 验证码输入组件
 * 支持6位数字验证码输入,自动聚焦下一格
 */
import React, { useState, useRef, useCallback } from 'react';
import {
  View,
  TextInput,
  Text,
  StyleSheet,
  NativeSyntheticEvent,
  TextInputKeyPressEventData,
} from 'react-native';

interface VerificationCodeInputProps {
  length?: number;
  onComplete: (code: string) => void;
  onChange?: (code: string) => void;
}

export const VerificationCodeInput: React.FC<VerificationCodeInputProps> = ({
  length = 6,
  onComplete,
  onChange,
}) => {
  const [code, setCode] = useState<string[]>(Array(length).fill(''));
  const inputs = useRef<(TextInput | null)[]>([]);

  /**
   * 处理输入变化
   */
  const handleChange = useCallback((value: string, index: number) => {
    // 只允许输入数字
    const numeric = value.replace(/\D/g, '').slice(-1);

    if (!numeric) {
      // 处理删除
      const newCode = [...code];
      newCode[index] = '';
      setCode(newCode);
      onChange?.(newCode.join(''));

      // 聚焦到前一个输入框
      if (index > 0) {
        inputs.current[index - 1]?.focus();
      }
      return;
    }

    const newCode = [...code];
    newCode[index] = numeric;
    setCode(newCode);
    onChange?.(newCode.join(''));

    // 自动聚焦到下一个输入框
    if (index < length - 1 && numeric) {
      inputs.current[index + 1]?.focus();
    }

    // 检查是否完成
    if (index === length - 1 || newCode.every(c => c)) {
      const fullCode = newCode.join('');
      if (fullCode.length === length) {
        onComplete(fullCode);
      }
    }
  }, [code, length, onChange, onComplete]);

  /**
   * 处理按键
   */
  const handleKeyPress = useCallback((e: NativeSyntheticEvent<TextInputKeyPressEventData>, index: number) => {
    if (e.nativeEvent.key === 'Backspace' && !code[index] && index > 0) {
      inputs.current[index - 1]?.focus();
    }
  }, [code]);

  return (
    <View style={codeStyles.container}>
      {code.map((digit, index) => (
        <TextInput
          key={index}
          ref={ref => (inputs.current[index] = ref)}
          style={[
            codeStyles.input,
            digit && codeStyles.inputFilled,
          ]}
          value={digit}
          onChangeText={value => handleChange(value, index)}
          onKeyPress={e => handleKeyPress(e, index)}
          keyboardType="number-pad"
          maxLength={1}
          selectTextOnFocus
          textAlign="center"
        />
      ))}
    </View>
  );
};

const codeStyles = StyleSheet.create({
  container: {
    flexDirection: 'row',
    justifyContent: 'center',
    gap: 12,
  },
  input: {
    width: 45,
    height: 55,
    backgroundColor: '#F5F5F5',
    borderRadius: 10,
    borderWidth: 2,
    borderColor: '#E5E5E5',
    fontSize: 24,
    fontWeight: 'bold',
    color: '#333',
  },
  inputFilled: {
    backgroundColor: '#E3F2FD',
    borderColor: '#007AFF',
  },
});

3.5 使用示例

typescript 复制代码
/**
 * 手机号输入组件使用示例
 */
import React, { useState } from 'react';
import {
  View,
  StyleSheet,
  ScrollView,
  Alert,
  SafeAreaView,
  TouchableOpacity,
  Text,
} from 'react-native';
import { PhoneInput } from './PhoneInput';
import { VerificationCodeInput } from './VerificationCodeInput';
import { PhoneValidator } from './PhoneValidator';

const PhoneInputDemo: React.FC = () => {
  const [phone, setPhone] = useState('');
  const [isPhoneValid, setIsPhoneValid] = useState(false);
  const [showCodeInput, setShowCodeInput] = useState(false);

  const handlePhoneChange = (phoneNum: string, isValid: boolean) => {
    setPhone(phoneNum);
    setIsPhoneValid(isValid);
  };

  const handleSendCode = async (phoneNum: string) => {
    // 模拟发送验证码
    return new Promise<void>((resolve) => {
      setTimeout(() => {
        setShowCodeInput(true);
        resolve();
      }, 1000);
    });
  };

  const handleCodeComplete = (code: string) => {
    Alert.alert('验证成功', `手机号: ${phone}\n验证码: ${code}`);
  };

  return (
    <SafeAreaView style={demoStyles.container}>
      <ScrollView contentContainerStyle={demoStyles.content}>
        <Text style={demoStyles.title}>手机号验证</Text>

        <PhoneInput
          placeholder="请输入手机号"
          onPhoneChange={handlePhoneChange}
          enableVerificationCode
          onSendCode={handleSendCode}
        />

        {showCodeInput && (
          <View style={demoStyles.codeSection}>
            <Text style={demoStyles.codeTitle}>请输入验证码</Text>
            <VerificationCodeInput
              onComplete={handleCodeComplete}
              onChange={(code) => console.log('Code:', code)}
            />
            <Text style={demoStyles.codeHint}>
              验证码已发送至 {PhoneValidator.obscurePhone(phone)}
            </Text>
          </View>
        )}
      </ScrollView>
    </SafeAreaView>
  );
};

const demoStyles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F5F5F5',
  },
  content: {
    padding: 16,
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#333',
    marginBottom: 30,
    textAlign: 'center',
  },
  codeSection: {
    marginTop: 40,
    alignItems: 'center',
  },
  codeTitle: {
    fontSize: 16,
    fontWeight: '600',
    color: '#333',
    marginBottom: 20,
  },
  codeHint: {
    fontSize: 13,
    color: '#999',
    marginTop: 16,
  },
});

export default PhoneInputDemo;

四、最佳实践

功能 实现方式 效果
格式化显示 3-4-4空格分隔 便于用户核对
运营商识别 号段前缀匹配 显示运营商信息
验证码倒计时 60秒倒计时 防止频繁发送
区号选择 常用国家区号列表 支持国际手机号
自动聚焦 输入后自动跳转 流畅的输入体验

五、总结

本文详细介绍了在HarmonyOS平台上实现电话号码输入的完整方案,涵盖:

  1. 格式验证:中国大陆11位手机号验证、运营商识别
  2. 国际支持:多国家/地区区号选择
  3. 验证码集成:发送验证码、倒计时控制
  4. 用户体验:格式化显示、实时验证

📕个人领域 :Linux/C++/java/AI

🚀 个人主页有点流鼻涕 · CSDN

💬 座右铭 : "向光而行,沐光而生。"

相关推荐
果粒蹬i2 小时前
【HarmonyOS】RN_of_HarmonyOS实战项目_搜索框样式
华为·harmonyos
松叶似针2 小时前
Flutter三方库适配OpenHarmony【secure_application】— 测试策略与用例设计
flutter·harmonyos
lbb 小魔仙2 小时前
【HarmonyOS】RN_of_HarmonyOS实战项目_密码显示隐藏
华为·harmonyos
平安的平安2 小时前
【OpenHarmony】React Native鸿蒙实战:NetInfo 网络状态详解
网络·react native·harmonyos
果粒蹬i2 小时前
【HarmonyOS】RN_of_HarmonyOS实战项目_自动完成功能
华为·harmonyos
Betelgeuse762 小时前
【Flutter For OpenHarmony】 项目结项复盘
华为·交互·开源软件·鸿蒙
平安的平安3 小时前
【OpenHarmony】React Native鸿蒙实战:SecureStorage 安全存储详解
安全·react native·harmonyos
松叶似针3 小时前
Flutter三方库适配OpenHarmony【secure_application】— 错误处理与异常边界
flutter·harmonyos
果粒蹬i3 小时前
【HarmonyOS】RN_of_HarmonyOS实战项目_邮箱地址输入
华为·harmonyos