【HarmonyOS】RN_of_HarmonyOS实战项目_数字键盘输入

【HarmonyOS】RN of HarmonyOS实战项目:TextInput数字键盘输入优化

项目概述:本文详细介绍在HarmonyOS平台上使用React Native实现优化的数字键盘输入组件,涵盖金额输入、验证码输入、数量选择等多种数字输入场景,提供从基础实现到生产级应用的完整解决方案。


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

一、项目背景

数字键盘输入是移动应用中常见的交互方式,广泛应用于金额输入、密码验证、数量选择等场景。在HarmonyOS平台上实现优化的数字输入功能需要考虑:

  • 键盘类型:根据场景选择合适的键盘类型
  • 格式化显示:金额千分位、小数点处理等
  • 输入限制:最大值、最小值、小数位数控制
  • 用户体验:光标位置、删除操作、快捷输入

二、技术架构

2.1 数字输入类型

typescript 复制代码
/**
 * 数字输入模式
 */
type NumberInputMode =
  | 'integer'        // 整数输入
  | 'decimal'        // 小数输入
  | 'currency'       // 金额输入(带千分位)
  | 'percentage';    // 百分比输入

/**
 * 数字输入配置
 */
interface NumberInputConfig {
  mode: NumberInputMode;
  minValue?: number;
  maxValue?: number;
  decimalPlaces?: number;     // 小数位数
  allowNegative?: boolean;    // 是否允许负数
  prefix?: string;           // 前缀(如¥、$)
  suffix?: string;           // 后缀(如%)
  thousandSeparator?: boolean; // 是否使用千分位
}

2.2 数字处理流程

整数
小数
金额
百分比
有效
无效
用户输入数字
输入模式
过滤非数字字符
处理小数点
添加千分位
添加%符号
范围验证
更新显示
拒绝输入

三、核心实现代码

3.1 数字格式化工具类

typescript 复制代码
/**
 * 数字格式化工具类
 * 提供数字输入的各种格式化和验证功能
 *
 * @platform HarmonyOS 2.0+
 * @react-native 0.72+
 */
export class NumberFormatter {
  /**
   * 格式化金额(带千分位)
   */
  static formatCurrency(
    value: number,
    options: {
      prefix?: string;
      suffix?: string;
      decimalPlaces?: number;
      thousandSeparator?: boolean;
    } = {}
  ): string {
    const {
      prefix = '',
      suffix = '',
      decimalPlaces = 2,
      thousandSeparator = true,
    } = options;

    const fixed = value.toFixed(decimalPlaces);
    const [integerPart, decimalPart] = fixed.split('.');

    let formatted = integerPart;

    if (thousandSeparator) {
      formatted = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
    }

    let result = prefix + formatted;

    if (decimalPlaces > 0) {
      result += '.' + decimalPart;
    }

    return result + suffix;
  }

  /**
   * 解析格式化的金额字符串
   */
  static parseCurrency(
    formatted: string,
    options: {
      prefix?: string;
      suffix?: string;
    } = {}
  ): number {
    const { prefix = '', suffix = '' } = options;

    let cleaned = formatted;

    // 移除前缀
    if (prefix && cleaned.startsWith(prefix)) {
      cleaned = cleaned.substring(prefix.length);
    }

    // 移除后缀
    if (suffix && cleaned.endsWith(suffix)) {
      cleaned = cleaned.substring(0, cleaned.length - suffix.length);
    }

    // 移除千分位
    cleaned = cleaned.replace(/,/g, '');

    return parseFloat(cleaned) || 0;
  }

  /**
   * 格式化百分比
   */
  static formatPercentage(value: number, decimalPlaces: number = 2): string {
    return `${(value * 100).toFixed(decimalPlaces)}%`;
  }

  /**
   * 解析百分比字符串
   */
  static parsePercentage(formatted: string): number {
    return parseFloat(formatted.replace('%', '')) / 100;
  }

