在这里插入图片描述

一、核心原理:单位转换器的设计与实现

1.1 单位转换器的设计理念

单位转换器是一个实用的工具,主要用于:

  • 长度转换:米、千米、厘米、毫米、英寸、英尺等
  • 重量转换:千克、克、毫克、磅、盎司等
  • 温度转换:摄氏度、华氏度、开尔文等
  • 体积转换:升、毫升、加仑等

1.2 单位转换器的核心要素

一个完整的单位转换器需要考虑:

  1. 输入区域:用户输入数值
  2. 单位选择:选择源单位和目标单位
  3. 转换逻辑:实现各种单位之间的转换
  4. 结果显示:显示转换后的结果
  5. 历史记录:保存最近的转换记录
  6. 常用单位:提供常用单位的快捷选择
  7. 实时转换:输入时实时显示转换结果

1.3 实现原理

单位转换器的核心实现原理:

  • 使用状态管理存储输入值、源单位、目标单位
  • 使用转换公式实现单位之间的转换
  • 使用 ScrollView 确保页面可滚动
  • 使用 TouchableOpacity 实现按钮交互
  • 使用 TextInput 实现数值输入
  • 使用 StyleSheet 实现样式定制

二、基础单位转换器实现

2.1 组件结构

单位转换器组件包含以下部分:

  1. 类型选择:选择转换类型(长度、重量、温度)
  2. 输入区域:输入要转换的数值
  3. 单位选择:选择源单位和目标单位
  4. 结果显示:显示转换后的结果
  5. 历史记录:显示最近的转换记录

2.2 完整代码实现

import React, { useState, useCallback, memo } from 'react';
import {
  View,
  Text,
  StyleSheet,
  TouchableOpacity,
  SafeAreaView,
  ScrollView,
  TextInput,
} from 'react-native';

// 转换类型
type ConversionType = 'length' | 'weight' | 'temperature';

// 单位定义
const units = {
  length: [
    { label: '米', value: 'm', factor: 1 },
    { label: '千米', value: 'km', factor: 1000 },
    { label: '厘米', value: 'cm', factor: 0.01 },
    { label: '毫米', value: 'mm', factor: 0.001 },
    { label: '英寸', value: 'in', factor: 0.0254 },
    { label: '英尺', value: 'ft', factor: 0.3048 },
  ],
  weight: [
    { label: '千克', value: 'kg', factor: 1 },
    { label: '克', value: 'g', factor: 0.001 },
    { label: '毫克', value: 'mg', factor: 0.000001 },
    { label: '磅', value: 'lb', factor: 0.453592 },
    { label: '盎司', value: 'oz', factor: 0.0283495 },
  ],
  temperature: [
    { label: '摄氏度', value: 'c' },
    { label: '华氏度', value: 'f' },
    { label: '开尔文', value: 'k' },
  ],
};

