一、应用概述

货币转换(Currency Converter) 是一款实用的汇率换算工具,支持人民币(CNY)、美元(USD)、欧元(EUR)、日元(JPY)、英镑(GBP)和韩元(KRW)六大主流货币之间的实时换算。用户只需输入金额并选择源货币与目标货币,即可获得精确的换算结果,同时支持一键互换货币对和快速切换源货币。应用基于 HarmonyOS 的 ArkTS 语言和声明式 UI 框架构建,完美展示了 Flex 弹性布局、Record 字典数据结构、双向数据流、以及 UI 状态驱动的货币选择等核心技术的综合运用。

对于鸿蒙开发者而言,货币转换是一个典型的数据驱动应用案例——它让你在约90行代码中,同时掌握字典查找双向绑定弹性换行布局三大关键能力,这些能力广泛适用于汇率、计算器、配置面板等数据展示与操作场景。

1.1 功能特性

  • 六大货币支持:人民币、美元、欧元、日元、英镑、韩元
  • 实时汇率换算:输入金额立即转换,无需点击确认按钮
  • 货币符号显示:每种货币使用其标准符号(¥、$、€、£、₩)
  • 一键互换功能:点击圆形按钮快速交换源货币和目标货币
  • 汇率信息展示:显示当前使用的汇率值
  • 货币快速选择:通过 Flex 布局的可点击标签切换源货币
  • 货币名称展示:每种货币显示其中文名称,提升可读性

1.2 适用场景

  • 出国旅游前的汇率查询
  • 跨境电商购物比价
  • 外汇投资参考
  • 国际贸易成本计算
  • 财务报告货币换算

二、技术架构

2.1 整体架构概览

┌─────────────────────────────────────────┐
│             数据层                        │
│  RATES (Record<string, Record<>>) 汇率表  │
│  CURRENCIES (string[]) 货币列表           │
│  SYMBOLS / NAMES 元数据字典              │
├─────────────────────────────────────────┤
│             状态管理层                     │
│  @State fromCurrency / toCurrency /      │
│         amount / result                  │
├─────────────────────────────────────────┤
│             业务逻辑层                     │
│  convert() 汇率换算 + get rate() 汇率查询  │
├─────────────────────────────────────────┤
│             UI 表示层                     │
│  输入面板 / 互换按钮 / 结果面板 /          │
│  货币选择区                               │
└─────────────────────────────────────────┘

2.2 核心数据结构设计

汇率数据使用嵌套的 Record 类型存储:

const RATES: Record<string, Record<string, number>> = {
  'CNY': { 'USD': 0.14, 'EUR': 0.13, 'JPY': 21.5, 'GBP': 0.11, 'KRW': 190, 'CNY': 1 },
  'USD': { 'CNY': 7.15, 'EUR': 0.92, 'JPY': 153, 'GBP': 0.79, 'KRW': 1360, 'USD': 1 },
  'EUR': { 'CNY': 7.78, 'USD': 1.09, 'JPY': 166, 'GBP': 0.86, 'KRW': 1480, 'EUR': 1 },
  'JPY': { 'CNY': 0.047, 'USD': 0.0065, 'EUR': 0.0060, 'GBP': 0.0052, 'KRW': 8.9, 'JPY': 1 },
};

Record<K, V> 是 ArkTS 中的字典类型,等价于 TypeScript 的 Record<K, V>。这里使用了两层嵌套:

  • 第一层键为源货币代码
  • 第二层键为目标货币代码
  • 值为汇率(直接汇率,即1单位源货币可兑换的目标货币数)

这种设计实现了O(1) 时间复杂度的汇率查找,比数组遍历或 switch-case 语句高效得多。

辅助元数据同样使用 Record 存储:

const SYMBOLS: Record<string, string> = { 'CNY': '¥', 'USD': '$', 'EUR': '€', 'JPY': '¥', 'GBP': '£', 'KRW': '₩' };
const NAMES: Record<string, string> = { 'CNY': '人民币', 'USD': '美元', 'EUR': '欧元', 'JPY': '日元', 'GBP': '英镑', 'KRW': '韩元' };

三、核心代码深度分析

3.1 完整的页面源码

import { router } from '@kit.ArkUI';

const RATES: Record<string, Record<string, number>> = {
  'CNY': { 'USD': 0.14, 'EUR': 0.13, 'JPY': 21.5, 'GBP': 0.11, 'KRW': 190, 'CNY': 1 },
  'USD': { 'CNY': 7.15, 'EUR': 0.92, 'JPY': 153, 'GBP': 0.79, 'KRW': 1360, 'USD': 1 },
  'EUR': { 'CNY': 7.78, 'USD': 1.09, 'JPY': 166, 'GBP': 0.86, 'KRW': 1480, 'EUR': 1 },
  'JPY': { 'CNY': 0.047, 'USD': 0.0065, 'EUR': 0.0060, 'GBP': 0.0052, 'KRW': 8.9, 'JPY': 1 },
};

