Files
gao-customer-h5/src/utils/package.ts
T

146 lines
6.0 KiB
TypeScript

import type { PackageConfigStatus } from "@/types/Package";
import type { FundOperationType, FundRecordStatus } from "@/types/SalesOrder";
import { formatCentsAmount } from "@/utils/money";
/** 生成套餐业务幂等请求号。每次新操作必须重新生成。 */
export function createPackageRequestNo(): string {
return window.crypto.randomUUID();
}
/** 将分转换为金额展示文本,不用浮点数做金额格式化。 */
export function formatPackageAmount(cents?: number | null): string {
return formatCentsAmount(cents);
}
/** 将分转换为金额输入框文本。 */
export function formatPackageAmountInput(cents?: number | null): string {
if (cents === undefined || cents === null) {
return "";
}
const negative = cents < 0;
const absolute = Math.abs(Math.trunc(cents));
return `${negative ? "-" : ""}${Math.floor(absolute / 100)}.${String(absolute % 100).padStart(2, "0")}`;
}
/** 将用户输入的元金额精确转换为后端要求的分。 */
export function parsePackageAmount(value: string): number | undefined {
const text = value.trim();
if (!/^\d+(?:\.\d{1,2})?$/.test(text)) {
return undefined;
}
const [yuanText, fenText = ""] = text.split(".");
const cents = BigInt(yuanText) * 100n + BigInt(fenText.padEnd(2, "0") || "0");
const result = Number(cents);
return Number.isSafeInteger(result) ? result : undefined;
}
/** 规范化最多三位小数的非负定点数量。 */
export function normalizePackageQuantity(value: number | string): string | undefined {
const text = String(value).trim();
if (!/^\d+(?:\.\d{1,3})?$/.test(text)) {
return undefined;
}
const [integerText, fractionText = ""] = text.split(".");
const integer = integerText.replace(/^0+(?=\d)/, "") || "0";
const fraction = fractionText.replace(/0+$/, "");
return fraction ? `${integer}.${fraction}` : integer;
}
/** 使用 BigInt 比较定点数量,避免 JavaScript 二进制浮点误差。 */
export function comparePackageQuantity(left: number | string, right: number | string): number {
const leftParts = toQuantityParts(left);
const rightParts = toQuantityParts(right);
if (!leftParts || !rightParts) {
return 0;
}
const scale = Math.max(leftParts.scale, rightParts.scale);
const leftValue = leftParts.value * 10n ** BigInt(scale - leftParts.scale);
const rightValue = rightParts.value * 10n ** BigInt(scale - rightParts.scale);
return leftValue < rightValue ? -1 : rightValue < leftValue ? 1 : 0;
}
/** 将当前单位数量精确换算为最多三位小数的基础单位数量。 */
export function toPackageBaseQuantity(quantity: number | string, baseQuantityPerUnit: number | string): string | undefined {
const quantityParts = toDecimalParts(quantity, 3);
const unitParts = toDecimalParts(baseQuantityPerUnit, 6);
if (!quantityParts || !unitParts) {
return undefined;
}
return formatDecimal(quantityParts.value * unitParts.value, quantityParts.scale + unitParts.scale, 3);
}
/** 将数量从一个单位精确换算到另一个单位,结果最多保留三位小数。 */
export function convertPackageQuantity(quantity: number | string, fromBaseQuantityPerUnit: number | string, toBaseQuantityPerUnit: number | string): string | undefined {
const quantityParts = toDecimalParts(quantity, 3);
const fromParts = toDecimalParts(fromBaseQuantityPerUnit, 6);
const toParts = toDecimalParts(toBaseQuantityPerUnit, 6);
if (!quantityParts || !fromParts || !toParts || toParts.value === 0n) {
return undefined;
}
const numerator = quantityParts.value * fromParts.value * 10n ** BigInt(toParts.scale + 3);
const denominator = 10n ** BigInt(quantityParts.scale + fromParts.scale) * toParts.value;
if (numerator % denominator !== 0n) {
return undefined;
}
return formatDecimal(numerator / denominator, 3, 3);
}
/** 精确相加两个最多三位小数的数量。 */
export function addPackageQuantity(left: number | string, right: number | string): string | undefined {
const leftParts = toQuantityParts(left);
const rightParts = toQuantityParts(right);
if (!leftParts || !rightParts) {
return undefined;
}
const scale = Math.max(leftParts.scale, rightParts.scale);
const value = leftParts.value * 10n ** BigInt(scale - leftParts.scale) + rightParts.value * 10n ** BigInt(scale - rightParts.scale);
return formatDecimal(value, scale, 3);
}
export function getPackageQuantityText(value?: number | string | null): string {
if (value === undefined || value === null || value === "") {
return "0";
}
return normalizePackageQuantity(value) || "0";
}
export function getPackageConfigStatusText(status?: PackageConfigStatus): string {
return ({ DRAFT: "草稿", ACTIVE: "启用", INACTIVE: "停用" } as Record<string, string>)[status || ""] || "未知";
}
export function getFundOperationText(type?: FundOperationType): string {
return ({ RECEIVE: "线下收款", REFUND: "退款", CONFIRM_NO_REFUND: "确认无需退款", REVOKE: "资金冲正" } as Record<string, string>)[type || ""] || "未知";
}
export function getFundStatusText(status?: FundRecordStatus): string {
return ({ PENDING: "待核销", VERIFIED: "已核销", REJECTED: "已拒绝", CANCELLED: "已撤销" } as Record<string, string>)[status || ""] || "未知";
}
function toQuantityParts(value: number | string): { value: bigint; scale: number } | undefined {
return toDecimalParts(value, 3);
}
function toDecimalParts(value: number | string, maxScale: number): { value: bigint; scale: number } | undefined {
const text = String(value).trim();
if (!new RegExp(`^\\d+(?:\\.\\d{1,${maxScale}})?$`).test(text)) {
return undefined;
}
const [integerText, fractionText = ""] = text.split(".");
return {
value: BigInt(`${integerText}${fractionText}`),
scale: fractionText.length
};
}
function formatDecimal(value: bigint, scale: number, maxScale: number): string | undefined {
while (maxScale < scale && value % 10n === 0n) {
value /= 10n;
scale--;
}
if (maxScale < scale) {
return undefined;
}
const text = value.toString().padStart(scale + 1, "0");
return normalizePackageQuantity(scale ? `${text.slice(0, -scale)}.${text.slice(-scale)}` : text);
}