Spaces:
Sleeping
Sleeping
| /** | |
| * 汇率换算器前端交互逻辑 | |
| * | |
| * 功能: | |
| * - 加载货币列表和汇率数据 | |
| * - 实时输入实时换算 | |
| * - 搜索和筛选货币 | |
| * - 防抖处理优化性能 | |
| */ | |
| class CurrencyConverter { | |
| constructor() { | |
| // 数据 | |
| this.currencies = []; | |
| this.rates = {}; | |
| this.baseCurrency = "CNY"; | |
| // 状态 | |
| this.activeInput = null; | |
| this.debounceTimer = null; | |
| this.currentFilter = "all"; | |
| // 常用货币列表 | |
| this.priorityCodes = [ | |
| "CNY", | |
| "USD", | |
| "EUR", | |
| "GBP", | |
| "JPY", | |
| "HKD", | |
| "AUD", | |
| "CAD", | |
| "CHF", | |
| "SGD", | |
| "KRW", | |
| "TWD", | |
| "THB", | |
| "MYR", | |
| "INR", | |
| "RUB", | |
| ]; | |
| // 货币国旗映射(更全面的覆盖) | |
| this.currencyFlags = { | |
| // 主要货币 | |
| USD: "🇺🇸", | |
| EUR: "🇪🇺", | |
| GBP: "🇬🇧", | |
| JPY: "🇯🇵", | |
| CNY: "🇨🇳", | |
| AUD: "🇦🇺", | |
| CAD: "🇨🇦", | |
| CHF: "🇨🇭", | |
| HKD: "🇭🇰", | |
| SGD: "🇸🇬", | |
| // 欧洲 | |
| SEK: "🇸🇪", | |
| NOK: "🇳🇴", | |
| DKK: "🇩🇰", | |
| PLN: "🇵🇱", | |
| HUF: "🇭🇺", | |
| CZK: "🇨🇿", | |
| RON: "🇷🇴", | |
| BGN: "🇧🇬", | |
| HRK: "🇭🇷", | |
| ISK: "🇮🇸", | |
| // 亚洲 | |
| KRW: "🇰🇷", | |
| TWD: "🇹🇼", | |
| THB: "🇹🇭", | |
| MYR: "🇲🇾", | |
| INR: "🇮🇳", | |
| IDR: "🇮🇩", | |
| VND: "🇻🇳", | |
| PHP: "🇵🇭", | |
| PKR: "🇵🇰", | |
| // 中东 | |
| AED: "🇦🇪", | |
| SAR: "🇸🇦", | |
| ILS: "🇮🇱", | |
| TRY: "🇹🇷", | |
| EGP: "🇪🇬", | |
| // 美洲 | |
| MXN: "🇲🇽", | |
| BRL: "🇧🇷", | |
| ARS: "🇦🇷", | |
| CLP: "🇨🇱", | |
| COP: "🇨🇴", | |
| PEN: "🇵🇪", | |
| UYU: "🇺🇾", | |
| // 非洲 | |
| ZAR: "🇿🇦", | |
| NGN: "🇳🇬", | |
| KES: "🇰🇪", | |
| // 大洋洲 | |
| NZD: "🇳🇿", | |
| // 东欧与独联体 | |
| RUB: "🇷🇺", | |
| UAH: "🇺🇦", | |
| KZT: "🇰🇿", | |
| }; | |
| // 货币符号映射(用于没有国旗的货币) | |
| this.currencySymbols = { | |
| USD: "$", | |
| EUR: "€", | |
| GBP: "£", | |
| JPY: "¥", | |
| CNY: "¥", | |
| AUD: "$", | |
| CAD: "$", | |
| CHF: "Fr", | |
| HKD: "$", | |
| SGD: "$", | |
| KRW: "₩", | |
| TWD: "$", | |
| THB: "฿", | |
| MYR: "RM", | |
| INR: "₹", | |
| IDR: "Rp", | |
| VND: "₫", | |
| PHP: "₱", | |
| AED: "د.إ", | |
| SAR: "﷼", | |
| ILS: "₪", | |
| TRY: "₺", | |
| RUB: "₽", | |
| MXN: "$", | |
| BRL: "R$", | |
| ARS: "$", | |
| ZAR: "R", | |
| NZD: "$", | |
| SEK: "kr", | |
| NOK: "kr", | |
| DKK: "kr", | |
| PLN: "zł", | |
| CZK: "Kč", | |
| HUF: "Ft", | |
| RON: "lei", | |
| BGN: "лв", | |
| HRK: "kn", | |
| ISK: "kr", | |
| PKR: "₨", | |
| EGP: "£", | |
| CLP: "$", | |
| COP: "$", | |
| PEN: "S/", | |
| UYU: "$", | |
| NGN: "₦", | |
| KES: "KSh", | |
| UAH: "₴", | |
| KZT: "₸", | |
| }; | |
| // 初始化 | |
| this.init(); | |
| } | |
| /** | |
| * 初始化应用 | |
| */ | |
| async init() { | |
| try { | |
| // 并行加载数据 | |
| await Promise.all([this.loadCurrencies(), this.loadRates()]); | |
| // 隐藏加载状态 | |
| this.hideLoading(); | |
| // 渲染界面 | |
| this.renderCurrencyGrid(); | |
| this.setupEventListeners(); | |
| // 应用默认筛选状态 | |
| this.applyFilter(); | |
| this.updateStatusInfo(); | |
| } catch (error) { | |
| console.error("Initialization failed:", error); | |
| this.showError("加载汇率数据失败,请检查网络连接"); | |
| } | |
| } | |
| /** | |
| * 加载货币列表 | |
| */ | |
| async loadCurrencies() { | |
| try { | |
| const response = await fetch("/api/currencies"); | |
| if (!response.ok) { | |
| throw new Error(`HTTP ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| if (data.success) { | |
| this.currencies = data.currencies; | |
| console.log(`Loaded ${this.currencies.length} currencies`); | |
| } else { | |
| throw new Error("API returned unsuccessful response"); | |
| } | |
| } catch (error) { | |
| console.error("Failed to load currencies:", error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * 加载汇率数据 | |
| */ | |
| async loadRates() { | |
| try { | |
| const response = await fetch("/api/rates"); | |
| if (!response.ok) { | |
| throw new Error(`HTTP ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| if (data.success) { | |
| this.rates = data.rates; | |
| this.baseCurrency = data.base_currency; | |
| console.log( | |
| `Loaded rates for ${Object.keys(this.rates).length} currencies` | |
| ); | |
| } else { | |
| throw new Error("API returned unsuccessful response"); | |
| } | |
| } catch (error) { | |
| console.error("Failed to load rates:", error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * 获取货币图标(国旗或符号) | |
| */ | |
| getCurrencyIcon(currency) { | |
| // 优先使用国旗 emoji | |
| if (this.currencyFlags[currency.code]) { | |
| return this.currencyFlags[currency.code]; | |
| } | |
| // 使用货币符号 | |
| if (this.currencySymbols[currency.code]) { | |
| return this.currencySymbols[currency.code]; | |
| } | |
| // 备选:使用 currency.symbol(如果API提供) | |
| if (currency.symbol && currency.symbol.length <= 3) { | |
| return currency.symbol; | |
| } | |
| // 最后:使用货币代码的前两个字母 | |
| return currency.code.substring(0, 2); | |
| } | |
| /** | |
| * 渲染货币网格 | |
| */ | |
| renderCurrencyGrid() { | |
| const grid = document.getElementById("currencyGrid"); | |
| if (!grid) return; | |
| grid.innerHTML = this.currencies | |
| .map((currency) => { | |
| const isPriority = this.priorityCodes.includes(currency.code); | |
| const rate = this.rates[currency.code] || 0; | |
| const icon = this.getCurrencyIcon(currency); | |
| return ` | |
| <div class="currency-card ${isPriority ? "priority" : ""}" | |
| data-code="${currency.code}" | |
| data-priority="${isPriority}"> | |
| <div class="currency-icon">${icon}</div> | |
| <div class="currency-info"> | |
| <div class="currency-code">${currency.code}</div> | |
| <div class="currency-name" title="${currency.name}">${ | |
| currency.name_cn | |
| }</div> | |
| </div> | |
| <div class="currency-input"> | |
| <input type="number" | |
| id="input-${currency.code}" | |
| data-code="${currency.code}" | |
| placeholder="0.00" | |
| step="any" | |
| inputmode="decimal"> | |
| <div class="currency-rate">1 ${ | |
| this.baseCurrency | |
| } = ${this.formatRate(rate)} ${currency.code}</div> | |
| </div> | |
| </div> | |
| `; | |
| }) | |
| .join(""); | |
| } | |
| /** | |
| * 设置事件监听器 | |
| */ | |
| setupEventListeners() { | |
| const grid = document.getElementById("currencyGrid"); | |
| const searchInput = document.getElementById("searchInput"); | |
| const retryBtn = document.getElementById("retryBtn"); | |
| // ========== 输入事件(事件委托)========== | |
| if (grid) { | |
| // 输入事件 | |
| grid.addEventListener("input", (e) => { | |
| if (e.target.tagName === "INPUT") { | |
| this.handleInput(e.target); | |
| } | |
| }); | |
| // 焦点事件 | |
| grid.addEventListener("focusin", (e) => { | |
| if (e.target.tagName === "INPUT") { | |
| this.activeInput = e.target; | |
| const card = e.target.closest(".currency-card"); | |
| if (card) { | |
| card.classList.add("active"); | |
| } | |
| } | |
| }); | |
| grid.addEventListener("focusout", (e) => { | |
| if (e.target.tagName === "INPUT") { | |
| const card = e.target.closest(".currency-card"); | |
| if (card) { | |
| card.classList.remove("active"); | |
| } | |
| } | |
| }); | |
| } | |
| // ========== 搜索事件 ========== | |
| if (searchInput) { | |
| searchInput.addEventListener("input", (e) => { | |
| this.filterCurrencies(e.target.value); | |
| }); | |
| } | |
| // ========== 筛选按钮 ========== | |
| document.querySelectorAll(".filter-btn").forEach((btn) => { | |
| btn.addEventListener("click", (e) => { | |
| // 获取实际的按钮元素(处理点击子元素的情况) | |
| const button = e.currentTarget; | |
| // 更新按钮状态 | |
| document | |
| .querySelectorAll(".filter-btn") | |
| .forEach((b) => b.classList.remove("active")); | |
| button.classList.add("active"); | |
| // 应用筛选 | |
| this.currentFilter = button.dataset.filter; | |
| this.applyFilter(); | |
| }); | |
| }); | |
| // ========== 重试按钮 ========== | |
| if (retryBtn) { | |
| retryBtn.addEventListener("click", () => { | |
| this.hideError(); | |
| this.showLoading(); | |
| this.init(); | |
| }); | |
| } | |
| } | |
| /** | |
| * 处理输入事件 | |
| */ | |
| handleInput(input) { | |
| // 清除之前的定时器 | |
| clearTimeout(this.debounceTimer); | |
| // 防抖处理:100ms 后执行计算 | |
| this.debounceTimer = setTimeout(() => { | |
| this.calculateAll(input); | |
| }, 100); | |
| } | |
| /** | |
| * 计算所有货币的换算值 | |
| */ | |
| calculateAll(sourceInput) { | |
| const sourceCode = sourceInput.dataset.code; | |
| const sourceValue = parseFloat(sourceInput.value); | |
| // 如果输入为空、无效或为零,清空所有其他输入 | |
| if ( | |
| isNaN(sourceValue) || | |
| sourceValue === 0 || | |
| sourceInput.value.trim() === "" | |
| ) { | |
| this.clearAll(sourceInput); | |
| return; | |
| } | |
| const sourceRate = this.rates[sourceCode]; | |
| if (!sourceRate) { | |
| console.warn(`Rate not found for ${sourceCode}`); | |
| return; | |
| } | |
| // 计算并更新所有其他货币 | |
| this.currencies.forEach((currency) => { | |
| if (currency.code === sourceCode) return; | |
| const targetRate = this.rates[currency.code]; | |
| if (!targetRate) return; | |
| // 交叉汇率计算 | |
| // sourceRate = 1 CNY = X source_currency | |
| // targetRate = 1 CNY = Y target_currency | |
| // 所以 1 source_currency = (targetRate / sourceRate) target_currency | |
| const crossRate = targetRate / sourceRate; | |
| const result = sourceValue * crossRate; | |
| const input = document.getElementById(`input-${currency.code}`); | |
| if (input) { | |
| input.value = this.formatNumber(result); | |
| } | |
| }); | |
| } | |
| /** | |
| * 清空所有输入(除了指定的输入框) | |
| */ | |
| clearAll(exceptInput = null) { | |
| this.currencies.forEach((currency) => { | |
| const input = document.getElementById(`input-${currency.code}`); | |
| if (input && input !== exceptInput) { | |
| input.value = ""; | |
| } | |
| }); | |
| } | |
| /** | |
| * 格式化数字显示 | |
| */ | |
| formatNumber(num) { | |
| if (num === 0) return ""; | |
| // 根据数值大小选择精度 | |
| if (Math.abs(num) >= 1000) { | |
| return num.toFixed(2); | |
| } else if (Math.abs(num) >= 1) { | |
| return num.toFixed(4); | |
| } else if (Math.abs(num) >= 0.0001) { | |
| return num.toFixed(6); | |
| } else { | |
| return num.toExponential(4); | |
| } | |
| } | |
| /** | |
| * 格式化汇率显示 | |
| */ | |
| formatRate(rate) { | |
| if (rate >= 1) { | |
| return rate.toFixed(4); | |
| } else if (rate >= 0.0001) { | |
| return rate.toFixed(6); | |
| } else { | |
| return rate.toExponential(4); | |
| } | |
| } | |
| /** | |
| * 搜索过滤货币 | |
| */ | |
| filterCurrencies(keyword) { | |
| const cards = document.querySelectorAll(".currency-card"); | |
| const lowerKeyword = keyword.toLowerCase().trim(); | |
| cards.forEach((card) => { | |
| const code = card.dataset.code.toLowerCase(); | |
| const currency = this.currencies.find( | |
| (c) => c.code === card.dataset.code | |
| ); | |
| const name = currency ? currency.name.toLowerCase() : ""; | |
| const nameCn = currency ? currency.name_cn : ""; | |
| // 匹配代码、英文名或中文名 | |
| const matchesSearch = | |
| !lowerKeyword || | |
| code.includes(lowerKeyword) || | |
| name.includes(lowerKeyword) || | |
| nameCn.includes(keyword); | |
| // 同时考虑当前筛选状态 | |
| const matchesFilter = | |
| this.currentFilter === "all" || | |
| (this.currentFilter === "priority" && card.dataset.priority === "true"); | |
| card.classList.toggle("hidden", !(matchesSearch && matchesFilter)); | |
| }); | |
| } | |
| /** | |
| * 应用筛选(常用/全部) | |
| */ | |
| applyFilter() { | |
| const searchInput = document.getElementById("searchInput"); | |
| const keyword = searchInput ? searchInput.value : ""; | |
| this.filterCurrencies(keyword); | |
| } | |
| /** | |
| * 更新状态信息 | |
| */ | |
| async updateStatusInfo() { | |
| try { | |
| const response = await fetch("/api/status"); | |
| const data = await response.json(); | |
| // 更新时间 | |
| const lastUpdateEl = document.getElementById("lastUpdate"); | |
| if (lastUpdateEl && data.last_update) { | |
| const date = new Date(data.last_update); | |
| lastUpdateEl.textContent = date.toLocaleString("zh-CN"); | |
| } | |
| // 货币数量 | |
| const countEl = document.getElementById("currencyCount"); | |
| if (countEl) { | |
| countEl.textContent = data.currencies_count || this.currencies.length; | |
| } | |
| // 基准货币 | |
| const baseEl = document.getElementById("baseCurrency"); | |
| if (baseEl) { | |
| baseEl.textContent = this.baseCurrency; | |
| } | |
| // 更新间隔 | |
| const updateIntervalEl = document.getElementById("updateInterval"); | |
| if (updateIntervalEl && data.update_interval_seconds) { | |
| updateIntervalEl.textContent = this.formatUpdateInterval( | |
| data.update_interval_seconds | |
| ); | |
| } | |
| } catch (error) { | |
| console.error("Failed to update status:", error); | |
| } | |
| } | |
| /** | |
| * 格式化更新间隔显示 | |
| */ | |
| formatUpdateInterval(seconds) { | |
| if (seconds < 60) { | |
| return `每 ${seconds} 秒更新一次`; | |
| } else if (seconds < 3600) { | |
| const minutes = Math.floor(seconds / 60); | |
| return `每 ${minutes} 分钟更新一次`; | |
| } else { | |
| const hours = Math.floor(seconds / 3600); | |
| return `每 ${hours} 小时更新一次`; | |
| } | |
| } | |
| /** | |
| * 显示加载状态 | |
| */ | |
| showLoading() { | |
| const loading = document.getElementById("loading"); | |
| const grid = document.getElementById("currencyGrid"); | |
| const tips = document.getElementById("tips"); | |
| if (loading) loading.classList.remove("hidden"); | |
| if (loading) loading.style.display = "flex"; | |
| if (grid) grid.style.display = "none"; | |
| if (tips) tips.style.display = "none"; | |
| } | |
| /** | |
| * 隐藏加载状态 | |
| */ | |
| hideLoading() { | |
| const loading = document.getElementById("loading"); | |
| const grid = document.getElementById("currencyGrid"); | |
| const tips = document.getElementById("tips"); | |
| if (loading) loading.classList.add("hidden"); | |
| if (loading) loading.style.display = "none"; | |
| if (grid) grid.style.display = "grid"; | |
| if (tips) tips.style.display = "flex"; | |
| } | |
| /** | |
| * 显示错误信息 | |
| */ | |
| showError(message) { | |
| const errorDiv = document.getElementById("errorMessage"); | |
| const errorText = document.getElementById("errorText"); | |
| const loading = document.getElementById("loading"); | |
| const grid = document.getElementById("currencyGrid"); | |
| if (loading) loading.style.display = "none"; | |
| if (grid) grid.style.display = "none"; | |
| if (errorDiv) { | |
| errorDiv.style.display = "flex"; | |
| } | |
| if (errorText) { | |
| errorText.textContent = message; | |
| } | |
| } | |
| /** | |
| * 隐藏错误信息 | |
| */ | |
| hideError() { | |
| const errorDiv = document.getElementById("errorMessage"); | |
| if (errorDiv) { | |
| errorDiv.style.display = "none"; | |
| } | |
| } | |
| } | |
| // ==================== 初始化应用 ==================== | |
| document.addEventListener("DOMContentLoaded", () => { | |
| // 创建全局实例 | |
| window.currencyConverter = new CurrencyConverter(); | |
| }); | |