【鸿蒙原生应用开发--ArkUI--012】Currency-converter 汇率转换应用开发教程

Currency-converter 汇率转换应用开发教程

项目介绍

项目背景

汇率转换是一个实用的金融工具应用,帮助用户在不同货币之间进行汇率换算。随着全球化的发展,跨境交易和出国旅游越来越频繁,一个方便的汇率转换工具变得非常必要。无论是出国旅游时计算外币价格,还是跨境购物时比较不同货币的商品价格,汇率转换应用都能提供极大的便利。

在现代金融环境中,汇率波动频繁,及时了解汇率变化对于投资者和旅行者都至关重要。本应用虽然使用模拟汇率数据,但提供了完整的汇率转换功能框架,开发者可以轻松接入真实的汇率API获取实时数据。

应用场景

  • 出国旅游:快速计算外币价格,帮助旅行者了解当地物价水平,合理规划旅行预算。

  • 跨境购物:比较不同货币的商品价格,在海淘或跨境电商购物时做出更明智的购买决策。

  • 外汇交易:实时查看汇率变动,为外汇投资者提供参考信息。

  • 财务管理:多币种资产管理,帮助有海外资产或收入的用户管理财务。

  • 商务交流:在国际商务活动中快速进行货币换算,提高工作效率。

功能特性

  1. 多货币支持:支持美元、人民币、欧元、日元、英镑、韩元、港币、新台币等主流货币。

  2. 实时转换:输入金额即时显示转换结果,无需点击按钮即可看到结果。

  3. 货币切换:一键交换源货币和目标货币,方便用户快速切换。

  4. 汇率显示:显示当前汇率信息,帮助用户了解货币之间的兑换比例。

  5. 常用汇率:列表显示常用货币汇率,方便用户快速参考。

最终效果

应用采用清新的绿色主题,象征着金融增长和财富。主界面包含:

  • 顶部标题栏显示应用名称
  • 金额输入区域,支持数字键盘输入
  • 货币选择区域,带交换按钮
  • 转换结果大字显示
  • 常用汇率列表,显示主要货币汇率

技术栈

  • 开发框架:HarmonyOS NEXT (API 20+)
  • 编程语言:ArkTS
  • UI框架:ArkUI 声明式 UI
  • 核心组件:Column, Row, Select, TextInput, List, Button

知识点讲解

1. Record 类型数据映射

Record 是 TypeScript 中的一种工具类型,用于创建键值对映射。在汇率转换应用中,我们使用 Record 来存储货币汇率、符号和名称等信息。

typescript 复制代码
// 定义汇率数据映射
// 键是货币代码,值是相对于美元的汇率
private readonly rates: Record<string, number> = {
  'USD': 1.0,      // 美元作为基准货币
  'CNY': 7.24,     // 1美元 = 7.24人民币
  'EUR': 0.92,     // 1美元 = 0.92欧元
  'JPY': 149.50,   // 1美元 = 149.50日元
  'GBP': 0.79,     // 1美元 = 0.79英镑
  'KRW': 1320.0,   // 1美元 = 1320韩元
  'HKD': 7.83,     // 1美元 = 7.83港币
  'TWD': 31.5      // 1美元 = 31.5新台币
}

// 定义货币符号映射
// 键是货币代码,值是货币符号
private readonly symbols: Record<string, string> = {
  'USD': '$',      // 美元符号
  'CNY': '¥',      // 人民币符号
  'EUR': '€',      // 欧元符号
  'JPY': '¥',      // 日元符号
  'GBP': '£',      // 英镑符号
  'KRW': '₩',      // 韩元符号
  'HKD': 'HK$',    // 港币符号
  'TWD': 'NT$'     // 新台币符号
}

// 定义货币名称映射
// 键是货币代码,值是货币中文名称
private readonly names: Record<string, string> = {
  'USD': '美元',
  'CNY': '人民币',
  'EUR': '欧元',
  'JPY': '日元',
  'GBP': '英镑',
  'KRW': '韩元',
  'HKD': '港币',
  'TWD': '新台币'
}