const CURRENCIES: string[] = ['CNY', 'USD', 'EUR', 'JPY', 'GBP', 'KRW'];
const SYMBOLS: Record<string, string> = { 'CNY': '¥', 'USD': '$', 'EUR': '€', 'JPY': '¥', 'GBP': '£', 'KRW': '₩' };
const NAMES: Record<string, string> = { 'CNY': '人民币', 'USD': '美元', 'EUR': '欧元', 'JPY': '日元', 'GBP': '英镑', 'KRW': '韩元' };

@Entry
@Component
struct index22 {
  @State fromCurrency: string = 'CNY';
  @State toCurrency: string = 'USD';
  @State amount: string = '100';
  @State result: string = '14.00';

  convert(): void {
    const val = parseFloat(this.amount);
    if (isNaN(val) || val < 0) { this.result = '无效'; return; }
    const rate = RATES[this.fromCurrency]?.[this.toCurrency];
    if (rate === undefined) { this.result = '无汇率'; return; }
    this.result = (val * rate).toFixed(2);
  }

  get rate(): string {
    const r = RATES[this.fromCurrency]?.[this.toCurrency];
    return (r !== undefined ? r : 0).toFixed(4);
  }

  build() {
    Column() {
      Row() {
        Text('‹ 返回').fontSize(16).fontColor('#0077FF').onClick(() => router.back())
        Text('💰 货币转换').fontSize(18).fontWeight(FontWeight.Bold)
        Blank()
      }.width('92%').padding({ top: 16, bottom: 8 })

      Column() {
        // 源货币输入面板
        Column() {
          Row() {
            Text(SYMBOLS[this.fromCurrency]).fontSize(24).fontWeight(FontWeight.Bold).width(36)
            TextInput({ text: this.amount })
              .layoutWeight(1).height(48).backgroundColor('#fff').borderRadius(8)
              .type(InputType.Number).padding({ left: 12 }).fontSize(20)
              .onChange((v: string) => { this.amount = v; this.convert() })
          }
          Text(NAMES[this.fromCurrency]).fontSize(13).fontColor('#888')
        }.width('92%').padding(12).backgroundColor('#fff').borderRadius(12)

        // 互换按钮
        Button() {
          Text('⇅ 互换').fontSize(20).fontColor('#fff')
        }.width(56).height(56).borderRadius(28).backgroundColor('#0077FF').margin(8)
          .onClick(() => {
            const t = this.fromCurrency;
            this.fromCurrency = this.toCurrency;
            this.toCurrency = t;
            this.convert();
          })

        // 目标货币结果面板
        Column() {
          Row() {
            Text(SYMBOLS[this.toCurrency]).fontSize(24).fontWeight(FontWeight.Bold).width(36)
            Text(this.result)
              .layoutWeight(1).height(48).fontSize(24).fontWeight(FontWeight.Bold)
              .fontColor('#F44336').padding({ left: 12 })
          }
          Text(NAMES[this.toCurrency]).fontSize(13).fontColor('#888')
        }.width('92%').padding(12).backgroundColor('#fff').borderRadius(12)

        // 汇率展示
        Text('汇率: 1 ' + this.fromCurrency + ' = ' + this.rate + ' ' + this.toCurrency)
          .fontSize(13).fontColor('#888').margin({ top: 12 })

        // 货币快速选择区
        Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Center }) {
          ForEach(CURRENCIES, (c: string) => {
            Text(NAMES[c])
              .fontSize(14).padding(8)
              .backgroundColor(c === this.fromCurrency ? '#0077FF' : '#eee')
              .fontColor(c === this.fromCurrency ? '#fff' : '#333')
              .borderRadius(16).margin(4)
              .onClick(() => { this.fromCurrency = c; this.convert() })
          })
        }.width('92%').margin({ top: 16 })
      }
      .layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 20 })
    }
    .width('100%').height('100%').backgroundColor('#f8f8f8')
  }
}

3.2 汇率查找机制

const rate = RATES[this.fromCurrency]?.[this.toCurrency];
if (rate === undefined) { this.result = '无汇率'; return; }
this.result = (val * rate).toFixed(2);

这里使用了可选链操作符?.),这是 ArkTS/TypeScript 提供的一种安全访问嵌套对象属性的语法:

  • RATES[this.fromCurrency] 获取源货币的汇率字典
  • ?.[this.toCurrency] 如果源货币字典存在,则继续查找目标货币汇率
  • 如果任一环节不存在,rate 的值为 undefined

这种写法避免了多层 if 判断和可能的 Cannot read properties of undefined 运行时错误,是现代 ArkTS 开发中推荐的安全访问模式。