  /**
   * 验证数字输入
   */
  static validateNumberInput(
    input: string,
    config: NumberInputConfig
  ): { isValid: boolean; formatted?: string; error?: string } {
    let cleaned = input;

    // 移除前缀后缀
    if (config.prefix && cleaned.startsWith(config.prefix)) {
      cleaned = cleaned.substring(config.prefix.length);
    }
    if (config.suffix && cleaned.endsWith(config.suffix)) {
      cleaned = cleaned.substring(0, cleaned.length - config.suffix.length);
    }

    // 移除千分位
    if (config.thousandSeparator) {
      cleaned = cleaned.replace(/,/g, '');
    }

    // 空值检查
    if (!cleaned || cleaned === '-' || cleaned === '.') {
      return { isValid: true };
    }

    // 负号处理
    if (!config.allowNegative && cleaned.includes('-')) {
      return { isValid: false, error: '不允许负数' };
    }

    // 只允许数字、小数点、负号
    if (!/^[-]?\d*\.?\d*$/.test(cleaned)) {
      return { isValid: false, error: '格式不正确' };
    }

    const value = parseFloat(cleaned);

    // 范围检查
    if (config.minValue !== undefined && value < config.minValue) {
      return {
        isValid: false,
        error: `不能小于 ${config.formatCurrency?.(config.minValue) || config.minValue}`,
      };
    }

    if (config.maxValue !== undefined && value > config.maxValue) {
      return {
        isValid: false,
        error: `不能大于 ${config.formatCurrency?.(config.maxValue) || config.maxValue}`,
      };
    }

    // 小数位数检查
    const decimalIndex = cleaned.indexOf('.');
    if (decimalIndex !== -1) {
      const decimalPlaces = cleaned.length - decimalIndex - 1;
      const maxDecimals = config.decimalPlaces ?? 2;
      if (decimalPlaces > maxDecimals) {
        return { isValid: false, error: `最多${maxDecimals}位小数` };
      }
    }

    return { isValid: true };
  }

  /**
   * 限制输入范围
   */
  static clamp(value: number, min?: number, max?: number): number {
    let clamped = value;
    if (min !== undefined) clamped = Math.max(clamped, min);
    if (max !== undefined) clamped = Math.min(clamped, max);
    return clamped;
  }
}

3.2 数字输入组件

typescript 复制代码
/**
 * 智能数字输入组件
 * 支持多种数字输入模式,自动格式化
 */
import React, { useState, useCallback, useRef, useEffect } from 'react';
import {
  View,
  TextInput,
  Text,
  TouchableOpacity,
  StyleSheet,
  NativeSyntheticEvent,
  TextInputKeyPressEventData,
} from 'react-native';
import { NumberFormatter, NumberInputConfig } from './NumberFormatter';

interface NumberInputProps {
  config: NumberInputConfig;
  value?: number;
  onChange?: (value: number) => void;
  placeholder?: string;
  autoFocus?: boolean;
}