// 使用 Record 的示例
const rate = this.rates['CNY']      // 获取人民币汇率:7.24
const symbol = this.symbols['USD']  // 获取美元符号:$
const name = this.names['EUR']      // 获取欧元名称:欧元

Record 的优势:

  • 类型安全:键和值都有明确的类型定义,编译时会进行类型检查
  • 代码提示:IDE 可以提供属性和方法的自动补全,提高开发效率
  • 易于维护:集中管理数据映射,修改时只需更改一处
  • 访问便捷:通过键名直接访问值,代码简洁易读

2. Select 选择器组件

Select 组件用于创建下拉选择框,用户可以从预定义的选项中选择。在汇率转换应用中,我们使用 Select 来让用户选择源货币和目标货币。

typescript 复制代码
// 基本用法
Select([
  { value: '选项1' },
  { value: '选项2' },
  { value: '选项3' }
])
.value('选项1')  // 设置当前选中值
.width(120)
.height(44)
.fontSize(16)
.onSelect((index: number) => {
  // 选择事件处理
  console.log(`选择了第 ${index} 项`)
})

// 动态生成选项
// 使用 Object.keys 获取所有货币代码,然后映射为 Select 选项格式
Select(Object.keys(this.rates).map(currency => ({ value: currency })))
  .value(this.fromCurrency)  // 绑定当前选中的源货币
  .width(120)
  .height(44)
  .fontSize(18)
  .fontWeight(FontWeight.Bold)
  .onSelect((index: number) => {
    // 获取所有货币代码数组
    const currencies = Object.keys(this.rates)
    // 根据索引获取选中的货币代码
    this.fromCurrency = currencies[index]
    // 重新执行转换
    this.convert()
  })

Select 常用属性:

  • .value():设置当前选中值
  • .selected():设置选中项的索引
  • .width() / .height():设置宽高
  • .fontSize():设置字体大小
  • .fontColor():设置字体颜色
  • .backgroundColor():设置背景颜色
  • .borderRadius():设置圆角
  • .onSelect():设置选择事件处理函数

3. 数学计算与精度处理

汇率转换涉及浮点数计算,需要注意精度问题。JavaScript 的浮点数计算可能会产生精度误差,因此需要进行适当的处理。

typescript 复制代码
private convert() {
  // 解析输入金额
  const amountNum = parseFloat(this.amount)
  
  // 验证输入是否有效
  if (isNaN(amountNum)) {
    this.result = '请输入有效金额'
    return
  }
  
  // 验证金额是否为正数
  if (amountNum <= 0) {
    this.result = '请输入正数金额'
    return
  }
  
  // 获取汇率
  const fromRate = this.rates[this.fromCurrency]
  const toRate = this.rates[this.toCurrency]
  
  if (fromRate && toRate) {
    // 先转换为美元(基准货币),再转换为目标货币
    // 计算公式:目标金额 = 源金额 / 源汇率 * 目标汇率
    const usdAmount = amountNum / fromRate
    const convertedAmount = usdAmount * toRate
    
    // 使用 toFixed 保留两位小数
    this.result = `${this.symbols[this.toCurrency]}${convertedAmount.toFixed(2)}`
  }
}

精度处理方法:

  • toFixed(n):保留 n 位小数,返回字符串
  • Math.round():四舍五入取整
  • parseFloat():解析字符串为浮点数
  • Math.floor():向下取整
  • Math.ceil():向上取整

4. 数组操作与映射

使用数组方法生成选项列表和处理数据。

typescript 复制代码
// 获取所有货币代码
const currencies = Object.keys(this.rates)  
// 结果:['USD', 'CNY', 'EUR', 'JPY', 'GBP', 'KRW', 'HKD', 'TWD']

// 映射为 Select 选项格式
const options = currencies.map(currency => ({ value: currency }))
// 结果:[{ value: 'USD' }, { value: 'CNY' }, ...]

// 在 Select 中使用
Select(Object.keys(this.rates).map(currency => ({ value: currency })))

// 使用 filter 过滤特定货币
const asianCurrencies = currencies.filter(currency => 
  ['CNY', 'JPY', 'KRW', 'HKD', 'TWD'].includes(currency)
)

// 使用 find 查找特定货币
const usdRate = Object.entries(this.rates).find(([key]) => key === 'USD')

