Files
lotteryFront/src/lib/money.ts
kang 99d49f457f
Some checks failed
lotteryfront CI / build (push) Has been cancelled
feat(hall): 支持按行选择开注商与选号种类并优化下注表格
2026-07-10 17:04:54 +08:00

121 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 与后端约定:金额存最小货币单位(如 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;
export function getCurrencyDecimalPlaces(currencyCode: string): number {
const code = currencyCode.trim().toUpperCase();
const row = usePlayerSessionStore
.getState()
.currencies.find((item) => item.code === code);
const decimals = row?.decimal_places;
if (typeof decimals === "number" && Number.isFinite(decimals) && decimals >= 0) {
return decimals;
}
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 formatMinorAmount(
minor: number | string,
decimalPlaces?: number,
): string {
const n = typeof minor === "string" ? Number(minor) : minor;
if (!Number.isFinite(n)) return "—";
const format = getCurrencyDisplayFormat();
const resolvedDisplayDecimals =
typeof decimalPlaces === "number" &&
Number.isFinite(decimalPlaces) &&
decimalPlaces >= 0
? Math.min(12, Math.trunc(decimalPlaces))
: format.displayDecimals;
return formatMinorWithDisplaySettings(n, {
...format,
displayDecimals: resolvedDisplayDecimals,
});
}
export function formatMinorAsCurrency(
minor: number | string,
currencyCode: string,
decimalPlaces?: number,
): string {
const amount = formatMinorAmount(minor, decimalPlaces);
return `${currencyCode} ${amount}`;
}
/**
* 用户输入如 `1000` 或 `1,000.50` → 最小货币单位整数(按币种实际 decimal_places
*/
export function parseDecimalInputToMinor(
raw: string,
decimalPlacesOrCurrencyCode?: number | string,
): number | null {
const decimalPlaces =
typeof decimalPlacesOrCurrencyCode === "string"
? getCurrencyDecimalPlaces(decimalPlacesOrCurrencyCode)
: typeof decimalPlacesOrCurrencyCode === "number" &&
Number.isFinite(decimalPlacesOrCurrencyCode) &&
decimalPlacesOrCurrencyCode >= 0
? decimalPlacesOrCurrencyCode
: decimalPlacesOrCurrencyCode;
const resolvedDecimalPlaces = decimalPlaces ?? DEFAULT_DECIMAL_PLACES;
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;
const factor = 10 ** resolvedDecimalPlaces;
const minor = Math.round(n * factor);
if (!Number.isSafeInteger(minor)) return null;
return minor;
}