export const NumberInput: React.FC<NumberInputProps> = ({
  config,
  value: controlledValue,
  onChange,
  placeholder = '请输入数字',
  autoFocus = false,
}) => {
  const [displayValue, setDisplayValue] = useState('');
  const [internalValue, setInternalValue] = useState<number>(0);
  const inputRef = useRef<TextInput>(null);

  const value = controlledValue ?? internalValue;

  /**
   * 初始化显示值
   */
  useEffect(() => {
    updateDisplayValue(value);
  }, []);

  /**
   * 更新显示值
   */
  const updateDisplayValue = useCallback((num: number) => {
    let formatted = '';

    switch (config.mode) {
      case 'currency':
        formatted = NumberFormatter.formatCurrency(num, {
          prefix: config.prefix,
          suffix: config.suffix,
          decimalPlaces: config.decimalPlaces,
          thousandSeparator: config.thousandSeparator ?? true,
        });
        break;
      case 'percentage':
        formatted = NumberFormatter.formatPercentage(
          num,
          config.decimalPlaces ?? 2
        );
        break;
      case 'decimal':
        formatted = num.toFixed(config.decimalPlaces ?? 2);
        break;
      case 'integer':
      default:
        formatted = Math.floor(num).toString();
        break;
    }

    setDisplayValue(formatted);
  }, [config]);

  /**
   * 处理输入变化
   */
  const handleChange = useCallback((text: string) => {
    const validation = NumberFormatter.validateNumberInput(text, config);

    if (!validation.isValid && validation.error) {
      return;
    }

    let cleaned = text;

    // 移除格式化符号进行解析
    if (config.mode === 'currency') {
      const parsed = NumberFormatter.parseCurrency(text, {
        prefix: config.prefix,
        suffix: config.suffix,
      });
      setInternalValue(parsed);
      onChange?.(parsed);
    } else if (config.mode === 'percentage') {
      const parsed = NumberFormatter.parsePercentage(text);
      setInternalValue(parsed);
      onChange?.(parsed);
    } else {
      const num = parseFloat(text) || 0;
      const clamped = NumberFormatter.clamp(
        num,
        config.minValue,
        config.maxValue
      );
      setInternalValue(clamped);
      onChange?.(clamped);
    }

    setDisplayValue(text);
  }, [config, onChange]);

  /**
   * 处理失焦
   */
  const handleBlur = useCallback(() => {
    updateDisplayValue(value);
  }, [value, updateDisplayValue]);

  /**
   * 处理聚焦
   */
  const handleFocus = useCallback(() => {
    // 聚焦时显示原始数字
    setDisplayValue(value.toString());
  }, [value]);

  /**
   * 增加数值
   */
  const handleIncrement = useCallback(() => {
    const step = config.mode === 'integer' ? 1 : 0.01;
    const newValue = NumberFormatter.clamp(
      value + step,
      config.minValue,
      config.maxValue
    );
    setInternalValue(newValue);
    onChange?.(newValue);
    updateDisplayValue(newValue);
  }, [value, config, onChange, updateDisplayValue]);

  /**
   * 减少数值
   */
  const handleDecrement = useCallback(() => {
    const step = config.mode === 'integer' ? 1 : 0.01;
    const newValue = NumberFormatter.clamp(
      value - step,
      config.minValue,
      config.maxValue
    );
    setInternalValue(newValue);
    onChange?.(newValue);
    updateDisplayValue(newValue);
  }, [value, config, onChange, updateDisplayValue]);

  /**
   * 获取键盘类型
   */
  const getKeyboardType = () => {
    switch (config.mode) {
      case 'integer':
        return 'number-pad';
      case 'decimal':
      case 'currency':
        return 'decimal-pad';
      default:
        return 'numbers-and-punctuation';
    }
  };

  return (
    <View style={numberStyles.container}>
      <View style={numberStyles.inputWrapper}>
        {config.prefix && (
          <Text style={numberStyles.prefix}>{config.prefix}</Text>
        )}

        <TextInput
          ref={inputRef}
          style={numberStyles.input}
          value={displayValue}
          onChangeText={handleChange}
          onBlur={handleBlur}
          onFocus={handleFocus}
          placeholder={placeholder}
          placeholderTextColor="#999"
          keyboardType={getKeyboardType()}
          autoFocus={autoFocus}
          returnKeyType="done"
        />

        {config.suffix && (
          <Text style={numberStyles.suffix}>{config.suffix}</Text>
        )}

        {config.mode === 'integer' && (
          <View style={numberStyles.stepper}>
            <TouchableOpacity
              onPress={handleDecrement}
              style={numberStyles.stepperButton}
              disabled={config.minValue !== undefined && value <= config.minValue}
            >
              <Text style={numberStyles.stepperIcon}>−</Text>
            </TouchableOpacity>
            <TouchableOpacity
              onPress={handleIncrement}
              style={numberStyles.stepperButton}
              disabled={config.maxValue !== undefined && value >= config.maxValue}
            >
              <Text style={numberStyles.stepperIcon}>+</Text>
            </TouchableOpacity>
          </View>
        )}
      </View>
    </View>
  );
};

