| 1 | /** 当前环境对应的货币代码 */ |
| 2 | export const appCurrency: 'USD' = 'USD' |
| 3 | |
| 4 | /** 当前环境对应的货币符号 */ |
| 5 | export const appCurrencySymbol: '$' = '$' |
| 6 | |
| 7 | /** 货币代码到符号的映射(优先查找,覆盖常见货币) */ |
| 8 | const currencySymbolMap: Record<string, string> = { |
| 9 | CNY: '¥', |
| 10 | USD: '$', |
| 11 | EUR: '€', |
| 12 | GBP: '£', |
| 13 | JPY: '¥', |
| 14 | PHP: '₱', |
| 15 | THB: '฿', |
| 16 | KRW: '₩', |
| 17 | VND: '₫', |
| 18 | INR: '₹', |
| 19 | IDR: 'Rp', |
| 20 | MYR: 'RM', |
| 21 | SGD: 'S$', |
| 22 | HKD: 'HK$', |
| 23 | TWD: 'NT$', |
| 24 | AUD: 'A$', |
| 25 | CAD: 'C$', |
| 26 | NZD: 'NZ$', |
| 27 | BRL: 'R$', |
| 28 | MXN: 'MX$', |
| 29 | RUB: '₽', |
| 30 | TRY: '₺', |
| 31 | ZAR: 'R', |
| 32 | AED: 'د.إ', |
| 33 | SAR: '﷼', |
| 34 | NGN: '₦', |
| 35 | EGP: 'E£', |
| 36 | PKR: '₨', |
| 37 | BDT: '৳', |
| 38 | ARS: 'AR$', |
| 39 | } |
| 40 | |
| 41 | /** Intl.NumberFormat 兜底缓存(处理 Map 中未覆盖的货币) */ |
| 42 | const intlSymbolCache = new Map<string, string>() |
| 43 | |
| 44 | /** 根据货币代码获取对应符号,优先硬编码 Map,未命中走 Intl.NumberFormat 兜底 */ |
| 45 | export function getCurrencySymbol(currency?: string): string { |
| 46 | if (!currency) |
| 47 | return appCurrencySymbol |
| 48 | if (currencySymbolMap[currency]) |
| 49 | return currencySymbolMap[currency] |
| 50 | const cached = intlSymbolCache.get(currency) |
| 51 | if (cached) |
| 52 | return cached |
| 53 | try { |
| 54 | const symbol = new Intl.NumberFormat('en', { |
| 55 | style: 'currency', |
| 56 | currency, |
| 57 | currencyDisplay: 'narrowSymbol', |
| 58 | }) |
| 59 | .formatToParts(0) |
| 60 | .find(p => p.type === 'currency') |
| 61 | ?.value ?? currency |
| 62 | intlSymbolCache.set(currency, symbol) |
| 63 | return symbol |
| 64 | } |
| 65 | catch { |
| 66 | return currency |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | /** 分转元,格式化为两位小数 */ |
| 71 | export function formatCents(cents: number | null | undefined): string { |
| 72 | if (cents == null || Number.isNaN(cents)) |
| 73 | return '0.00' |
| 74 | return (cents / 100).toFixed(2) |
| 75 | } |
| 76 | |
| 77 | /** 货币展示结构化数据 */ |
| 78 | export interface CurrencyDisplay { |
| 79 | /** 货币符号,如 '₱' */ |
| 80 | symbol: string |
| 81 | /** 格式化后的金额,如 '100.00' */ |
| 82 | amount: string |
| 83 | /** 货币代码,如 'PHP' */ |
| 84 | code: string |
| 85 | /** 完整文本,如 '₱100.00 PHP'(纯文本场景用) */ |
| 86 | text: string |
| 87 | } |
| 88 | |
| 89 | /** |
| 90 | * 生成货币展示结构化数据 |
| 91 | * @param value 金额值 |
| 92 | * @param currencyCode 货币代码,默认 appCurrency |
| 93 | * @param isCents 是否为分(默认 true,会除以 100;false 则直接使用原值) |
| 94 | */ |
| 95 | export function formatCurrencyDisplay( |
| 96 | value: number | string | null | undefined, |
| 97 | currencyCode?: string, |
| 98 | isCents: boolean = true, |
| 99 | ): CurrencyDisplay { |
| 100 | const code = currencyCode || appCurrency |
| 101 | const symbol = getCurrencySymbol(code) |
| 102 | const amount = isCents |
| 103 | ? formatCents(typeof value === 'string' ? Number(value) : value) |
| 104 | : String(value ?? '0.00') |
| 105 | return { |
| 106 | symbol, |
| 107 | amount, |
| 108 | code, |
| 109 | text: `${symbol}${amount} ${code}`, |
| 110 | } |
| 111 | } |
| 112 |