3.3 互换功能的实现

.onClick(() => {
  const t = this.fromCurrency;
  this.fromCurrency = this.toCurrency;
  this.toCurrency = t;
  this.convert();
})

互换操作使用了一个临时变量 t 来暂存 fromCurrency 的值,然后完成两个变量的交换。这种"三角交换"是编程中最基础的算法模式之一。交换后立即调用 convert() 重新计算结果,确保 UI 显示与最新状态同步。

3.4 Flex 弹性换行布局

Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Center }) {
  ForEach(CURRENCIES, (c: string) => {
    Text(NAMES[c])
      .fontSize(14).padding(8)
      .backgroundColor(c === this.fromCurrency ? '#0077FF' : '#eee')
      .fontColor(c === this.fromCurrency ? '#fff' : '#333')
      .borderRadius(16).margin(4)
      .onClick(() => { this.fromCurrency = c; this.convert() })
  })
}.width('92%').margin({ top: 16 })

Flex 组件是 HarmonyOS 中实现弹性布局的利器,支持主轴和交叉轴两个维度的灵活排列。本应用中使用了以下关键参数:

  • wrap: FlexWrap.Wrap:允许子项自动换行,当一行排不下时折到下一行
  • justifyContent: FlexAlign.Center:主轴方向居中对齐

如果不使用 FlexWrap.Wrap,当货币标签较多时,它们会被压缩在一行内显示不全。启用换行后,标签会在多行中自然分布,适配不同屏幕宽度。

每个货币标签同样使用了状态驱动的样式切换

.backgroundColor(c === this.fromCurrency ? '#0077FF' : '#eee')
.fontColor(c === this.fromCurrency ? '#fff' : '#333')

当前选中的源货币显示为蓝色背景白色文字,其他货币显示为灰色背景深色文字。点击任一货币标签,立即切换源货币并触发换算。

3.5 输入即计算的实时响应

TextInput({ text: this.amount })
  // ...
  .onChange((v: string) => { this.amount = v; this.convert() })

与"需要点击按钮才计算"的模式不同,本应用在 onChange 回调中直接调用 convert() 方法,实现了输入即计算的实时响应体验。用户每输入一个数字,换算结果就立即更新,这种交互模式在计算器类应用中尤为重要。


四、HarmonyOS 特性深度解析

4.1 Record 字典类型与类型安全

