Hugo-Jiang's picture
fix button click event
67c9c60
/**
* 汇率换算器前端交互逻辑
*
* 功能:
* - 加载货币列表和汇率数据
* - 实时输入实时换算
* - 搜索和筛选货币
* - 防抖处理优化性能
*/
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();
});