// 单位转换器组件
const UnitConverter = memo(() => {
  const [conversionType, setConversionType] = useState<ConversionType>('length');
  const [inputValue, setInputValue] = useState('');
  const [fromUnit, setFromUnit] = useState(units.length[0].value);
  const [toUnit, setToUnit] = useState(units.length[1].value);
  const [result, setResult] = useState('');
  const [history, setHistory] = useState<Array<{ type: string; from: string; to: string; value: string; result: string }>>([]);

  // 执行转换
  const convert = useCallback(() => {
    const value = parseFloat(inputValue);
    if (isNaN(value)) {
      setResult('请输入有效的数值');
      return;
    }

    let convertedValue: number;

    if (conversionType === 'temperature') {
      // 温度转换需要特殊处理
      convertedValue = convertTemperature(value, fromUnit, toUnit);
    } else {
      // 长度和重量转换使用因子
      const fromUnits = units[conversionType];
      const fromFactor = fromUnits.find(u => u.value === fromUnit)?.factor || 1;
      const toFactor = fromUnits.find(u => u.value === toUnit)?.factor || 1;
      convertedValue = (value * fromFactor) / toFactor;
    }

    const resultStr = convertedValue.toFixed(4);
    setResult(resultStr);

    // 添加到历史记录
    const newHistory = {
      type: conversionType === 'length' ? '长度' : conversionType === 'weight' ? '重量' : '温度',
      from: units[conversionType].find(u => u.value === fromUnit)?.label || fromUnit,
      to: units[conversionType].find(u => u.value === toUnit)?.label || toUnit,
      value: inputValue,
      result: resultStr,
    };
    setHistory(prev => [newHistory, ...prev].slice(0, 10));
  }, [inputValue, fromUnit, toUnit, conversionType]);

  // 温度转换
  const convertTemperature = useCallback((value: number, from: string, to: string): number => {
    // 先转换为摄氏度
    let celsius: number;
    if (from === 'f') {
      celsius = (value - 32) * 5 / 9;
    } else if (from === 'k') {
      celsius = value - 273.15;
    } else {
      celsius = value;
    }

    // 从摄氏度转换为目标单位
    if (to === 'f') {
      return celsius * 9 / 5 + 32;
    } else if (to === 'k') {
      return celsius + 273.15;
    } else {
      return celsius;
    }
  }, []);

  // 实时转换
  React.useEffect(() => {
    if (inputValue) {
      const value = parseFloat(inputValue);
      if (!isNaN(value)) {
        let convertedValue: number;

        if (conversionType === 'temperature') {
          convertedValue = convertTemperature(value, fromUnit, toUnit);
        } else {
          const fromUnits = units[conversionType];
          const fromFactor = fromUnits.find(u => u.value === fromUnit)?.factor || 1;
          const toFactor = fromUnits.find(u => u.value === toUnit)?.factor || 1;
          convertedValue = (value * fromFactor) / toFactor;
        }

        setResult(convertedValue.toFixed(4));
      }
    } else {
      setResult('');
    }
  }, [inputValue, fromUnit, toUnit, conversionType, convertTemperature]);

  // 切换转换类型
  const handleTypeChange = useCallback((type: ConversionType) => {
    setConversionType(type);
    setInputValue('');
    setResult('');
    setFromUnit(units[type][0].value);
    setToUnit(units[type][1].value);
  }, []);

  // 清除输入
  const handleClear = useCallback(() => {
    setInputValue('');
    setResult('');
  }, []);

  return (
    <SafeAreaView style={styles.container}>
      <ScrollView style={styles.scrollView}>
        {/* 标题区域 */}
        <View style={styles.header}>
          <Text style={styles.title}>React Native for Harmony</Text>
          <Text style={styles.subtitle}>单位转换器</Text>
        </View>

        {/* 转换器主体 */}
        <View style={styles.converterContainer}>
          {/* 转换类型选择 */}
          <View style={styles.typeSelector}>
            <TouchableOpacity
              style={[styles.typeButton, conversionType === 'length' && styles.activeTypeButton]}
              onPress={() => handleTypeChange('length')}
            >
              <Text style={[styles.typeButtonText, conversionType === 'length' && styles.activeTypeButtonText]}>长度</Text>
            </TouchableOpacity>
            <TouchableOpacity
              style={[styles.typeButton, conversionType === 'weight' && styles.activeTypeButton]}
              onPress={() => handleTypeChange('weight')}
            >
              <Text style={[styles.typeButtonText, conversionType === 'weight' && styles.activeTypeButtonText]}>重量</Text>
            </TouchableOpacity>
            <TouchableOpacity
              style={[styles.typeButton, conversionType === 'temperature' && styles.activeTypeButton]}
              onPress={() => handleTypeChange('temperature')}
            >
              <Text style={[styles.typeButtonText, conversionType === 'temperature' && styles.activeTypeButtonText]}>温度</Text>
            </TouchableOpacity>
          </View>

          {/* 输入区域 */}
          <View style={styles.inputSection}>
            <Text style={styles.label}>输入数值</Text>
            <TextInput
              style={styles.input}
              value={inputValue}
              onChangeText={setInputValue}
              placeholder="请输入数值"
              keyboardType="decimal-pad"
              placeholderTextColor="#909399"
            />
          </View>

          {/* 源单位选择 */}
          <View style={styles.unitSection}>
            <Text style={styles.label}></Text>
            <View style={styles.unitButtons}>
              {units[conversionType].map(unit => (
                <TouchableOpacity
                  key={unit.value}
                  style={[styles.unitButton, fromUnit === unit.value && styles.activeUnitButton]}
                  onPress={() => setFromUnit(unit.value)}
                >
                  <Text style={[styles.unitButtonText, fromUnit === unit.value && styles.activeUnitButtonText]}>
                    {unit.label}
                  </Text>
                </TouchableOpacity>
              ))}
            </View>
          </View>

          {/* 目标单位选择 */}
          <View style={styles.unitSection}>
            <Text style={styles.label}></Text>
            <View style={styles.unitButtons}>
              {units[conversionType].map(unit => (
                <TouchableOpacity
                  key={unit.value}
                  style={[styles.unitButton, toUnit === unit.value && styles.activeUnitButton]}
                  onPress={() => setToUnit(unit.value)}
                >
                  <Text style={[styles.unitButtonText, toUnit === unit.value && styles.activeUnitButtonText]}>
                    {unit.label}
                  </Text>
                </TouchableOpacity>
              ))}
            </View>
          </View>

          {/* 结果显示 */}
          <View style={styles.resultSection}>
            <Text style={styles.resultLabel}>转换结果</Text>
            <View style={styles.resultBox}>
              <Text style={styles.resultText}>{result || '--'}</Text>
            </View>
          </View>

          {/* 操作按钮 */}
          <View style={styles.actionButtons}>
            <TouchableOpacity
              style={styles.actionButton}
              onPress={handleClear}
            >
              <Text style={styles.actionButtonText}>清除</Text>
            </TouchableOpacity>
            <TouchableOpacity
              style={[styles.actionButton, styles.convertButton]}
              onPress={convert}
            >
              <Text style={styles.actionButtonText}>转换</Text>
            </TouchableOpacity>
          </View>
        </View>

        {/* 历史记录 */}
        {history.length > 0 && (
          <View style={styles.historyContainer}>
            <Text style={styles.historyTitle}>历史记录</Text>
            {history.map((item, index) => (
              <View key={index} style={styles.historyItem}>
                <Text style={styles.historyType}>{item.type}</Text>
                <Text style={styles.historyText}>
                  {item.value} {item.from} = {item.result} {item.to}
                </Text>
              </View>
            ))}
          </View>
        )}

        {/* 说明区域 */}
        <View style={styles.infoCard}>
          <Text style={styles.infoTitle}>💡 功能说明</Text>
          <Text style={styles.infoText}>• 多种类型:支持长度、重量、温度转换</Text>
          <Text style={styles.infoText}>• 实时转换:输入时实时显示转换结果</Text>
          <Text style={styles.infoText}>• 历史记录:保存最近的10条转换记录</Text>
          <Text style={styles.infoText}>• 精确计算:保留4位小数</Text>
          <Text style={styles.infoText}>• 温度转换:支持摄氏度、华氏度、开尔文</Text>
          <Text style={styles.infoText}>• 鸿蒙端完美兼容,转换准确</Text>
        </View>
      </ScrollView>
    </SafeAreaView>
  );
});