const numberStyles = StyleSheet.create({
  container: {
    width: '100%',
  },
  inputWrapper: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#fff',
    borderWidth: 1,
    borderColor: '#E5E5E5',
    borderRadius: 12,
    paddingHorizontal: 16,
  },
  prefix: {
    fontSize: 16,
    color: '#666',
    marginRight: 4,
  },
  input: {
    flex: 1,
    height: 50,
    fontSize: 18,
    color: '#333',
    paddingVertical: 0,
  },
  suffix: {
    fontSize: 16,
    color: '#666',
    marginLeft: 4,
  },
  stepper: {
    flexDirection: 'row',
    gap: 4,
    marginLeft: 8,
  },
  stepperButton: {
    width: 32,
    height: 32,
    borderRadius: 6,
    backgroundColor: '#F5F5F5',
    justifyContent: 'center',
    alignItems: 'center',
  },
  stepperIcon: {
    fontSize: 18,
    color: '#333',
    fontWeight: 'bold',
  },
});

3.3 验证码/密码数字输入组件

typescript 复制代码
/**
 * 验证码数字输入组件
 * 专门用于6位数字验证码输入
 */
import React, { useState, useRef, useCallback, useEffect } from 'react';
import {
  View,
  Text,
  TouchableOpacity,
  StyleSheet,
  TextInput,
} from 'react-native';

interface CodeInputProps {
  length?: number;
  onComplete?: (code: string) => void;
  onChange?: (code: string) => void;
  autoFocus?: boolean;
  secureTextEntry?: boolean;
}

export const CodeInput: React.FC<CodeInputProps> = ({
  length = 6,
  onComplete,
  onChange,
  autoFocus = true,
  secureTextEntry = false,
}) => {
  const [code, setCode] = useState<string[]>(Array(length).fill(''));
  const [focusedIndex, setFocusedIndex] = useState(0);
  const inputs = useRef<(TextInput | null)[]>([]);

  /**
   * 聚焦指定输入框
   */
  const focusInput = useCallback((index: number) => {
    setFocusedIndex(index);
    inputs.current[index]?.focus();
  }, []);

  /**
   * 处理输入变化
   */
  const handleChange = useCallback((value: string, index: number) => {
    const numeric = value.replace(/\D/g, '').slice(-1);
    const newCode = [...code];
    newCode[index] = numeric;
    setCode(newCode);

    onChange?.(newCode.join(''));

    // 自动聚焦下一个
    if (numeric && index < length - 1) {
      focusInput(index + 1);
    }

    // 检查是否完成
    if (newCode.every(c => c)) {
      onComplete?.(newCode.join(''));
    }
  }, [code, length, focusInput, onChange, onComplete]);

  /**
   * 处理按键
   */
  const handleKeyPress = useCallback((e: any, index: number) => {
    if (e.nativeEvent.key === 'Backspace' && !code[index] && index > 0) {
      focusInput(index - 1);
    }
  }, [code, focusInput]);

  /**
   * 处理点击
   */
  const handlePress = useCallback((index: number) => {
    focusInput(index);
  }, [focusInput]);

  /**
   * 初始聚焦
   */
  useEffect(() => {
    if (autoFocus) {
      focusInput(0);
    }
  }, [autoFocus, focusInput]);

  return (
    <View style={codeStyles.container}>
      {code.map((digit, index) => (
        <TouchableOpacity
          key={index}
          style={[
            codeStyles.box,
            focusedIndex === index && codeStyles.boxFocused,
            digit && codeStyles.boxFilled,
          ]}
          onPress={() => handlePress(index)}
          activeOpacity={1}
        >
          <TextInput
            ref={ref => (inputs.current[index] = ref)}
            style={codeStyles.input}
            value={digit}
            onChangeText={value => handleChange(value, index)}
            onKeyPress={e => handleKeyPress(e, index)}
            onFocus={() => setFocusedIndex(index)}
            keyboardType="number-pad"
            maxLength={1}
            secureTextEntry={secureTextEntry}
            selectTextOnFocus
            textAlign="center"
          />
          {!digit && focusedIndex === index && (
            <View style={codeStyles.cursor} />
          )}
        </TouchableOpacity>
      ))}
    </View>
  );
};