常用数组方法:

  • map():映射数组元素,返回新数组
  • filter():过滤数组元素,返回符合条件的元素
  • find():查找第一个符合条件的元素
  • indexOf():获取元素索引
  • includes():检查数组是否包含某个元素
  • reduce():累加计算

5. 条件渲染

根据条件显示不同的内容。在汇率转换应用中,我们根据转换结果显示不同的提示信息。

typescript 复制代码
// 使用三元运算符
Text(this.result !== '' ? this.result : '转换结果将显示在这里')
  .fontSize(18)
  .fontColor(this.result !== '' ? '#059669' : '#64748b')

// 使用 if/else
if (this.result !== '') {
  Text(this.result)
    .fontSize(36)
    .fontWeight(FontWeight.Bold)
    .fontColor('#059669')
} else {
  Text('请输入金额并点击转换')
    .fontSize(16)
    .fontColor('#64748b')
}

// 使用 && 短路运算
{this.showError && Text('输入格式错误').fontColor('#ef4444')}

6. 事件处理

处理用户交互事件,包括输入变化、点击和选择。

typescript 复制代码
// 输入变化事件
// 当用户在输入框中输入内容时触发
TextInput({ placeholder: '请输入金额', text: this.amount })
  .onChange((value: string) => {
    this.amount = value
    this.convert()  // 实时转换
  })

// 点击事件
// 当用户点击按钮时触发
Button('转换')
  .onClick(() => {
    this.convert()
  })

// 选择事件
// 当用户从下拉列表中选择选项时触发
Select(...)
  .onSelect((index: number) => {
    this.fromCurrency = Object.keys(this.rates)[index]
    this.convert()
  })

7. 样式设置

使用链式调用设置组件的各种样式属性。

typescript 复制代码
Text('汇率转换')
  .fontSize(24)                    // 字体大小
  .fontWeight(FontWeight.Bold)     // 字体粗细
  .fontColor('#1e293b')            // 字体颜色
  .margin({ top: 16 })             // 外边距
  .padding({ left: 20 })           // 内边距
  .width('100%')                   // 宽度
  .textAlign(TextAlign.Center)     // 文本对齐方式

Button('转换')
  .width('90%')                    // 宽度
  .height(50)                      // 高度
  .fontSize(18)                    // 字体大小
  .fontColor('#ffffff')            // 字体颜色
  .backgroundColor('#059669')      // 背景颜色
  .borderRadius(12)                // 圆角大小

Column() {
  // 内容
}
.width('100%')                     // 宽度
.padding(20)                       // 内边距
.backgroundColor('#ffffff')        // 背景颜色
.borderRadius(16)                  // 圆角大小
.margin({ left: 20, right: 20 })   // 外边距

8. 布局组件

使用 Column 和 Row 创建垂直和水平布局。

typescript 复制代码
// 垂直布局
Column() {
  Text('第一行')
  Text('第二行')
  Text('第三行')
}
.width('100%')
.padding(20)

// 水平布局
Row() {
  Text('左侧')
  Blank()  // 占据剩余空间
  Text('右侧')
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)

// 嵌套布局
Column() {
  Row() {
    // 水平内容
  }
  .width('100%')
  
  Column() {
    // 垂直内容
  }
  .width('100%')
}
.width('100%')

9. 组件生命周期

组件生命周期方法用于在特定时机执行操作。

typescript 复制代码
@Component
struct CurrencyConverter {
  // 组件即将出现时调用
  // 适合进行初始化操作,如加载数据、设置初始状态等
  aboutToAppear() {
    // 初始化数据
    this.convert()
  }
  
  // 组件即将消失时调用
  // 适合进行清理操作,如取消定时器、保存数据等
  aboutToDisappear() {
    // 清理资源
  }
  
  build() {
    // 构建 UI
    // 此方法会在状态变化时自动重新调用
  }
}

10. 字符串模板

使用模板字符串(反引号)格式化输出,可以在字符串中嵌入变量和表达式。

typescript 复制代码
// 基本模板
const message = `当前汇率: 1 ${this.fromCurrency} = ${rate} ${this.toCurrency}`

