refactor: update currency component for improved clarity and functionality
- Translated currency names and comments from Chinese to English for better accessibility. - Enhanced caching logic for exchange rates, ensuring data validity based on the date. - Improved user feedback messages for currency addition and removal actions. - Updated UI elements to reflect changes in language and functionality, including placeholders and labels.
This commit is contained in:
		| @@ -15,46 +15,45 @@ interface Currency { | ||||
|   symbol: string; | ||||
| } | ||||
|  | ||||
| // 常用货币列表 (仅包含 Frankfurter API 支持的货币) | ||||
| // Available currencies (supported by Frankfurter API) | ||||
| const AVAILABLE_CURRENCIES: Currency[] = [ | ||||
|   { code: "USD", name: "美元", symbol: "$" }, | ||||
|   { code: "CNY", name: "人民币", symbol: "¥" }, | ||||
|   { code: "EUR", name: "欧元", symbol: "€" }, | ||||
|   { code: "GBP", name: "英镑", symbol: "£" }, | ||||
|   { code: "JPY", name: "日元", symbol: "¥" }, | ||||
|   { code: "HKD", name: "港币", symbol: "HK$" }, | ||||
|   { code: "AUD", name: "澳元", symbol: "A$" }, | ||||
|   { code: "CAD", name: "加元", symbol: "C$" }, | ||||
|   { code: "SGD", name: "新加坡元", symbol: "S$" }, | ||||
|   { code: "CHF", name: "瑞士法郎", symbol: "CHF" }, | ||||
|   { code: "NZD", name: "新西兰元", symbol: "NZ$" }, | ||||
|   { code: "KRW", name: "韩元", symbol: "₩" }, | ||||
|   { code: "THB", name: "泰铢", symbol: "฿" }, | ||||
|   { code: "MYR", name: "马来西亚林吉特", symbol: "RM" }, | ||||
|   { code: "INR", name: "印度卢比", symbol: "₹" }, | ||||
|   { code: "BRL", name: "巴西雷亚尔", symbol: "R$" }, | ||||
|   { code: "MXN", name: "墨西哥比索", symbol: "MX$" }, | ||||
|   { code: "SEK", name: "瑞典克朗", symbol: "kr" }, | ||||
|   { code: "NOK", name: "挪威克朗", symbol: "kr" }, | ||||
|   { code: "DKK", name: "丹麦克朗", symbol: "kr" }, | ||||
|   { code: "PLN", name: "波兰兹罗提", symbol: "zł" }, | ||||
|   { code: "TRY", name: "土耳其里拉", symbol: "₺" }, | ||||
|   { code: "PHP", name: "菲律宾比索", symbol: "₱" }, | ||||
|   { code: "IDR", name: "印尼盾", symbol: "Rp" }, | ||||
|   { code: "ILS", name: "以色列新谢克尔", symbol: "₪" }, | ||||
|   { code: "CZK", name: "捷克克朗", symbol: "Kč" }, | ||||
|   { code: "RON", name: "罗马尼亚列伊", symbol: "lei" }, | ||||
|   { code: "HUF", name: "匈牙利福林", symbol: "Ft" }, | ||||
|   { code: "BGN", name: "保加利亚列弗", symbol: "лв" }, | ||||
|   { code: "ISK", name: "冰岛克朗", symbol: "kr" }, | ||||
|   { code: "USD", name: "US Dollar", symbol: "$" }, | ||||
|   { code: "CNY", name: "Chinese Yuan", symbol: "¥" }, | ||||
|   { code: "EUR", name: "Euro", symbol: "€" }, | ||||
|   { code: "GBP", name: "British Pound", symbol: "£" }, | ||||
|   { code: "JPY", name: "Japanese Yen", symbol: "¥" }, | ||||
|   { code: "HKD", name: "Hong Kong Dollar", symbol: "HK$" }, | ||||
|   { code: "AUD", name: "Australian Dollar", symbol: "A$" }, | ||||
|   { code: "CAD", name: "Canadian Dollar", symbol: "C$" }, | ||||
|   { code: "SGD", name: "Singapore Dollar", symbol: "S$" }, | ||||
|   { code: "CHF", name: "Swiss Franc", symbol: "CHF" }, | ||||
|   { code: "NZD", name: "New Zealand Dollar", symbol: "NZ$" }, | ||||
|   { code: "KRW", name: "South Korean Won", symbol: "₩" }, | ||||
|   { code: "THB", name: "Thai Baht", symbol: "฿" }, | ||||
|   { code: "MYR", name: "Malaysian Ringgit", symbol: "RM" }, | ||||
|   { code: "INR", name: "Indian Rupee", symbol: "₹" }, | ||||
|   { code: "BRL", name: "Brazilian Real", symbol: "R$" }, | ||||
|   { code: "MXN", name: "Mexican Peso", symbol: "MX$" }, | ||||
|   { code: "SEK", name: "Swedish Krona", symbol: "kr" }, | ||||
|   { code: "NOK", name: "Norwegian Krone", symbol: "kr" }, | ||||
|   { code: "DKK", name: "Danish Krone", symbol: "kr" }, | ||||
|   { code: "PLN", name: "Polish Złoty", symbol: "zł" }, | ||||
|   { code: "TRY", name: "Turkish Lira", symbol: "₺" }, | ||||
|   { code: "PHP", name: "Philippine Peso", symbol: "₱" }, | ||||
|   { code: "IDR", name: "Indonesian Rupiah", symbol: "Rp" }, | ||||
|   { code: "ILS", name: "Israeli New Shekel", symbol: "₪" }, | ||||
|   { code: "CZK", name: "Czech Koruna", symbol: "Kč" }, | ||||
|   { code: "RON", name: "Romanian Leu", symbol: "lei" }, | ||||
|   { code: "HUF", name: "Hungarian Forint", symbol: "Ft" }, | ||||
|   { code: "BGN", name: "Bulgarian Lev", symbol: "лв" }, | ||||
|   { code: "ISK", name: "Icelandic Króna", symbol: "kr" }, | ||||
| ]; | ||||
|  | ||||
| // 必选货币(不可删除) - 美元第一,人民币第二 | ||||
| // Required currencies (cannot be removed) - USD first, CNY second | ||||
| const REQUIRED_CURRENCIES = ["USD", "CNY"]; | ||||
|  | ||||
| const STORAGE_KEY = "selectedCurrencies"; | ||||
| const RATES_CACHE_KEY = "currencyRatesCache"; | ||||
| const RATES_CACHE_DURATION = 60 * 60 * 1000; // 1小时(毫秒) | ||||
|  | ||||
| const Tool: FC = () => { | ||||
|   const [amounts, setAmounts] = useState<Record<string, string>>({}); | ||||
| @@ -63,7 +62,7 @@ const Tool: FC = () => { | ||||
|   const [loading, setLoading] = useState<boolean>(false); | ||||
|   const [open, setOpen] = useState(false); | ||||
|  | ||||
|   // 从 localStorage 加载已选货币 | ||||
|   // Load selected currencies from localStorage | ||||
|   useEffect(() => { | ||||
|     try { | ||||
|       const saved = localStorage.getItem(STORAGE_KEY); | ||||
| @@ -77,11 +76,11 @@ const Tool: FC = () => { | ||||
|     } catch (error) { | ||||
|       console.error("Failed to load saved currencies:", error); | ||||
|     } | ||||
|     // 默认选择 USD 和 CNY (美元第一) | ||||
|     // Default: USD and CNY (USD first) | ||||
|     setSelectedCurrencies(["USD", "CNY"]); | ||||
|   }, []); | ||||
|  | ||||
|   // 保存已选货币到 localStorage | ||||
|   // Save selected currencies to localStorage | ||||
|   useEffect(() => { | ||||
|     if (selectedCurrencies.length >= 2) { | ||||
|       try { | ||||
| @@ -92,39 +91,39 @@ const Tool: FC = () => { | ||||
|     } | ||||
|   }, [selectedCurrencies]); | ||||
|  | ||||
|   // 获取汇率数据 - 使用 stale-while-revalidate 缓存策略 | ||||
|   // Fetch exchange rates with date-based caching | ||||
|   useEffect(() => { | ||||
|     const fetchRates = async (useCache = true) => { | ||||
|     const fetchRates = async () => { | ||||
|       try { | ||||
|         // 1. 尝试从缓存加载 | ||||
|         if (useCache) { | ||||
|           const cached = localStorage.getItem(RATES_CACHE_KEY); | ||||
|           if (cached) { | ||||
|             try { | ||||
|               const { rates: cachedRates, timestamp } = JSON.parse(cached); | ||||
|               const age = Date.now() - timestamp; | ||||
|                | ||||
|               if (age < RATES_CACHE_DURATION) { | ||||
|                 // 缓存未过期,直接使用 | ||||
|                 setRates(cachedRates); | ||||
|                 setLoading(false); | ||||
|                 return; | ||||
|               } else { | ||||
|                 // 缓存过期,先使用旧数据,后台更新 | ||||
|                 setRates(cachedRates); | ||||
|                 setLoading(false); | ||||
|                 // 继续执行下面的网络请求 | ||||
|               } | ||||
|             } catch (e) { | ||||
|               console.error("Failed to parse cached rates:", e); | ||||
|         // 1. Try to load from cache | ||||
|         const cached = localStorage.getItem(RATES_CACHE_KEY); | ||||
|         if (cached) { | ||||
|           try { | ||||
|             const { rates: cachedRates, date: cachedDate } = JSON.parse(cached); | ||||
|              | ||||
|             // Check if cached data is from today | ||||
|             const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD | ||||
|              | ||||
|             if (cachedDate === today) { | ||||
|               // Cache is valid (same date), use it directly | ||||
|               setRates(cachedRates); | ||||
|               setLoading(false); | ||||
|               return; | ||||
|             } else { | ||||
|               // Cache is outdated, show old data first then update in background | ||||
|               setRates(cachedRates); | ||||
|               setLoading(false); | ||||
|               // Continue to fetch new data below | ||||
|             } | ||||
|           } catch (e) { | ||||
|             console.error("Failed to parse cached rates:", e); | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         // 2. 从网络获取最新数据 | ||||
|         // 2. Fetch latest data from network | ||||
|         setLoading(true); | ||||
|         const response = await fetch("https://api.frankfurter.app/latest?base=USD", { | ||||
|           cache: "no-cache", // 绕过浏览器缓存,确保获取最新数据 | ||||
|           cache: "no-cache", | ||||
|         }); | ||||
|          | ||||
|         if (!response.ok) { | ||||
| @@ -133,34 +132,35 @@ const Tool: FC = () => { | ||||
|          | ||||
|         const data = await response.json(); | ||||
|         const allRates: Record<string, number> = { USD: 1, ...data.rates }; | ||||
|         const apiDate = data.date; // Date from API (YYYY-MM-DD format) | ||||
|          | ||||
|         // 3. 更新状态和缓存 | ||||
|         // 3. Update state and cache | ||||
|         setRates(allRates); | ||||
|         localStorage.setItem( | ||||
|           RATES_CACHE_KEY, | ||||
|           JSON.stringify({ rates: allRates, timestamp: Date.now() }) | ||||
|           JSON.stringify({ rates: allRates, date: apiDate }) | ||||
|         ); | ||||
|       } catch (error) { | ||||
|         // 如果有缓存,即使网络请求失败也继续使用缓存 | ||||
|         // If cache exists, continue using it even if network fails | ||||
|         const cached = localStorage.getItem(RATES_CACHE_KEY); | ||||
|         if (cached) { | ||||
|           try { | ||||
|             const { rates: cachedRates } = JSON.parse(cached); | ||||
|             setRates(cachedRates); | ||||
|             toast.info("使用缓存的汇率数据(网络请求失败)"); | ||||
|             toast.info("Using cached exchange rates (network request failed)"); | ||||
|           } catch (e) { | ||||
|             console.error("Failed to use cached rates:", e); | ||||
|             if (error instanceof Error) { | ||||
|               toast.error(`获取汇率失败: ${error.message}`); | ||||
|               toast.error(`Failed to fetch rates: ${error.message}`); | ||||
|             } else { | ||||
|               toast.error("获取汇率失败"); | ||||
|               toast.error("Failed to fetch rates"); | ||||
|             } | ||||
|           } | ||||
|         } else { | ||||
|           if (error instanceof Error) { | ||||
|             toast.error(`获取汇率失败: ${error.message}`); | ||||
|             toast.error(`Failed to fetch rates: ${error.message}`); | ||||
|           } else { | ||||
|             toast.error("获取汇率失败"); | ||||
|             toast.error("Failed to fetch rates"); | ||||
|           } | ||||
|         } | ||||
|       } finally { | ||||
| @@ -171,13 +171,13 @@ const Tool: FC = () => { | ||||
|     fetchRates(); | ||||
|   }, []); | ||||
|  | ||||
|   // 初始化金额并在汇率加载后计算其他货币 | ||||
|   // Initialize amounts after rates are loaded | ||||
|   useEffect(() => { | ||||
|     if (selectedCurrencies.length > 0 && Object.keys(rates).length > 0) { | ||||
|       if (Object.keys(amounts).length === 0) { | ||||
|         // 首次初始化,以 1 USD 为基准 | ||||
|         // Initial setup: 1 USD as base | ||||
|         const initialAmounts: Record<string, string> = {}; | ||||
|         const baseAmountInUSD = 1; // 1美元 | ||||
|         const baseAmountInUSD = 1; // 1 USD | ||||
|          | ||||
|         selectedCurrencies.forEach(code => { | ||||
|           if (rates[code]) { | ||||
| @@ -192,17 +192,17 @@ const Tool: FC = () => { | ||||
|     } | ||||
|   }, [selectedCurrencies, rates, amounts]); | ||||
|  | ||||
|   // 监听汇率更新,自动重新计算所有金额 | ||||
|   // Auto-update amounts when rates change | ||||
|   useEffect(() => { | ||||
|     if (Object.keys(rates).length > 0 && Object.keys(amounts).length > 0) { | ||||
|       // 找到第一个有值的货币作为基准 | ||||
|       // Find first currency with value as base | ||||
|       const baseCurrency = selectedCurrencies.find(code => amounts[code] && parseFloat(amounts[code]) > 0); | ||||
|        | ||||
|       if (baseCurrency && rates[baseCurrency]) { | ||||
|         const baseValue = parseFloat(amounts[baseCurrency]); | ||||
|         const amountInUSD = baseValue / rates[baseCurrency]; | ||||
|          | ||||
|         // 重新计算所有货币 | ||||
|         // Recalculate all currencies | ||||
|         const updatedAmounts: Record<string, string> = {}; | ||||
|         selectedCurrencies.forEach(code => { | ||||
|           if (rates[code]) { | ||||
| @@ -213,7 +213,7 @@ const Tool: FC = () => { | ||||
|           } | ||||
|         }); | ||||
|          | ||||
|         // 只在值有变化时更新 | ||||
|         // Only update if values changed | ||||
|         const hasChanged = selectedCurrencies.some( | ||||
|           code => updatedAmounts[code] !== amounts[code] | ||||
|         ); | ||||
| @@ -225,9 +225,9 @@ const Tool: FC = () => { | ||||
|     // eslint-disable-next-line react-hooks/exhaustive-deps | ||||
|   }, [rates]); | ||||
|  | ||||
|   // 处理货币金额变化 | ||||
|   // Handle amount input change | ||||
|   const handleAmountChange = (currencyCode: string, value: string) => { | ||||
|     // 只允许数字和小数点 | ||||
|     // Only allow numbers and decimal point | ||||
|     if (value !== "" && !/^\d*\.?\d*$/.test(value)) { | ||||
|       return; | ||||
|     } | ||||
| @@ -237,7 +237,7 @@ const Tool: FC = () => { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // 如果输入为空,清空所有货币 | ||||
|     // Clear all if empty | ||||
|     if (value === "" || value === "0" || parseFloat(value) === 0) { | ||||
|       const newAmounts: Record<string, string> = {}; | ||||
|       selectedCurrencies.forEach(code => { | ||||
| @@ -249,13 +249,13 @@ const Tool: FC = () => { | ||||
|  | ||||
|     const inputValue = parseFloat(value); | ||||
|      | ||||
|     // 计算其他货币的值 | ||||
|     // Calculate other currencies | ||||
|     const newAmounts: Record<string, string> = { [currencyCode]: value }; | ||||
|      | ||||
|     // 先转换为 USD | ||||
|     // Convert to USD first | ||||
|     const amountInUSD = inputValue / rates[currencyCode]; | ||||
|      | ||||
|     // 转换为其他货币 | ||||
|     // Convert to other currencies | ||||
|     selectedCurrencies.forEach(code => { | ||||
|       if (code !== currencyCode && rates[code]) { | ||||
|         const convertedAmount = amountInUSD * rates[code]; | ||||
| @@ -266,12 +266,12 @@ const Tool: FC = () => { | ||||
|     setAmounts(newAmounts); | ||||
|   }; | ||||
|  | ||||
|   // 添加货币 | ||||
|   // Add currency | ||||
|   const addCurrency = (currencyCode: string) => { | ||||
|     if (!selectedCurrencies.includes(currencyCode)) { | ||||
|       setSelectedCurrencies([...selectedCurrencies, currencyCode]); | ||||
|        | ||||
|       // 计算新货币的初始金额 | ||||
|       // Calculate initial amount for new currency | ||||
|       if (rates[currencyCode] && Object.keys(amounts).length > 0) { | ||||
|         const firstCurrency = selectedCurrencies[0]; | ||||
|         if (firstCurrency && amounts[firstCurrency] && rates[firstCurrency]) { | ||||
| @@ -282,31 +282,31 @@ const Tool: FC = () => { | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       toast.success(`已添加 ${currencyCode}`); | ||||
|       toast.success(`Added ${currencyCode}`); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   // 删除货币 | ||||
|   // Remove currency | ||||
|   const removeCurrency = (currencyCode: string) => { | ||||
|     if (REQUIRED_CURRENCIES.includes(currencyCode)) { | ||||
|       toast.error(`${currencyCode} 是必选货币,无法删除`); | ||||
|       toast.error(`${currencyCode} is required and cannot be removed`); | ||||
|       return; | ||||
|     } | ||||
|     if (selectedCurrencies.length <= 2) { | ||||
|       toast.error("至少需要保留两种货币"); | ||||
|       toast.error("At least two currencies are required"); | ||||
|       return; | ||||
|     } | ||||
|     setSelectedCurrencies(selectedCurrencies.filter((c) => c !== currencyCode)); | ||||
|      | ||||
|     // 删除对应的金额 | ||||
|     // Remove corresponding amount | ||||
|     const newAmounts = { ...amounts }; | ||||
|     delete newAmounts[currencyCode]; | ||||
|     setAmounts(newAmounts); | ||||
|      | ||||
|     toast.success(`已删除 ${currencyCode}`); | ||||
|     toast.success(`Removed ${currencyCode}`); | ||||
|   }; | ||||
|  | ||||
|   // 获取货币信息 | ||||
|   // Get currency info | ||||
|   const getCurrency = (code: string): Currency | undefined => { | ||||
|     return AVAILABLE_CURRENCIES.find((c) => c.code === code); | ||||
|   }; | ||||
| @@ -314,23 +314,23 @@ const Tool: FC = () => { | ||||
|   return ( | ||||
|     <div className="flex flex-col gap-4 h-full"> | ||||
|  | ||||
|       {/* 货币选择器 */} | ||||
|       {/* Currency selector */} | ||||
|       <div className="flex flex-col gap-3"> | ||||
|         <div className="flex items-center justify-between"> | ||||
|           <Label>选择货币</Label> | ||||
|           {/* 添加货币按钮 - 固定在右上角 */} | ||||
|           <Label>Select Currencies</Label> | ||||
|           {/* Add currency button - fixed at top right */} | ||||
|           <Popover open={open} onOpenChange={setOpen}> | ||||
|             <PopoverTrigger asChild> | ||||
|               <Button variant="outline" size="sm" className="gap-1"> | ||||
|                 <Plus className="size-4" /> | ||||
|                 添加货币 | ||||
|                 Add Currency | ||||
|               </Button> | ||||
|             </PopoverTrigger> | ||||
|             <PopoverContent className="w-[300px] p-0" align="end" side="bottom" sideOffset={5}> | ||||
|               <Command> | ||||
|                 <CommandInput placeholder="搜索货币..." /> | ||||
|                 <CommandInput placeholder="Search currencies..." /> | ||||
|                 <CommandList> | ||||
|                   <CommandEmpty>未找到货币</CommandEmpty> | ||||
|                   <CommandEmpty>No currency found</CommandEmpty> | ||||
|                   <CommandGroup> | ||||
|                     {AVAILABLE_CURRENCIES.map((currency) => { | ||||
|                       const isSelected = selectedCurrencies.includes(currency.code); | ||||
| @@ -341,7 +341,7 @@ const Tool: FC = () => { | ||||
|                           value={`${currency.code} ${currency.name}`} | ||||
|                           disabled={isRequired} | ||||
|                           onSelect={() => { | ||||
|                             if (isRequired) return; // 必选货币不可操作 | ||||
|                             if (isRequired) return; // Required currencies cannot be toggled | ||||
|                             if (isSelected && selectedCurrencies.length > 2) { | ||||
|                               removeCurrency(currency.code); | ||||
|                             } else if (!isSelected) { | ||||
| @@ -367,7 +367,7 @@ const Tool: FC = () => { | ||||
|           </Popover> | ||||
|         </div> | ||||
|          | ||||
|         {/* 已选货币 Badges */} | ||||
|         {/* Selected currency badges */} | ||||
|         <div className="flex flex-wrap gap-2"> | ||||
|           {selectedCurrencies.map((code) => { | ||||
|             const currency = getCurrency(code); | ||||
| @@ -398,18 +398,18 @@ const Tool: FC = () => { | ||||
|         </div> | ||||
|          | ||||
|         <span className="text-xs text-muted-foreground"> | ||||
|           {REQUIRED_CURRENCIES.map((c) => getCurrency(c)?.code).join("、")} 为必选货币,至少保留两种货币 | ||||
|           {REQUIRED_CURRENCIES.map((c) => getCurrency(c)?.code).join(", ")} are required. At least two currencies needed. | ||||
|         </span> | ||||
|       </div> | ||||
|  | ||||
|       {/* 货币列表 */} | ||||
|       {/* Currency list */} | ||||
|       {loading ? ( | ||||
|         <div className="flex items-center justify-center py-8 text-muted-foreground"> | ||||
|           加载汇率数据中... | ||||
|           Loading exchange rates... | ||||
|         </div> | ||||
|       ) : ( | ||||
|         <div className="flex flex-col gap-3 flex-1 overflow-auto"> | ||||
|           <div className="text-sm font-medium">货币金额:</div> | ||||
|           <div className="text-sm font-medium">Currency Amounts:</div> | ||||
|           <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 auto-rows-min"> | ||||
|             {selectedCurrencies.map((code) => { | ||||
|               const currency = getCurrency(code); | ||||
| @@ -425,7 +425,7 @@ const Tool: FC = () => { | ||||
|                   <CardContent> | ||||
|                     <Input | ||||
|                       type="text" | ||||
|                       placeholder="请输入金额" | ||||
|                       placeholder="Enter amount" | ||||
|                       value={amounts[code] || ""} | ||||
|                       onChange={(e) => handleAmountChange(code, e.target.value)} | ||||
|                       className="text-xl font-semibold" | ||||
| @@ -440,7 +440,16 @@ const Tool: FC = () => { | ||||
|  | ||||
|       {!loading && Object.keys(rates).length > 0 && ( | ||||
|         <div className="text-xs text-muted-foreground"> | ||||
|           汇率数据来源: Frankfurter API (基于欧洲央行数据) | ||||
|           Exchange rate data from:{" "} | ||||
|           <a | ||||
|             href="https://www.frankfurter.app/" | ||||
|             target="_blank" | ||||
|             rel="noopener noreferrer" | ||||
|             className="underline hover:text-foreground transition-colors" | ||||
|           > | ||||
|             Frankfurter API | ||||
|           </a> | ||||
|           {" "}(Based on European Central Bank data) | ||||
|         </div> | ||||
|       )} | ||||
|     </div> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 typist
					typist