const codeStyles = StyleSheet.create({
  container: {
    flexDirection: 'row',
    justifyContent: 'center',
    gap: 10,
  },
  box: {
    width: 48,
    height: 56,
    backgroundColor: '#F5F5F5',
    borderRadius: 12,
    borderWidth: 2,
    borderColor: '#E5E5E5',
    justifyContent: 'center',
    alignItems: 'center',
    overflow: 'hidden',
  },
  boxFocused: {
    borderColor: '#007AFF',
    backgroundColor: '#FFF',
  },
  boxFilled: {
    backgroundColor: '#E3F2FD',
    borderColor: '#007AFF',
  },
  input: {
    width: '100%',
    height: '100%',
    fontSize: 28,
    fontWeight: 'bold',
    color: '#333',
    letterSpacing: 0,
  },
  cursor: {
    position: 'absolute',
    width: 2,
    height: 28,
    backgroundColor: '#007AFF',
    borderRadius: 1,
  },
});

3.4 数字选择器组件

typescript 复制代码
/**
 * 数字选择器组件
 * 带动画效果的数字选择器
 */
import React, { useState, useCallback } from 'react';
import {
  View,
  Text,
  TouchableOpacity,
  StyleSheet,
  Animated,
} from 'react-native';

interface NumberPickerProps {
  value: number;
  onChange: (value: number) => void;
  min?: number;
  max?: number;
  step?: number;
  label?: string;
}

export const NumberPicker: React.FC<NumberPickerProps> = ({
  value,
  onChange,
  min = 0,
  max = 100,
  step = 1,
  label,
}) => {
  const scaleAnim = React.useRef(new Animated.Value(1)).current;

  /**
   * 动画效果
   */
  const animate = useCallback(() => {
    Animated.sequence([
      Animated.timing(scaleAnim, {
        toValue: 0.9,
        duration: 100,
        useNativeDriver: true,
      }),
      Animated.timing(scaleAnim, {
        toValue: 1,
        duration: 100,
        useNativeDriver: true,
      }),
    ]).start();
  }, [scaleAnim]);

  /**
   * 增加数值
   */
  const handleIncrement = useCallback(() => {
    const newValue = Math.min(value + step, max);
    if (newValue !== value) {
      onChange(newValue);
      animate();
    }
  }, [value, step, max, onChange, animate]);

  /**
   * 减少数值
   */
  const handleDecrement = useCallback(() => {
    const newValue = Math.max(value - step, min);
    if (newValue !== value) {
      onChange(newValue);
      animate();
    }
  }, [value, step, min, onChange, animate]);

  return (
    <View style={pickerStyles.container}>
      {label && <Text style={pickerStyles.label}>{label}</Text>}

      <View style={pickerStyles.picker}>
        <TouchableOpacity
          style={[
            pickerStyles.button,
            value <= min && pickerStyles.buttonDisabled,
          ]}
          onPress={handleDecrement}
          disabled={value <= min}
          activeOpacity={0.7}
        >
          <Text style={pickerStyles.buttonText}>−</Text>
        </TouchableOpacity>

        <Animated.View
          style={[
            pickerStyles.valueContainer,
            { transform: [{ scale: scaleAnim }] },
          ]}
        >
          <Text style={pickerStyles.value}>{value}</Text>
        </Animated.View>

        <TouchableOpacity
          style={[
            pickerStyles.button,
            value >= max && pickerStyles.buttonDisabled,
          ]}
          onPress={handleIncrement}
          disabled={value >= max}
          activeOpacity={0.7}
        >
          <Text style={pickerStyles.buttonText}>+</Text>
        </TouchableOpacity>
      </View>
    </View>
  );
};