// 格式化数字
const amount = `¥${convertedAmount.toFixed(2)}`

// 组合字符串
const displayText = `${this.symbols[this.toCurrency]}${amount}`

// 多行模板
const description = `
  源货币: ${this.fromCurrency}
  目标货币: ${this.toCurrency}
  汇率: ${rate}
`

完整代码解析

页面结构设计

复制代码
┌─────────────────────────────────┐
│         顶部标题栏              │
│         [汇率转换]              │
├─────────────────────────────────┤
│         输入金额                │
│  ┌───────────────────────────┐  │
│  │          1.00             │  │
│  └───────────────────────────┘  │
├─────────────────────────────────┤
│         货币选择                │
│  从          ⇄         到      │
│ [USD ▼]              [CNY ▼]   │
│  美元                人民币      │
├─────────────────────────────────┤
│         转换结果                │
│  ┌───────────────────────────┐  │
│  │        ¥7.24              │  │
│  │                           │  │
│  │  1 USD = 7.24 CNY         │  │
│  └───────────────────────────┘  │
├─────────────────────────────────┤
│         常用汇率                │
│  USD (美元)              $1.00  │
│  EUR (欧元)              €0.92  │
│  JPY (日元)            ¥149.50  │
│  GBP (英镑)              £0.79  │
└─────────────────────────────────┘

各区域详解

1. 顶部标题栏
typescript 复制代码
Row() {
  Text('汇率转换')
    .fontSize(24)
    .fontWeight(FontWeight.Bold)
    .fontColor('#1e293b')
}
.width('100%')
.padding({ left: 20, right: 20, top: 16, bottom: 16 })
2. 金额输入区域
typescript 复制代码
Column() {
  Text('输入金额')
    .fontSize(14)
    .fontColor('#64748b')
    .width('100%')
    .margin({ bottom: 8 })
  
  TextInput({ placeholder: '请输入金额', text: this.amount })
    .width('100%')
    .height(50)
    .fontSize(24)
    .fontWeight(FontWeight.Bold)
    .textAlign(TextAlign.Center)
    .backgroundColor('#ffffff')
    .borderRadius(12)
    .borderWidth(1)
    .borderColor('#e2e8f0')
    .onChange((value: string) => {
      this.amount = value
      this.convert()  // 输入变化时实时转换
    })
}
.width('100%')
.padding({ left: 20, right: 20, bottom: 20 })
3. 货币选择区域
typescript 复制代码
Row() {
  // 源货币选择
  Column() {
    Text('从')
      .fontSize(12)
      .fontColor('#64748b')
      .margin({ bottom: 4 })
    
    Select(Object.keys(this.rates).map(currency => ({ value: currency })))
      .value(this.fromCurrency)
      .width(120)
      .height(44)
      .fontSize(18)
      .fontWeight(FontWeight.Bold)
      .onSelect((index: number) => {
        this.fromCurrency = Object.keys(this.rates)[index]
        this.convert()
      })
    
    Text(this.names[this.fromCurrency])
      .fontSize(12)
      .fontColor('#64748b')
      .margin({ top: 4 })
  }
  .width('40%')
  .alignItems(HorizontalAlign.Center)
  
  // 交换按钮
  Button() {
    Text('⇄')
      .fontSize(28)
      .fontColor('#059669')
  }
  .width(60)
  .height(60)
  .backgroundColor('#ffffff')
  .borderRadius(30)
  .borderWidth(2)
  .borderColor('#059669')
  .onClick(() => {
    this.swapCurrencies()
  })
  
  // 目标货币选择
  Column() {
    Text('到')
      .fontSize(12)
      .fontColor('#64748b')
      .margin({ bottom: 4 })
    
    Select(Object.keys(this.rates).map(currency => ({ value: currency })))
      .value(this.toCurrency)
      .width(120)
      .height(44)
      .fontSize(18)
      .fontWeight(FontWeight.Bold)
      .onSelect((index: number) => {
        this.toCurrency = Object.keys(this.rates)[index]
        this.convert()
      })
    
    Text(this.names[this.toCurrency])
      .fontSize(12)
      .fontColor('#64748b')
      .margin({ top: 4 })
  }
  .width('40%')
  .alignItems(HorizontalAlign.Center)
}
.width('100%')
.padding({ left: 20, right: 20, bottom: 30 })
4. 转换结果区域
typescript 复制代码
Column() {
  Text('转换结果')
    .fontSize(14)
    .fontColor('#64748b')
    .margin({ bottom: 12 })
  
  Text(this.result)
    .fontSize(36)
    .fontWeight(FontWeight.Bold)
    .fontColor('#059669')
    .margin({ bottom: 8 })
  
  Text(`1 ${this.fromCurrency} = ${(this.rates[this.toCurrency] / this.rates[this.fromCurrency]).toFixed(4)} ${this.toCurrency}`)
    .fontSize(14)
    .fontColor('#64748b')
}
.width('100%')
.padding(24)
.backgroundColor('#ffffff')
.borderRadius(16)
.margin({ left: 20, right: 20, bottom: 20 })
5. 常用汇率列表
typescript 复制代码
Column() {
  Text('常用汇率')
    .fontSize(16)
    .fontWeight(FontWeight.Bold)
    .fontColor('#1e293b')
    .width('100%')
    .margin({ bottom: 12 })
  
  ForEach(['USD', 'EUR', 'JPY', 'GBP'], (currency: string) => {
    Row() {
      Text(`${currency} (${this.names[currency]})`)
        .fontSize(14)
        .fontColor('#1e293b')
      
      Blank()
      
      Text(`${this.symbols[currency]}${this.rates[currency].toFixed(2)}`)
        .fontSize(14)
        .fontWeight(FontWeight.Medium)
        .fontColor('#059669')
    }
    .width('100%')
    .padding({ top: 8, bottom: 8 })
    .borderWidth({ bottom: 1 })
    .borderColor('#e2e8f0')
  })
}
.width('100%')
.padding(20)
.backgroundColor('#ffffff')
.borderRadius(16)
.margin({ left: 20, right: 20 })