UnitConverter.displayName = 'UnitConverter';

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F5F7FA',
  },
  scrollView: {
    flex: 1,
  },

  // ======== 标题区域 ========
  header: {
    padding: 20,
    backgroundColor: '#FFFFFF',
    borderBottomWidth: 1,
    borderBottomColor: '#EBEEF5',
  },
  title: {
    fontSize: 24,
    fontWeight: '700',
    color: '#303133',
    textAlign: 'center',
    marginBottom: 8,
  },
  subtitle: {
    fontSize: 16,
    fontWeight: '500',
    color: '#909399',
    textAlign: 'center',
  },

  // ======== 转换器容器 ========
  converterContainer: {
    backgroundColor: '#FFFFFF',
    margin: 16,
    borderRadius: 16,
    padding: 20,
    shadowColor: '#000000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.08,
    shadowRadius: 8,
    elevation: 4,
  },

  // ======== 转换类型选择 ========
  typeSelector: {
    flexDirection: 'row',
    marginBottom: 24,
  },
  typeButton: {
    flex: 1,
    paddingVertical: 12,
    marginHorizontal: 4,
    borderRadius: 8,
    backgroundColor: '#F5F7FA',
    alignItems: 'center',
  },
  activeTypeButton: {
    backgroundColor: '#409EFF',
  },
  typeButtonText: {
    fontSize: 14,
    fontWeight: '600',
    color: '#606266',
  },
  activeTypeButtonText: {
    color: '#FFFFFF',
  },

  // ======== 输入区域 ========
  inputSection: {
    marginBottom: 20,
  },
  label: {
    fontSize: 14,
    fontWeight: '600',
    color: '#303133',
    marginBottom: 8,
  },
  input: {
    backgroundColor: '#F5F7FA',
    borderRadius: 8,
    padding: 12,
    fontSize: 16,
    color: '#303133',
    borderWidth: 1,
    borderColor: '#EBEEF5',
  },

  // ======== 单位选择 ========
  unitSection: {
    marginBottom: 20,
  },
  unitButtons: {
    flexDirection: 'row',
    flexWrap: 'wrap',
  },
  unitButton: {
    paddingHorizontal: 12,
    paddingVertical: 8,
    borderRadius: 6,
    backgroundColor: '#F5F7FA',
    marginRight: 8,
    marginBottom: 8,
  },
  activeUnitButton: {
    backgroundColor: '#409EFF',
  },
  unitButtonText: {
    fontSize: 14,
    color: '#606266',
  },
  activeUnitButtonText: {
    color: '#FFFFFF',
  },

  // ======== 结果显示 ========
  resultSection: {
    marginBottom: 20,
  },
  resultLabel: {
    fontSize: 14,
    fontWeight: '600',
    color: '#303133',
    marginBottom: 8,
  },
  resultBox: {
    backgroundColor: '#F5F7FA',
    borderRadius: 8,
    padding: 16,
    alignItems: 'center',
  },
  resultText: {
    fontSize: 24,
    fontWeight: '700',
    color: '#409EFF',
  },

  // ======== 操作按钮 ========
  actionButtons: {
    flexDirection: 'row',
  },
  actionButton: {
    flex: 1,
    paddingVertical: 12,
    borderRadius: 8,
    backgroundColor: '#F5F7FA',
    alignItems: 'center',
    marginHorizontal: 4,
  },
  convertButton: {
    backgroundColor: '#409EFF',
  },
  actionButtonText: {
    fontSize: 16,
    fontWeight: '600',
    color: '#303133',
  },

  // ======== 历史记录 ========
  historyContainer: {
    backgroundColor: '#FFFFFF',
    margin: 16,
    borderRadius: 12,
    padding: 16,
    shadowColor: '#000000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.08,
    shadowRadius: 8,
    elevation: 4,
  },
  historyTitle: {
    fontSize: 16,
    fontWeight: '600',
    color: '#303133',
    marginBottom: 12,
  },
  historyItem: {
    paddingVertical: 8,
    borderBottomWidth: 1,
    borderBottomColor: '#EBEEF5',
  },
  historyType: {
    fontSize: 12,
    color: '#909399',
    marginBottom: 4,
  },
  historyText: {
    fontSize: 14,
    color: '#606266',
  },

  // ======== 信息卡片 ========
  infoCard: {
    backgroundColor: '#FFFFFF',
    borderRadius: 12,
    padding: 16,
    margin: 16,
    marginTop: 0,
    shadowColor: '#000000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.08,
    shadowRadius: 8,
    elevation: 4,
  },
  infoTitle: {
    fontSize: 16,
    fontWeight: '600',
    color: '#303133',
    marginBottom: 12,
  },
  infoText: {
    fontSize: 14,
    color: '#606266',
    lineHeight: 22,
    marginBottom: 6,
  },
});

