137 lines
5.3 KiB
TypeScript
137 lines
5.3 KiB
TypeScript
"use client";
|
||
|
||
import { useMemo, useState } from "react";
|
||
|
||
const CURRENCIES = [
|
||
{ code: "CNY", name: "人民币", flag: "🇨🇳", rate: 1 },
|
||
{ code: "USD", name: "美元", flag: "🇺🇸", rate: 0.138 },
|
||
{ code: "EUR", name: "欧元", flag: "🇪🇺", rate: 0.127 },
|
||
{ code: "THB", name: "泰铢", flag: "🇹🇭", rate: 4.85 },
|
||
{ code: "IDR", name: "印尼盾", flag: "🇮🇩", rate: 2180 },
|
||
{ code: "JPY", name: "日元", flag: "🇯🇵", rate: 20.5 },
|
||
{ code: "MXN", name: "墨西哥比索", flag: "🇲🇽", rate: 2.35 },
|
||
{ code: "GBP", name: "英镑", flag: "🇬🇧", rate: 0.109 },
|
||
];
|
||
|
||
const PRESETS = [
|
||
{ label: "清迈月租", amount: 2500, from: "CNY", emoji: "🏔️" },
|
||
{ label: "巴厘岛生活费", amount: 4500, from: "CNY", emoji: "🏝️" },
|
||
{ label: "里斯本月租", amount: 800, from: "EUR", emoji: "🌊" },
|
||
{ label: "东京月租", amount: 120000, from: "JPY", emoji: "🗼" },
|
||
];
|
||
|
||
export default function CurrencyConverter() {
|
||
const [amount, setAmount] = useState(5000);
|
||
const [from, setFrom] = useState("CNY");
|
||
const [to, setTo] = useState("USD");
|
||
const [swapping, setSwapping] = useState(false);
|
||
|
||
const result = useMemo(() => {
|
||
const fromRate = CURRENCIES.find((c) => c.code === from)?.rate ?? 1;
|
||
const toRate = CURRENCIES.find((c) => c.code === to)?.rate ?? 1;
|
||
return (amount / fromRate) * toRate;
|
||
}, [amount, from, to]);
|
||
|
||
const swap = () => {
|
||
setSwapping(true);
|
||
setFrom(to);
|
||
setTo(from);
|
||
setTimeout(() => setSwapping(false), 400);
|
||
};
|
||
|
||
const applyPreset = (p: typeof PRESETS[0]) => {
|
||
setAmount(p.amount);
|
||
setFrom(p.from);
|
||
setTo("CNY");
|
||
};
|
||
|
||
const fromCur = CURRENCIES.find((c) => c.code === from)!;
|
||
const toCur = CURRENCIES.find((c) => c.code === to)!;
|
||
|
||
return (
|
||
<section className="section currency-section" id="currency">
|
||
<div className="container">
|
||
<div className="section-header reveal">
|
||
<span className="section-tag">💱 CURRENCY</span>
|
||
<h2>多币种换算器</h2>
|
||
<p>快速换算旅居生活费,规划跨国预算更轻松</p>
|
||
</div>
|
||
|
||
<div className="currency-wrapper reveal">
|
||
<div className="currency-card">
|
||
<div className="currency-input-group">
|
||
<label>💰 金额</label>
|
||
<div className="currency-input-row">
|
||
<input
|
||
type="number"
|
||
value={amount}
|
||
onChange={(e) => setAmount(Math.max(0, +e.target.value || 0))}
|
||
min={0}
|
||
/>
|
||
<select value={from} onChange={(e) => setFrom(e.target.value)}>
|
||
{CURRENCIES.map((c) => (
|
||
<option key={c.code} value={c.code}>{c.flag} {c.code}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<button className={`currency-swap${swapping ? " swapping" : ""}`} onClick={swap} aria-label="交换货币">
|
||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||
<path d="M7 16V4M7 4L3 8M7 4l4 4M17 8v12M17 20l4-4M17 20l-4-4" />
|
||
</svg>
|
||
</button>
|
||
|
||
<div className="currency-result">
|
||
<label>✨ 换算结果</label>
|
||
<div className="currency-result-display">
|
||
<span className="currency-result-flag">{toCur.flag}</span>
|
||
<span className="currency-result-amount">
|
||
{result.toLocaleString(undefined, { maximumFractionDigits: 2 })}
|
||
</span>
|
||
<span className="currency-result-code">{toCur.code}</span>
|
||
</div>
|
||
<p className="currency-rate-hint">
|
||
1 {fromCur.code} ≈ {((fromCur.rate / toCur.rate)).toLocaleString(undefined, { maximumFractionDigits: 4 })} {toCur.code}
|
||
</p>
|
||
</div>
|
||
|
||
<div className="currency-to-select">
|
||
<label>兑换为</label>
|
||
<div className="currency-chips">
|
||
{CURRENCIES.filter((c) => c.code !== from).map((c) => (
|
||
<button
|
||
key={c.code}
|
||
className={`currency-chip${to === c.code ? " active" : ""}`}
|
||
onClick={() => setTo(c.code)}
|
||
>
|
||
{c.flag} {c.code}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="currency-presets">
|
||
<h3>🏙️ 快捷参考</h3>
|
||
<p className="currency-presets-desc">一键填入典型旅居开销</p>
|
||
<div className="currency-preset-grid">
|
||
{PRESETS.map((p) => (
|
||
<button key={p.label} className="currency-preset-card" onClick={() => applyPreset(p)}>
|
||
<span className="preset-emoji">{p.emoji}</span>
|
||
<strong>{p.label}</strong>
|
||
<span>{p.amount.toLocaleString()} {p.from}</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
<div className="currency-tip">
|
||
<span>💡</span>
|
||
<p>实际汇率会波动,建议用 Wise 或 Revolut 获取实时汇率。此处为参考汇率。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|