logo
Build Your Own Web Toolbox

在本节中,我们将实现在线工具箱的汇率转换功能。主要包括以下功能点:

  • 实时汇率查询
  • 多币种支持
  • 汇率计算器
  • 历史汇率走势图

页面设计

页面布局

<!-- pages/tools/currency.vue -->
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { Currency } from '~/types'

// 币种列表
const currencies = ref<Currency[]>([
  { code: 'CNY', name: '人民币' },
  { code: 'USD', name: '美元' },
  { code: 'EUR', name: '欧元' },
  { code: 'JPY', name: '日元' },
  { code: 'GBP', name: '英镑' },
  // 更多币种...
])

// 表单数据
const form = ref({
  amount: 100,
  fromCurrency: 'CNY',
  toCurrency: 'USD'
})

// 转换结果
const result = ref<number | null>(null)
</script>

<template>
  <div class="container mx-auto px-4 py-8">
    <h1 class="text-3xl font-bold mb-8">汇率转换</h1>
    
    <!-- 转换表单 -->
    <div class="card bg-base-100 shadow-xl">
      <div class="card-body">
        <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
          <!-- 金额输入 -->
          <div class="form-control">
            <label class="label">
              <span class="label-text">金额</span>
            </label>
            <input
              v-model="form.amount"
              type="number"
              class="input input-bordered"
              min="0"
              step="0.01"
            />
          </div>
          
          <!-- 源币种选择 -->
          <div class="form-control">
            <label class="label">
              <span class="label-text">从</span>
            </label>
            <select
              v-model="form.fromCurrency"
              class="select select-bordered"
            >
              <option
                v-for="currency in currencies"
                :key="currency.code"
                :value="currency.code"
              >
                {{ currency.name }} ({{ currency.code }})
              </option>
            </select>
          </div>
          
          <!-- 目标币种选择 -->
          <div class="form-control">
            <label class="label">
              <span class="label-text">到</span>
            </label>
            <select
              v-model="form.toCurrency"
              class="select select-bordered"
            >
              <option
                v-for="currency in currencies"
                :key="currency.code"
                :value="currency.code"
              >
                {{ currency.name }} ({{ currency.code }})
              </option>
            </select>
          </div>
        </div>
        
        <!-- 转换按钮 -->
        <div class="mt-6">
          <button
            class="btn btn-primary"
            @click="handleConvert"
          >
            转换
          </button>
        </div>
        
        <!-- 转换结果 -->
        <div v-if="result" class="mt-6">
          <div class="text-xl">
            {{ form.amount }} {{ form.fromCurrency }} =
            {{ result.toFixed(2) }} {{ form.toCurrency }}
          </div>
        </div>
      </div>
    </div>
    
    <!-- 历史走势图 -->
    <div class="mt-8">
      <h2 class="text-2xl font-bold mb-4">历史走势</h2>
      <div class="card bg-base-100 shadow-xl">
        <div class="card-body">
          <CurrencyChart
            :from-currency="form.fromCurrency"
            :to-currency="form.toCurrency"
          />
        </div>
      </div>
    </div>
  </div>
</template>

类型定义

// types/currency.ts
export interface Currency {
  code: string
  name: string
}

export interface ExchangeRate {
  from: string
  to: string
  rate: number
  timestamp: number
}

export interface HistoricalRate {
  date: string
  rate: number
}

API 封装

// composables/useCurrency.ts
import { ref } from 'vue'
import type { ExchangeRate, HistoricalRate } from '~/types'