核心方法实现

1. 执行转换
typescript 复制代码
private convert() {
  // 解析输入金额
  const amountNum = parseFloat(this.amount)
  
  // 验证输入是否为有效数字
  if (isNaN(amountNum)) {
    this.result = '请输入有效金额'
    return
  }
  
  // 验证金额是否为正数
  if (amountNum <= 0) {
    this.result = '请输入正数金额'
    return
  }
  
  // 获取源货币和目标货币的汇率
  const fromRate = this.rates[this.fromCurrency]
  const toRate = this.rates[this.toCurrency]
  
  // 确保汇率存在
  if (fromRate && toRate) {
    // 先转换为美元(基准货币),再转换为目标货币
    const usdAmount = amountNum / fromRate
    const convertedAmount = usdAmount * toRate
    
    // 格式化结果,保留两位小数
    this.result = `${this.symbols[this.toCurrency]}${convertedAmount.toFixed(2)}`
  }
}
2. 交换货币
typescript 复制代码
private swapCurrencies() {
  // 交换源货币和目标货币
  const temp = this.fromCurrency
  this.fromCurrency = this.toCurrency
  this.toCurrency = temp
  
  // 重新执行转换
  this.convert()
}
3. 组件初始化
typescript 复制代码
aboutToAppear() {
  // 组件出现时执行初始转换
  this.convert()
}

常见问题与解决方案

问题1:输入非数字字符时程序出错

现象:在输入框中输入字母或特殊符号时,转换结果显示错误或程序崩溃。

原因parseFloat() 无法解析非数字字符串,返回 NaN,后续计算会产生错误。

解决方案

typescript 复制代码
private convert() {
  const amountNum = parseFloat(this.amount)
  
  // 检查是否为有效数字
  if (isNaN(amountNum)) {
    this.result = '请输入有效金额'
    return
  }
  
  // 检查是否为正数
  if (amountNum <= 0) {
    this.result = '请输入正数金额'
    return
  }
  
  // 继续转换...
}

问题2:汇率精度问题

现象 :转换结果出现很长的小数位,如 7.240000000000001

原因:JavaScript 浮点数计算存在精度误差,这是 IEEE 754 浮点数标准的固有问题。

