feat: 增强 iframe 通信机制与通知处理功能

实现 resolvePostMessageTargetOrigin,优化 iframe 消息通信中的目标来源(origin)解析与校验。
更新 IframeBridge:支持定期刷新允许的来源列表,并优化消息事件管理机制。
重构 usePendingWalletReconcile:优化待对账通知的获取与缓存逻辑,提升性能与用户体验。
增强 NotificationsScreen:新增待对账通知内容,并优化界面展示效果。
更新英文、尼泊尔语与中文语言包,新增待对账通知相关翻译文案。
This commit is contained in:
2026-06-01 13:38:30 +08:00
parent aeaba5eea3
commit b819894e75
17 changed files with 362 additions and 72 deletions

View File

@@ -1,7 +1,9 @@
/**
* 与后端约定:金额存最小货币单位(如 NPR 2 位小数 → 分);展示时除以 10^decimals。
* 与后端约定:金额存最小货币单位(如 NPR 2 位小数 → 分);展示时除以 10^displayDecimals。
* 展示分隔符与 {@link CurrencyFormatter} / 后台「货币格式」设置一致。
*/
import { getCurrencyDisplayFormat } from "@/lib/currency-display-settings";
import { usePlayerSessionStore } from "@/stores/player-session-store";
const DEFAULT_DECIMAL_PLACES = 2;
@@ -20,27 +22,62 @@ export function getCurrencyDecimalPlaces(currencyCode: string): number {
return DEFAULT_DECIMAL_PLACES;
}
function formatIntegerWithThousands(value: number, thousandsSep: string): string {
const digits = String(Math.trunc(value));
if (!thousandsSep) return digits;
return digits.replace(/\B(?=(\d{3})+(?!\d))/g, thousandsSep);
}
function formatMinorWithDisplaySettings(
minorUnits: number,
format = getCurrencyDisplayFormat(),
): string {
const { displayDecimals, decimalSeparator, thousandsSeparator } = format;
const divisor = Math.max(1, 10 ** displayDecimals);
const negative = minorUnits < 0;
const abs = Math.abs(minorUnits);
const integerPart = Math.floor(abs / divisor);
const fractionRaw = abs % divisor;
const fractionPadded = String(fractionRaw).padStart(displayDecimals, "0");
const integerPartFormatted = formatIntegerWithThousands(
integerPart,
thousandsSeparator,
);
if (displayDecimals === 0) {
return `${negative ? "-" : ""}${integerPartFormatted}`;
}
return `${negative ? "-" : ""}${integerPartFormatted}${decimalSeparator}${fractionPadded}`;
}
export function formatMinorAsCurrency(
minor: number | string,
currencyCode: string,
decimalPlaces?: number,
): string {
const resolvedDecimalPlaces =
typeof decimalPlaces === "number" && Number.isFinite(decimalPlaces) && decimalPlaces >= 0
? decimalPlaces
: getCurrencyDecimalPlaces(currencyCode);
const n = typeof minor === "string" ? Number(minor) : minor;
if (!Number.isFinite(n)) return `${currencyCode}`;
const divisor = 10 ** resolvedDecimalPlaces;
const major = n / divisor;
return `${currencyCode} ${major.toLocaleString(undefined, {
minimumFractionDigits: resolvedDecimalPlaces,
maximumFractionDigits: resolvedDecimalPlaces,
})}`;
const format = getCurrencyDisplayFormat();
const resolvedDisplayDecimals =
typeof decimalPlaces === "number" &&
Number.isFinite(decimalPlaces) &&
decimalPlaces >= 0
? Math.min(12, Math.trunc(decimalPlaces))
: format.displayDecimals;
const amount = formatMinorWithDisplaySettings(n, {
...format,
displayDecimals: resolvedDisplayDecimals,
});
return `${currencyCode} ${amount}`;
}
/**
* 用户输入如 `1000` 或 `1000.5` → 最小货币单位整数。
* 用户输入如 `1000` 或 `1,000.50` → 最小货币单位整数(按币种实际 decimal_places
*/
export function parseDecimalInputToMinor(
raw: string,
@@ -53,9 +90,19 @@ export function parseDecimalInputToMinor(
Number.isFinite(decimalPlacesOrCurrencyCode) &&
decimalPlacesOrCurrencyCode >= 0
? decimalPlacesOrCurrencyCode
: decimalPlacesOrCurrencyCode;
: decimalPlacesOrCurrencyCode;
const resolvedDecimalPlaces = decimalPlaces ?? DEFAULT_DECIMAL_PLACES;
const cleaned = raw.replace(/,/g, "").trim();
const format = getCurrencyDisplayFormat();
let cleaned = raw.trim();
if (format.thousandsSeparator) {
cleaned = cleaned.split(format.thousandsSeparator).join("");
}
if (format.decimalSeparator && format.decimalSeparator !== ".") {
cleaned = cleaned.replaceAll(format.decimalSeparator, ".");
}
cleaned = cleaned.replace(/,/g, "");
if (cleaned === "") return null;
const n = Number(cleaned);
if (!Number.isFinite(n) || n < 0) return null;