export default UnitConverter;

三、核心实现要点

3.1 单位定义

定义各种单位及其转换因子:

const units = {
  length: [
    { label: '米', value: 'm', factor: 1 },
    { label: '千米', value: 'km', factor: 1000 },
    { label: '厘米', value: 'cm', factor: 0.01 },
    { label: '毫米', value: 'mm', factor: 0.001 },
    { label: '英寸', value: 'in', factor: 0.0254 },
    { label: '英尺', value: 'ft', factor: 0.3048 },
  ],
  weight: [
    { label: '千克', value: 'kg', factor: 1 },
    { label: '克', value: 'g', factor: 0.001 },
    { label: '毫克', value: 'mg', factor: 0.000001 },
    { label: '磅', value: 'lb', factor: 0.453592 },
    { label: '盎司', value: 'oz', factor: 0.0283495 },
  ],
  temperature: [
    { label: '摄氏度', value: 'c' },
    { label: '华氏度', value: 'f' },
    { label: '开尔文', value: 'k' },
  ],
};

单位定义要点:

  • 使用对象数组存储单位信息
  • factor 表示相对于基准单位的转换因子
  • 温度单位不需要 factor,使用特殊转换逻辑

3.2 长度和重量转换

使用转换因子实现长度和重量转换:

const fromUnits = units[conversionType];
const fromFactor = fromUnits.find(u => u.value === fromUnit)?.factor || 1;
const toFactor = fromUnits.find(u => u.value === toUnit)?.factor || 1;
convertedValue = (value * fromFactor) / toFactor;