const pickerStyles = StyleSheet.create({
  container: {
    alignItems: 'center',
  },
  label: {
    fontSize: 14,
    color: '#666',
    marginBottom: 12,
  },
  picker: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#fff',
    borderRadius: 16,
    padding: 8,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 8,
    elevation: 3,
  },
  button: {
    width: 44,
    height: 44,
    borderRadius: 12,
    backgroundColor: '#007AFF',
    justifyContent: 'center',
    alignItems: 'center',
  },
  buttonDisabled: {
    backgroundColor: '#E5E5E5',
  },
  buttonText: {
    fontSize: 24,
    color: '#fff',
    fontWeight: 'bold',
  },
  valueContainer: {
    minWidth: 80,
    height: 44,
    justifyContent: 'center',
    alignItems: 'center',
    marginHorizontal: 16,
  },
  value: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#333',
  },
});

3.5 使用示例

typescript 复制代码
/**
 * 数字输入组件使用示例
 */
import React, { useState } from 'react';
import {
  View,
  StyleSheet,
  ScrollView,
  SafeAreaView,
} from 'react-native';
import { NumberInput } from './NumberInput';
import { CodeInput } from './CodeInput';
import { NumberPicker } from './NumberPicker';

const NumberInputDemo: React.FC = () => {
  const [amount, setAmount] = useState(0);
  const [quantity, setQuantity] = useState(1);
  const [percentage, setPercentage] = useState(0);
  const [code, setCode] = useState('');

  return (
    <SafeAreaView style={demoStyles.container}>
      <ScrollView contentContainerStyle={demoStyles.content}>
        <Text style={demoStyles.title}>数字键盘输入</Text>

        {/* 金额输入 */}
        <View style={demoStyles.section}>
          <Text style={demoStyles.sectionTitle}>金额输入</Text>
          <NumberInput
            config={{
              mode: 'currency',
              prefix: '¥',
              minValue: 0,
              maxValue: 999999,
              decimalPlaces: 2,
            }}
            value={amount}
            onChange={setAmount}
            placeholder="请输入金额"
          />
          <Text style={demoStyles.valueText}>
            当前值: ¥{amount.toFixed(2)}
          </Text>
        </View>

        {/* 数量选择 */}
        <View style={demoStyles.section}>
          <Text style={demoStyles.sectionTitle}>数量选择</Text>
          <NumberPicker
            value={quantity}
            onChange={setQuantity}
            min={1}
            max={99}
            step={1}
            label="商品数量"
          />
        </View>

        {/* 百分比输入 */}
        <View style={demoStyles.section}>
          <Text style={demoStyles.sectionTitle}>百分比输入</Text>
          <NumberInput
            config={{
              mode: 'percentage',
              minValue: 0,
              maxValue: 100,
              decimalPlaces: 1,
            }}
            value={percentage}
            onChange={setPercentage}
            placeholder="请输入百分比"
          />
        </View>

        {/* 验证码输入 */}
        <View style={demoStyles.section}>
          <Text style={demoStyles.sectionTitle}>验证码输入</Text>
          <CodeInput
            length={6}
            onChange={setCode}
            onComplete={(c) => console.log('Complete:', c)}
          />
          {code.length > 0 && (
            <Text style={demoStyles.valueText}>
              输入: {code}
            </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',
  },
  section: {
    backgroundColor: '#fff',
    borderRadius: 12,
    padding: 16,
    marginBottom: 20,
  },
  sectionTitle: {
    fontSize: 16,
    fontWeight: '600',
    color: '#333',
    marginBottom: 12,
  },
  valueText: {
    fontSize: 14,
    color: '#999',
    marginTop: 8,
    textAlign: 'center',
  },
});

export default NumberInputDemo;

四、最佳实践

功能 实现方式 效果
金额格式化 千分位分隔符 清晰的金额显示
验证码输入 自动聚焦下一格 流畅的输入体验
数字选择器 带动画的增减按钮 直观的交互
小数处理 可配置小数位数 灵活的精度控制
范围限制 min/max值验证 防止无效输入

五、总结

本文详细介绍了在HarmonyOS平台上实现数字键盘输入的完整方案,涵盖:

  1. 多种输入模式:整数、小数、金额、百分比
  2. 格式化显示:千分位、货币符号、百分比符号
  3. 专用组件:验证码输入、数字选择器
  4. 用户体验:自动聚焦、动画效果、范围限制

完整项目代码AtomGitDemos

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