export function useCurrency() {
  const loading = ref(false)
  const error = ref<string | null>(null)
  
  // 获取实时汇率
  async function getExchangeRate(
    fromCurrency: string,
    toCurrency: string
  ): Promise<ExchangeRate | null> {
    loading.value = true
    error.value = null
    
    try {
      // 调用聚合数据汇率 API
      const response = await fetch(`/api/currency/rate?from=${fromCurrency}&to=${toCurrency}`)
      const data = await response.json()
      
      if (!data.success) {
        throw new Error(data.message)
      }
      
      return {
        from: fromCurrency,
        to: toCurrency,
        rate: data.result.rate,
        timestamp: data.result.timestamp
      }
    } catch (e) {
      error.value = e.message
      return null
    } finally {
      loading.value = false
    }
  }
  
  // 获取历史汇率数据
  async function getHistoricalRates(
    fromCurrency: string,
    toCurrency: string,
    days: number = 30
  ): Promise<HistoricalRate[]> {
    loading.value = true
    error.value = null
    
    try {
      const response = await fetch(
        `/api/currency/history?from=${fromCurrency}&to=${toCurrency}&days=${days}`
      )
      const data = await response.json()
      
      if (!data.success) {
        throw new Error(data.message)
      }
      
      return data.result.rates
    } catch (e) {
      error.value = e.message
      return []
    } finally {
      loading.value = false
    }
  }
  
  return {
    loading,
    error,
    getExchangeRate,
    getHistoricalRates
  }
}

汇率图表组件

<!-- components/CurrencyChart.vue -->
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import { Line } from 'vue-chartjs'
import type { HistoricalRate } from '~/types'

const props = defineProps<{
  fromCurrency: string
  toCurrency: string
}>()

const { getHistoricalRates } = useCurrency()
const chartData = ref<HistoricalRate[]>([])

async function loadChartData() {
  chartData.value = await getHistoricalRates(
    props.fromCurrency,
    props.toCurrency
  )
}

// 监听币种变化,重新加载数据
watch(
  [() => props.fromCurrency, () => props.toCurrency],
  () => loadChartData(),
  { immediate: true }
)

// 图表配置
const chartConfig = computed(() => ({
  type: 'line',
  data: {
    labels: chartData.value.map(item => item.date),
    datasets: [
      {
        label: `${props.fromCurrency}/${props.toCurrency} 汇率`,
        data: chartData.value.map(item => item.rate),
        borderColor: '#3b82f6',
        tension: 0.1
      }
    ]
  },
  options: {
    responsive: true,
    maintainAspectRatio: false,
    plugins: {
      legend: {
        position: 'top'
      }
    },
    scales: {
      y: {
        beginAtZero: false
      }
    }
  }
}))
</script>

<template>
  <div class="h-[400px]">
    <Line
      v-if="chartData.length > 0"
      :data="chartConfig.data"
      :options="chartConfig.options"
    />
    <div
      v-else
      class="flex items-center justify-center h-full"
    >
      <span class="loading loading-spinner loading-lg"></span>
    </div>
  </div>
</template>

后端 API 实现

// server/api/currency/rate.ts
import { defineEventHandler } from 'h3'

export default defineEventHandler(async (event) => {
  const query = getQuery(event)
  const { from, to } = query
  
  if (!from || !to) {
    return {
      success: false,
      message: '请提供源币种和目标币种'
    }
  }
  
  try {
    // 调用聚合数据 API
    const response = await fetch(
      `http://apis.juhe.cn/fapig/currency/exchange?from=${from}&to=${to}&key=${process.env.JUHE_API_KEY}`
    )
    const data = await response.json()
    
    if (data.error_code !== 0) {
      throw new Error(data.reason)
    }
    
    return {
      success: true,
      result: {
        rate: data.result.rate,
        timestamp: data.result.timestamp
      }
    }
  } catch (error) {
    return {
      success: false,
      message: error.message
    }
  }
})

使用说明

  1. 安装依赖
# 安装 Chart.js 和 Vue Chart.js
npm install chart.js vue-chartjs
  1. 环境变量配置

在项目根目录创建 .env 文件:

# .env
JUHE_API_KEY=你的聚合数据API密钥
  1. 功能测试
  • 启动开发服务器:npm run dev
  • 访问 http://localhost:3000/tools/currency
  • 输入金额,选择币种进行转换
  • 查看历史汇率走势图

优化建议

  1. 缓存优化

    • 使用 localStorage 缓存最近查询的汇率
    • 设置合理的缓存过期时间
  2. 性能优化

    • 使用防抖处理频繁的汇率查询
    • 按需加载 Chart.js
  3. 用户体验

    • 添加常用币种收藏功能
    • 支持快捷键操作
    • 添加汇率提醒功能

下一步

完成汇率转换功能后,我们将继续开发:

  1. 二维码生成工具
  2. 图片处理功能
  3. 更多实用工具