|
@@ -0,0 +1,361 @@
|
|
|
+/**
|
|
|
+ * @fileoverview 订单表单数字格式化工具函数
|
|
|
+ * @description 提供统一的数字格式化功能,确保4位浮点型数据的精确处理
|
|
|
+ */
|
|
|
+
|
|
|
+/**
|
|
|
+ * 数字格式化配置选项
|
|
|
+ * @typedef {Object} NumberFormatOptions
|
|
|
+ * @property {number} minimumFractionDigits - 最小小数位数
|
|
|
+ * @property {number} maximumFractionDigits - 最大小数位数
|
|
|
+ * @property {boolean} useGrouping - 是否使用千分位分隔符
|
|
|
+ */
|
|
|
+
|
|
|
+/**
|
|
|
+ * 数字验证结果
|
|
|
+ * @typedef {Object} NumberValidationResult
|
|
|
+ * @property {boolean} isValid - 是否为有效数字
|
|
|
+ * @property {number} value - 转换后的数字值
|
|
|
+ * @property {string} error - 错误信息(如果有)
|
|
|
+ */
|
|
|
+
|
|
|
+/**
|
|
|
+ * 格式化配置常量
|
|
|
+ * @description 定义不同类型数字的格式化规则
|
|
|
+ * @readonly
|
|
|
+ */
|
|
|
+export const NUMBER_FORMAT_CONFIG = Object.freeze({
|
|
|
+ /** 4位浮点型数量格式化选项 */
|
|
|
+ QUANTITY_FLOAT: {
|
|
|
+ minimumFractionDigits: 0,
|
|
|
+ maximumFractionDigits: 4,
|
|
|
+ useGrouping: false
|
|
|
+ },
|
|
|
+ /** 整数数量格式化选项 */
|
|
|
+ QUANTITY_INTEGER: {
|
|
|
+ minimumFractionDigits: 0,
|
|
|
+ maximumFractionDigits: 0,
|
|
|
+ useGrouping: false
|
|
|
+ },
|
|
|
+ /** 金额格式化选项(2位小数) */
|
|
|
+ AMOUNT: {
|
|
|
+ minimumFractionDigits: 2,
|
|
|
+ maximumFractionDigits: 2,
|
|
|
+ useGrouping: true
|
|
|
+ },
|
|
|
+ /** 税率格式化选项(2位小数) */
|
|
|
+ TAX_RATE: {
|
|
|
+ minimumFractionDigits: 0,
|
|
|
+ maximumFractionDigits: 2,
|
|
|
+ useGrouping: false
|
|
|
+ },
|
|
|
+ /** 单价格式化选项(2位小数) */
|
|
|
+ UNIT_PRICE: {
|
|
|
+ minimumFractionDigits: 0,
|
|
|
+ maximumFractionDigits: 2,
|
|
|
+ useGrouping: false
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+/**
|
|
|
+ * 数字类型枚举
|
|
|
+ * @description 定义支持的数字类型
|
|
|
+ * @readonly
|
|
|
+ */
|
|
|
+export const NUMBER_TYPES = Object.freeze({
|
|
|
+ /** 4位浮点型数量 */
|
|
|
+ QUANTITY_FLOAT: 'QUANTITY_FLOAT',
|
|
|
+ /** 整数数量 */
|
|
|
+ QUANTITY_INTEGER: 'QUANTITY_INTEGER',
|
|
|
+ /** 金额 */
|
|
|
+ AMOUNT: 'AMOUNT',
|
|
|
+ /** 税率 */
|
|
|
+ TAX_RATE: 'TAX_RATE',
|
|
|
+ /** 单价 */
|
|
|
+ UNIT_PRICE: 'UNIT_PRICE'
|
|
|
+})
|
|
|
+
|
|
|
+/**
|
|
|
+ * 验证并转换数字值
|
|
|
+ * @description 验证输入值是否为有效数字,并进行安全转换
|
|
|
+ * @param {*} value - 待验证的值
|
|
|
+ * @returns {NumberValidationResult} 验证结果
|
|
|
+ */
|
|
|
+export function validateNumber(value) {
|
|
|
+ // 处理null、undefined和空字符串
|
|
|
+ if (value === null || value === undefined || value === '') {
|
|
|
+ return {
|
|
|
+ isValid: true,
|
|
|
+ value: 0,
|
|
|
+ error: null
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 转换为数字
|
|
|
+ const numValue = Number(value)
|
|
|
+
|
|
|
+ // 检查是否为有效数字
|
|
|
+ if (isNaN(numValue) || !isFinite(numValue)) {
|
|
|
+ return {
|
|
|
+ isValid: false,
|
|
|
+ value: 0,
|
|
|
+ error: '无效的数字格式'
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ isValid: true,
|
|
|
+ value: numValue,
|
|
|
+ error: null
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 格式化4位浮点型数字
|
|
|
+ * @description 将数字格式化为4位小数的字符串,自动去除尾随零
|
|
|
+ * @param {number|string|null|undefined} value - 数字值
|
|
|
+ * @returns {string} 格式化后的数字字符串
|
|
|
+ * @example
|
|
|
+ * formatFloatNumber(123.45670000) // 返回 "123.4567"
|
|
|
+ * formatFloatNumber(100.0000) // 返回 "100"
|
|
|
+ * formatFloatNumber(null) // 返回 "0"
|
|
|
+ */
|
|
|
+export function formatFloatNumber(value) {
|
|
|
+ const validation = validateNumber(value)
|
|
|
+ if (!validation.isValid) {
|
|
|
+ return '0'
|
|
|
+ }
|
|
|
+
|
|
|
+ const numValue = validation.value
|
|
|
+
|
|
|
+ // 使用4位小数精度格式化
|
|
|
+ const formatted = numValue.toLocaleString('zh-CN', NUMBER_FORMAT_CONFIG.QUANTITY_FLOAT)
|
|
|
+
|
|
|
+ // 去除尾随零和不必要的小数点
|
|
|
+ return formatted.replace(/\.?0+$/, '')
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 格式化整数
|
|
|
+ * @description 将数字格式化为整数字符串
|
|
|
+ * @param {number|string|null|undefined} value - 数字值
|
|
|
+ * @returns {string} 格式化后的整数字符串
|
|
|
+ * @example
|
|
|
+ * formatIntegerNumber(123.456) // 返回 "123"
|
|
|
+ * formatIntegerNumber(null) // 返回 "0"
|
|
|
+ */
|
|
|
+export function formatIntegerNumber(value) {
|
|
|
+ const validation = validateNumber(value)
|
|
|
+ if (!validation.isValid) {
|
|
|
+ return '0'
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log(value, Math.round(validation.value).toLocaleString('zh-CN', NUMBER_FORMAT_CONFIG.QUANTITY_INTEGER))
|
|
|
+ return Math.round(validation.value).toLocaleString('zh-CN', NUMBER_FORMAT_CONFIG.QUANTITY_INTEGER)
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 格式化金额
|
|
|
+ * @description 将数字格式化为金额字符串,保留2位小数并添加千分位分隔符
|
|
|
+ * @param {number|string|null|undefined} value - 金额值
|
|
|
+ * @param {boolean} [withSymbol=true] - 是否添加货币符号
|
|
|
+ * @returns {string} 格式化后的金额字符串
|
|
|
+ * @example
|
|
|
+ * formatAmount(1234.567) // 返回 "¥1,234.57"
|
|
|
+ * formatAmount(1234.567, false) // 返回 "1,234.57"
|
|
|
+ * formatAmount(null) // 返回 "¥0.00"
|
|
|
+ */
|
|
|
+export function formatAmount(value, withSymbol = true) {
|
|
|
+ const validation = validateNumber(value)
|
|
|
+ if (!validation.isValid) {
|
|
|
+ return withSymbol ? '¥0.00' : '0.00'
|
|
|
+ }
|
|
|
+
|
|
|
+ const formatted = validation.value.toLocaleString('zh-CN', NUMBER_FORMAT_CONFIG.AMOUNT)
|
|
|
+ return withSymbol ? `¥${formatted}` : formatted
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 格式化税率
|
|
|
+ * @description 将数字格式化为税率字符串,最多保留2位小数
|
|
|
+ * @param {number|string|null|undefined} value - 税率值(0-100)
|
|
|
+ * @param {boolean} [withSymbol=true] - 是否添加百分号
|
|
|
+ * @returns {string} 格式化后的税率字符串
|
|
|
+ * @example
|
|
|
+ * formatTaxRate(13.5) // 返回 "13.5%"
|
|
|
+ * formatTaxRate(13.50, false) // 返回 "13.5"
|
|
|
+ * formatTaxRate(null) // 返回 "0%"
|
|
|
+ */
|
|
|
+export function formatTaxRate(value, withSymbol = true) {
|
|
|
+ const validation = validateNumber(value)
|
|
|
+ if (!validation.isValid) {
|
|
|
+ return withSymbol ? '0%' : '0'
|
|
|
+ }
|
|
|
+
|
|
|
+ const formatted = validation.value.toLocaleString('zh-CN', NUMBER_FORMAT_CONFIG.TAX_RATE)
|
|
|
+ return withSymbol ? `${formatted}%` : formatted
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 格式化单价
|
|
|
+ * @description 将数字格式化为单价字符串,最多保留4位小数
|
|
|
+ * @param {number|string|null|undefined} value - 单价值
|
|
|
+ * @returns {string} 格式化后的单价字符串
|
|
|
+ * @example
|
|
|
+ * formatUnitPrice(123.45670000) // 返回 "123.4567"
|
|
|
+ * formatUnitPrice(100.0000) // 返回 "100"
|
|
|
+ * formatUnitPrice(null) // 返回 "0"
|
|
|
+ */
|
|
|
+export function formatUnitPrice(value) {
|
|
|
+ const validation = validateNumber(value)
|
|
|
+ if (!validation.isValid) {
|
|
|
+ return '0.00'
|
|
|
+ }
|
|
|
+
|
|
|
+ const formatted = validation.value.toLocaleString('zh-CN', {
|
|
|
+ minimumFractionDigits: 2,
|
|
|
+ maximumFractionDigits: 2,
|
|
|
+ useGrouping: false
|
|
|
+ })
|
|
|
+
|
|
|
+ return formatted
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 通用数字格式化函数
|
|
|
+ * @description 根据指定类型格式化数字
|
|
|
+ * @param {number|string|null|undefined} value - 数字值
|
|
|
+ * @param {keyof typeof NUMBER_TYPES} type - 数字类型
|
|
|
+ * @param {Object} [options={}] - 额外选项
|
|
|
+ * @param {boolean} [options.withSymbol] - 是否添加符号(适用于金额和税率)
|
|
|
+ * @returns {string} 格式化后的数字字符串
|
|
|
+ * @example
|
|
|
+ * formatNumber(123.4567, NUMBER_TYPES.QUANTITY_FLOAT) // 返回 "123.4567"
|
|
|
+ * formatNumber(1234.56, NUMBER_TYPES.AMOUNT) // 返回 "¥1,234.56"
|
|
|
+ * formatNumber(13.5, NUMBER_TYPES.TAX_RATE) // 返回 "13.5%"
|
|
|
+ */
|
|
|
+export function formatNumber(value, type, options = {}) {
|
|
|
+ const { withSymbol = true } = options
|
|
|
+
|
|
|
+ switch (type) {
|
|
|
+ case NUMBER_TYPES.QUANTITY_FLOAT:
|
|
|
+ return formatFloatNumber(value)
|
|
|
+ case NUMBER_TYPES.QUANTITY_INTEGER:
|
|
|
+ return formatIntegerNumber(value)
|
|
|
+ case NUMBER_TYPES.AMOUNT:
|
|
|
+ return formatAmount(value, withSymbol)
|
|
|
+ case NUMBER_TYPES.TAX_RATE:
|
|
|
+ return formatTaxRate(value, withSymbol)
|
|
|
+ case NUMBER_TYPES.UNIT_PRICE:
|
|
|
+ return formatUnitPrice(value)
|
|
|
+ default:
|
|
|
+ return formatFloatNumber(value)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 解析格式化的数字字符串
|
|
|
+ * @description 将格式化的数字字符串解析为数字值
|
|
|
+ * @param {string} formattedValue - 格式化的数字字符串
|
|
|
+ * @returns {NumberValidationResult} 解析结果
|
|
|
+ * @example
|
|
|
+ * parseFormattedNumber("1,234.56") // 返回 { isValid: true, value: 1234.56, error: null }
|
|
|
+ * parseFormattedNumber("¥1,234.56") // 返回 { isValid: true, value: 1234.56, error: null }
|
|
|
+ * parseFormattedNumber("13.5%") // 返回 { isValid: true, value: 13.5, error: null }
|
|
|
+ */
|
|
|
+export function parseFormattedNumber(formattedValue) {
|
|
|
+ if (typeof formattedValue !== 'string') {
|
|
|
+ return validateNumber(formattedValue)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 移除货币符号、百分号、千分位分隔符等
|
|
|
+ const cleanValue = formattedValue
|
|
|
+ .replace(/[¥$€£%,\s]/g, '')
|
|
|
+ .trim()
|
|
|
+
|
|
|
+ return validateNumber(cleanValue)
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 精确计算两个数字的乘积
|
|
|
+ * @description 避免浮点数计算精度问题
|
|
|
+ * @param {number|string} num1 - 第一个数字
|
|
|
+ * @param {number|string} num2 - 第二个数字
|
|
|
+ * @returns {number} 计算结果
|
|
|
+ * @example
|
|
|
+ * preciseMultiply(0.1, 0.2) // 返回 0.02(而不是 0.020000000000000004)
|
|
|
+ */
|
|
|
+export function preciseMultiply(num1, num2) {
|
|
|
+ const validation1 = validateNumber(num1)
|
|
|
+ const validation2 = validateNumber(num2)
|
|
|
+
|
|
|
+ if (!validation1.isValid || !validation2.isValid) {
|
|
|
+ return 0
|
|
|
+ }
|
|
|
+
|
|
|
+ const value1 = validation1.value
|
|
|
+ const value2 = validation2.value
|
|
|
+
|
|
|
+ // 获取小数位数
|
|
|
+ const decimals1 = (value1.toString().split('.')[1] || '').length
|
|
|
+ const decimals2 = (value2.toString().split('.')[1] || '').length
|
|
|
+ const totalDecimals = decimals1 + decimals2
|
|
|
+
|
|
|
+ // 转换为整数进行计算,然后除以相应的倍数
|
|
|
+ const int1 = Math.round(value1 * Math.pow(10, decimals1))
|
|
|
+ const int2 = Math.round(value2 * Math.pow(10, decimals2))
|
|
|
+
|
|
|
+ return (int1 * int2) / Math.pow(10, totalDecimals)
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 精确计算两个数字的除法
|
|
|
+ * @description 避免浮点数计算精度问题
|
|
|
+ * @param {number|string} dividend - 被除数
|
|
|
+ * @param {number|string} divisor - 除数
|
|
|
+ * @returns {number} 计算结果
|
|
|
+ * @example
|
|
|
+ * preciseDivide(0.3, 0.1) // 返回 3(而不是 2.9999999999999996)
|
|
|
+ */
|
|
|
+export function preciseDivide(dividend, divisor) {
|
|
|
+ const validation1 = validateNumber(dividend)
|
|
|
+ const validation2 = validateNumber(divisor)
|
|
|
+
|
|
|
+ if (!validation1.isValid || !validation2.isValid || validation2.value === 0) {
|
|
|
+ return 0
|
|
|
+ }
|
|
|
+
|
|
|
+ const value1 = validation1.value
|
|
|
+ const value2 = validation2.value
|
|
|
+
|
|
|
+ // 获取小数位数
|
|
|
+ const decimals1 = (value1.toString().split('.')[1] || '').length
|
|
|
+ const decimals2 = (value2.toString().split('.')[1] || '').length
|
|
|
+ const maxDecimals = Math.max(decimals1, decimals2)
|
|
|
+
|
|
|
+ // 转换为整数进行计算
|
|
|
+ const int1 = Math.round(value1 * Math.pow(10, maxDecimals))
|
|
|
+ const int2 = Math.round(value2 * Math.pow(10, maxDecimals))
|
|
|
+
|
|
|
+ return int1 / int2
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 四舍五入到指定小数位数
|
|
|
+ * @description 精确的四舍五入函数
|
|
|
+ * @param {number|string} value - 数字值
|
|
|
+ * @param {number} [decimals=4] - 小数位数
|
|
|
+ * @returns {number} 四舍五入后的结果
|
|
|
+ * @example
|
|
|
+ * preciseRound(123.45678, 4) // 返回 123.4568
|
|
|
+ * preciseRound(123.45678, 2) // 返回 123.46
|
|
|
+ */
|
|
|
+export function preciseRound(value, decimals = 4) {
|
|
|
+ const validation = validateNumber(value)
|
|
|
+ if (!validation.isValid) {
|
|
|
+ return 0
|
|
|
+ }
|
|
|
+
|
|
|
+ const multiplier = Math.pow(10, decimals)
|
|
|
+ return Math.round(validation.value * multiplier) / multiplier
|
|
|
+}
|