转换逻辑:

  • 将输入值转换为基准单位(乘以 fromFactor)
  • 将基准单位转换为目标单位(除以 toFactor)
  • 例如:1 千米转换为米:1 * 1000 / 1 = 1000 米

3.3 温度转换

温度转换需要特殊处理:

const convertTemperature = useCallback((value: number, from: string, to: string): number => {
  // 先转换为摄氏度
  let celsius: number;
  if (from === 'f') {
    celsius = (value - 32) * 5 / 9;
  } else if (from === 'k') {
    celsius = value - 273.15;
  } else {
    celsius = value;
  }

  // 从摄氏度转换为目标单位
  if (to === 'f') {
    return celsius * 9 / 5 + 32;
  } else if (to === 'k') {
    return celsius + 273.15;
  } else {
    return celsius;
  }
}, []);

温度转换要点:

  • 先将源单位转换为摄氏度
  • 再从摄氏度转换为目标单位
  • 使用不同的转换公式

3.4 实时转换

输入时实时显示转换结果:

React.useEffect(() => {
  if (inputValue) {
    const value = parseFloat(inputValue);
    if (!isNaN(value)) {
      // 执行转换
      setResult(convertedValue.toFixed(4));
    }
  } else {
    setResult('');
  }
}, [inputValue, fromUnit, toUnit, conversionType, convertTemperature]);

实时转换要点:

  • 使用 useEffect 监听输入值和单位变化
  • 输入时自动执行转换
  • 保留 4 位小数

四、性能优化

4.1 使用 useCallback 优化

使用 useCallback 缓存回调函数:

const convert = useCallback(() => {
  const value = parseFloat(inputValue);
  if (isNaN(value)) {
    setResult('请输入有效的数值');
    return;
  }
  // 转换逻辑
}, [inputValue, fromUnit, toUnit, conversionType]);

const convertTemperature = useCallback((value: number, from: string, to: string): number => {
  // 温度转换逻辑
}, []);

为什么使用 useCallback?

  • 避免每次渲染都创建新函数
  • 减少子组件的重新渲染
  • 提升整体性能

4.2 使用 memo 优化

使用 memo 包装组件:

const UnitConverter = memo(() => {
  // ...
});

为什么使用 memo?

  • 避免不必要的重新渲染
  • 提升整体性能
  • 在复杂应用中效果更明显

4.3 限制历史记录数量

限制历史记录的数量:

setHistory(prev => [newHistory, ...prev].slice(0, 10));

为什么限制数量?

  • 避免历史记录过多占用内存
  • 提升性能
  • 只显示最近的 10 条记录

五、常见问题与解决方案

5.1 转换结果不正确

问题现象: 转换结果与预期不符

可能原因:

  1. 转换因子设置错误
  2. 温度转换公式错误
  3. 单位选择错误

解决方案:

// 检查转换因子
{ label: '千米', value: 'km', factor: 1000 },

// 检查温度转换公式
if (from === 'f') {
  celsius = (value - 32) * 5 / 9;
}

5.2 输入无效数值

问题现象: 输入非数值时显示错误

可能原因:

  1. 没有验证输入值
  2. 没有处理 NaN 情况

解决方案:

const value = parseFloat(inputValue);
if (isNaN(value)) {
  setResult('请输入有效的数值');
  return;
}

5.3 历史记录不更新

问题现象: 转换后历史记录没有更新

可能原因:

  1. 没有正确更新历史记录状态
  2. 历史记录对象结构错误

解决方案:

const newHistory = {
  type: conversionType === 'length' ? '长度' : conversionType === 'weight' ? '重量' : '温度',
  from: units[conversionType].find(u => u.value === fromUnit)?.label || fromUnit,
  to: units[conversionType].find(u => u.value === toUnit)?.label || toUnit,
  value: inputValue,
  result: resultStr,
};
setHistory(prev => [newHistory, ...prev].slice(0, 10));

六、扩展用法

6.1 添加更多单位类型

添加面积、体积、速度等单位类型:

const units = {
  length: [...],
  weight: [...],
  temperature: [...],
  area: [
    { label: '平方米', value: 'm2', factor: 1 },
    { label: '平方千米', value: 'km2', factor: 1000000 },
    { label: '平方英尺', value: 'ft2', factor: 0.092903 },
  ],
  volume: [
    { label: '升', value: 'l', factor: 1 },
    { label: '毫升', value: 'ml', factor: 0.001 },
    { label: '加仑', value: 'gal', factor: 3.78541 },
  ],
};

6.2 添加复制功能

添加复制转换结果的功能:

import { Clipboard } from 'react-native';

const handleCopy = useCallback(() => {
  if (result) {
    Clipboard.setString(result);
    // 显示复制成功提示
  }
}, [result]);

6.3 添加反向转换

添加快速反向转换的功能:

const handleReverse = useCallback(() => {
  if (result) {
    setInputValue(result);
    const temp = fromUnit;
    setFromUnit(toUnit);
    setToUnit(temp);
  }
}, [result, fromUnit, toUnit]);

七、总结

单位转换器是一个实用的工具,通过本篇文章,我们学习了:

  1. 单位定义:定义各种单位及其转换因子
  2. 转换逻辑:实现长度、重量、温度之间的转换
  3. 实时转换:输入时实时显示转换结果
  4. 历史记录:保存最近的转换记录
  5. 性能优化:使用 useCallback、memo 优化性能
  6. 错误处理:处理无效输入和转换错误

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

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