解决方案

typescript 复制代码
// 使用 toFixed 限制小数位数
this.result = `${this.symbols[this.toCurrency]}${convertedAmount.toFixed(2)}`

// 或者使用 Math.round 进行四舍五入
const rounded = Math.round(convertedAmount * 100) / 100
this.result = `${this.symbols[this.toCurrency]}${rounded}`

问题3:交换货币后结果未更新

现象:点击交换按钮后,转换结果没有变化。

原因:交换货币后没有重新执行转换函数。

解决方案

typescript 复制代码
private swapCurrencies() {
  const temp = this.fromCurrency
  this.fromCurrency = this.toCurrency
  this.toCurrency = temp
  
  // 交换后必须重新转换
  this.convert()
}

问题4:选择器选项显示不全

现象:下拉选择器中的选项文字被截断,无法完整显示。

原因:选择器宽度不够,无法容纳较长的选项文字。

解决方案

typescript 复制代码
Select(...)
  .width(120)  // 增加宽度以容纳完整文字
  .height(44)

扩展学习

可添加功能

  1. 实时汇率

    • 接入汇率 API 获取实时数据
    • 定时刷新汇率信息
    • 显示汇率更新时间
  2. 历史汇率

    • 显示汇率走势图
    • 查看历史数据
    • 汇率变化分析
  3. 收藏货币

    • 收藏常用货币对
    • 快速切换收藏的货币
    • 自定义货币列表
  4. 离线模式

    • 缓存汇率数据
    • 离线时使用缓存数据
    • 显示数据更新时间
  5. 多币种同时转换

    • 一次显示多种货币结果
    • 方便比较不同货币
    • 批量转换功能

优化建议

  1. 性能优化

    • 使用防抖处理输入,减少转换次数
    • 缓存计算结果,避免重复计算
    • 优化列表渲染性能
  2. 用户体验

    • 添加数字键盘,方便输入
    • 支持复制结果功能
    • 添加震动反馈
  3. 界面优化

    • 添加货币图标或国旗
    • 优化动画效果
    • 支持深色模式

总结

通过本教程,您学会了:

  1. Record 类型:如何使用 Record 创建键值对映射,管理货币汇率、符号和名称等数据。

  2. Select 组件:如何创建下拉选择框,实现货币选择功能。

  3. 数学计算:如何进行浮点数计算和精度处理,避免精度误差。

  4. 数组映射:如何使用 map 方法生成选项列表。

  5. 实时转换:如何在输入变化时实时更新转换结果。

  6. 数据格式化:如何使用 toFixed 方法格式化数字显示。

这些知识点不仅适用于汇率转换应用,还可以应用于各种需要数据映射和计算的场景,如单位转换、价格计算等。

相关推荐
李二。2 小时前
鸿蒙 HarmonyOS 校园风登录页面开发实战 —— 基于 ArkTS 的 Stage 模型完整教程
华为·harmonyos
大雷神2 小时前
第30篇|图片文件落盘:沙箱路径、Uri 与后续读取
harmonyos
枫叶丹42 小时前
【HarmonyOS 6.0】Live View Kit 实况窗开发详解:进度胶囊支持副文本功能探究
开发语言·华为·harmonyos
想你依然心痛2 小时前
HarmonyOS 6(API 23)智能体驱动的沉浸式AR城市地下管网运维中心
运维·ar·harmonyos·智能体
非凡大爹3 小时前
实验十 华为路由器和交换机实现RIP 动态路由协议配置实验指导书
运维·网络·计算机网络·华为
Goway_Hui3 小时前
【鸿蒙原生应用开发--ArkUI--014】Expense-tracker 记账应用开发教程
华为·harmonyos
不羁的木木3 小时前
《HarmonyOS技术精讲》五:实战项目 ── 智能支架助手
华为·harmonyos
枫叶丹44 小时前
【HarmonyOS 6.0】Map Kit瓦片图层深度解析:本地加载方式与瓦片数据缓存能力
开发语言·缓存·华为·harmonyos
大雷神4 小时前
第29篇|单拍按钮背后:从点击到 PhotoOutput 回调
harmonyos