Record<K, V> 是 ArkTS 中用于定义字典(键值对)的类型工具。它的类型参数:

  • K:键的类型(通常为 stringnumber
  • V:值的类型
const RATES: Record<string, Record<string, number>> = { ... };

这表示 RATES 是一个字典,其键为字符串(货币代码),值为另一个字典(Record<string, number>——目标货币代码到汇率的映射)。

使用 Record 而非普通对象的好处:

  1. 类型约束:编译时检查键和值的类型是否正确
  2. 可读性:明确表达了"这是一个字典"的语义
  3. 安全性:访问不存在的键时返回 undefined 而非抛出异常

4.2 TextInput 与 @State 的双向数据绑定

TextInput({ text: this.amount })
  .onChange((v: string) => { this.amount = v; this.convert() })

在 ArkTS 中,TextInputtext 参数是单向绑定的——组件显示 @State 的值,但用户输入的变化需要通过 onChange 回调手动同步回 @State 变量。这种模式实现了外观上的双向绑定

用户输入 → onChange → this.amount = v → UI 重新渲染 → TextInput 显示最新值

如果需要真正的双向绑定(即修改 @State 变量后,TextInput 自动更新其显示内容),则必须确保 text 参数始终引用最新的 @State 值。本应用中正是这样做的,因此当 convert() 方法没有修改 amount 时,TextInput 的内容不会意外变化。

4.3 布局权重的灵活运用

TextInput({ text: this.amount })
  .layoutWeight(1)  // 占据剩余空间
  .height(48)

layoutWeight(1) 是 ArkTS 中实现弹性宽度的关键属性。在 Row 布局中,当货币符号(Text)固定宽度为36px,TextInput 通过 layoutWeight(1) 占据剩余的所有空间。这种设计确保:

  1. 货币符号始终可见且不压缩
  2. 输入框自适应屏幕宽度
  3. 在不同屏幕尺寸下都能完美适配

4.4 Button 的内容插槽

Button() {
  Text('⇅ 互换').fontSize(20).fontColor('#fff')
}

HarmonyOS 的 Button 组件支持内容插槽(Content Slot),即可以在 Button 的尾随闭包中放入任意 UI 组件作为按钮的内容。这与传统移动开发中按钮只能显示文本的模式不同,ArkTS 的按钮可以是文字、图标、甚至复杂布局的组合。


五、UI/UX 设计模式

5.1 视觉层级设计

应用的视觉层级遵循"输入 → 操作 → 输出"的自然流程:

┌─────────────────────────────────┐
│  ① 源货币输入面板(白色卡片)     │
│     显示货币符号和名称            │
├─────────────────────────────────┤
│  ② 互换按钮(圆形蓝色按钮)       │
├─────────────────────────────────┤
│  ③ 目标货币结果面板(白色卡片)    │
│     结果显示为红色大字体           │
├─────────────────────────────────┤
│  ④ 汇率信息(灰色小字)           │
├─────────────────────────────────┤
│  ⑤ 货币选择区(Flex 标签)        │
└─────────────────────────────────┘

这种从上到下的视觉引导,让用户自然完成"输入→查看结果"的任务流程。

5.2 输入与结果面板对比

源货币面板和目标货币面板采用了对称设计

元素 源货币面板 目标货币面板
背景 白色卡片 白色卡片
符号 24px 粗体 24px 粗体
内容 TextInput 可编辑 Text 只读显示
结果颜色 默认黑色 红色强调
货币名称 灰色小字 灰色小字

两个面板的高度、内边距、圆角都保持一致,形成视觉上的对仗感。唯一的区别在于——源货币面板是可编辑的输入框,目标货币面板是只读的结果展示——这种"同中有异"的设计清晰传达了各自的功能角色。

5.3 互换按钮的圆形设计

Button() {
  Text('⇅ 互换').fontSize(20).fontColor('#fff')
}.width(56).height(56).borderRadius(28).backgroundColor('#0077FF').margin(8)

互换按钮被设计为圆形(borderRadius(28) 等于宽度56的一半),这是一种符合 Material Design 规范的 FAB(Floating Action Button,浮动操作按钮)风格。圆形按钮在两个矩形面板之间形成视觉节奏的变化,同时"⇅"符号直观传达了"上下互换"的操作含义。


六、最佳实践与经验总结

6.1 汇率数据管理

本应用使用硬编码的模拟汇率数据,适用于演示和学习场景。在生产环境中,建议:

  1. 使用远程 API:通过 http 模块从第三方汇率服务获取实时汇率
  2. 本地缓存:将获取的汇率缓存到 Preferences 中,减少网络请求
  3. 定时更新:使用 setInterval 定期刷新汇率数据
  4. 离线容错:当网络不可用时,使用缓存或最后已知的汇率

6.2 货币代码标准化

使用 ISO 4217 标准的三字母货币代码(CNY、USD、EUR 等)作为货币标识,这是国际金融领域的通行做法。好处包括:

  • 通用性:全球通用的标准代码
  • 可扩展性:易于添加新货币
  • 兼容性:与外部 API 的数据格式兼容

6.3 输入防抖考量

在实时换算场景中,如果未来接入网络 API 获取实时汇率,频繁的 onChange 触发可能导致大量网络请求。此时应考虑输入防抖(Debounce)机制:

// 防抖示例(非本应用代码)
let debounceTimer: number = -1;
onChange((v: string) => {
  this.amount = v;
  clearTimeout(debounceTimer);
  debounceTimer = setTimeout(() => this.convert(), 300);
});

不过在本应用中,换算是纯本地计算,性能开销极小,因此无需防抖。

6.4 可选链的安全访问

const rate = RATES[this.fromCurrency]?.[this.toCurrency];

这种写法等价于:

const fromRates = RATES[this.fromCurrency];
const rate = fromRates !== undefined ? fromRates[this.toCurrency] : undefined;

可选链操作符让代码更简洁、更安全,是 ArkTS 开发中推荐使用的现代语法特性。

6.5 国际化考虑

本应用已经包含了中文货币名称。如果要支持多语言,可以将 NAMES 设计为按语言分组的嵌套字典:

const NAMES: Record<string, Record<string, string>> = {
  'zh': { 'CNY': '人民币', ... },
  'en': { 'CNY': 'Chinese Yuan', ... },
};

七、总结

货币转换应用在约90行代码中,完美展示了 ArkTS 声明式 UI 开发的多个核心概念:

  • Record 字典数据结构:O(1) 时间复杂度的高效数据查询
  • Flex 弹性换行布局:自适应屏幕宽度的标签排列
  • @State 状态驱动:自动触发的 UI 渲染更新
  • 实时计算反馈:输入即计算的流畅交互体验
  • 可选链安全访问:优雅的嵌套数据访问方式
  • 状态驱动的样式切换:视觉反馈与功能状态绑定

这个应用不仅是一个实用的汇率换算工具,更是一个数据驱动 UI 开发的典范案例。它清晰地展示了如何将数据结构、业务逻辑和 UI 表示层有机地组织在一起,构建出既高效又美观的应用程序。对于希望深入学习 ArkTS 数据管理和布局技术的开发者来说,货币转换应用提供了一个绝佳的参考范本。

Logo

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

更多推荐