Kaynağa Gözat

refactor(订单表单): 重构数字格式化逻辑并优化精度处理

yz 1 ay önce
ebeveyn
işleme
4fa73bf33b

+ 60 - 24
src/components/order-form/constants.js

@@ -133,39 +133,43 @@ export {
  * @property {string} itemName - 物料名称
  * @property {string} specs - 规格型号
  * @property {string} unit - 单位
- * @property {number} availableQuantity - 可用数量
- * @property {number} orderQuantity - 订单数量
- * @property {number} confirmQuantity - 确认数量
- * @property {number} unitPrice - 单价
- * @property {number} taxRate - 税率(%)
- * @property {number} taxAmount - 税额
- * @property {number} totalAmount - 总金额
+ * @property {number} availableQuantity - 可用数量(4位浮点型)
+ * @property {number} orderQuantity - 订单数量(整数)
+ * @property {number} confirmQuantity - 确认数量(整数)
+ * @property {number} unitPrice - 单价(4位浮点型)
+ * @property {number} taxRate - 税率百分比(4位浮点型)(%)
+ * @property {number} taxAmount - 税额(2位小数)
+ * @property {number} totalAmount - 总金额(2位小数)
  * @property {string} remark - 备注
- * @property {string} status - 状态
- * @property {string} dataSource - 数据来源 (REMOTE|IMPORTED)
+ * @property {keyof typeof MaterialDetailStatus} status - 状态
+ * @property {keyof typeof MaterialDetailDataSource} dataSource - 数据来源 (REMOTE|IMPORTED)
+ * @property {boolean} isDeletable - 是否可删除
  * @property {string} mainCategoryId - 主物料分类ID
  * @property {string} mainItemCategoryName - 主物料分类名称
  * @property {string} warehouseId - 仓库ID
  * @property {string} warehouseName - 仓库名称
- * @property {Date} createTime - 创建时间
- * @property {Date} updateTime - 更新时间
+ * @property {Date} createTime - 创建时间(ISO字符串)
+ * @property {Date} updateTime - 更新时间(ISO字符串)
  */
 
 /**
  * 订单表单模型
  * @typedef {Object} OrderFormModel
- * @property {string} orderCode - 订单编码
- * @property {string} orderType - 订单类型
- * @property {string} orderStatus - 订单状态
+ * @property {string} id - 订单ID
+ * @property {string} orderNumber - 订单编号
+ * @property {keyof typeof OrderType} orderType - 订单类型
+ * @property {keyof typeof OrderStatus} status - 订单状态
  * @property {string} customerCode - 客户编码
  * @property {string} customerName - 客户名称
- * @property {string} contactPerson - 联系人
+ * @property {string} customerContact - 客户联系人
  * @property {string} contactPhone - 联系电话
  * @property {string} deliveryAddress - 交货地址
- * @property {Date} expectedDeliveryDate - 预期交货日期
+ * @property {Date} orderDate - 订单日期
+ * @property {Date} deliveryDate - 预期交货日期
+ * @property {number} totalQuantity - 总数量
+ * @property {number} totalAmount - 订单总金额
  * @property {string} remark - 备注
  * @property {MaterialDetailRecord[]} materialDetails - 物料明细列表
- * @property {number} totalAmount - 订单总金额
  * @property {Date} createTime - 创建时间
  * @property {Date} updateTime - 更新时间
  * @property {string} createdBy - 创建人
@@ -173,13 +177,14 @@ export {
  */
 
 /**
- * 物料明细查询参数
- * @typedef {Object} MaterialDetailQueryParams
- * @property {string} [materialCode] - 物料编码
- * @property {string} [materialName] - 物料名称
+ * 物料查询参数
+ * @typedef {Object} MaterialQueryParams
+ * @property {string} [itemCode] - 物料编码
+ * @property {string} [itemName] - 物料名称
  * @property {string} [mainItemCategoryId] - 主物料分类ID
  * @property {string} [subItemCategoryId] - 子物料分类ID
  * @property {string} [brandId] - 品牌ID
+ * @property {string} [warehouseId] - 仓库ID
  * @property {number} [pageNum] - 页码
  * @property {number} [pageSize] - 每页大小
  */
@@ -187,7 +192,7 @@ export {
 /**
  * 物料删除事件数据
  * @typedef {Object} MaterialDeleteEventData
- * @property {string} id - 要删除的物料明细ID
+ * @property {MaterialDetailRecord} row - 要删除的物料明细记录
  * @property {number} index - 在列表中的索引
  */
 
@@ -197,23 +202,28 @@ export {
  * @property {boolean} required - 是否必填
  * @property {string} message - 验证失败消息
  * @property {string} [trigger] - 触发方式
+ * @property {string} [type] - 验证类型
+ * @property {number} [min] - 最小值
+ * @property {number} [max] - 最大值
  * @property {Function} [validator] - 自定义验证函数
  */
 
 /**
  * API响应数据结构
+ * @template T
  * @typedef {Object} ApiResponse
  * @property {number} code - 响应状态码
  * @property {string} message - 响应消息
- * @property {*} data - 响应数据
+ * @property {T} data - 响应数据
  * @property {boolean} success - 是否成功
  * @property {number} timestamp - 时间戳
  */
 
 /**
  * 分页响应数据结构
+ * @template T
  * @typedef {Object} PaginatedResponse
- * @property {Array} records - 数据记录列表
+ * @property {T[]} records - 数据记录列表
  * @property {number} total - 总记录数
  * @property {number} size - 每页大小
  * @property {number} current - 当前页码
@@ -221,3 +231,29 @@ export {
  * @property {boolean} hasNext - 是否有下一页
  * @property {boolean} hasPrevious - 是否有上一页
  */
+
+/**
+ * 数字验证结果
+ * @typedef {Object} NumberValidationResult
+ * @property {boolean} isValid - 是否为有效数字
+ * @property {number} value - 转换后的数字值
+ * @property {string} [error] - 错误信息(验证失败时)
+ */
+
+/**
+ * 数字格式化配置
+ * @typedef {Object} NumberFormatConfig
+ * @property {number} decimalPlaces - 小数位数
+ * @property {boolean} [showThousandsSeparator] - 是否显示千分位分隔符
+ * @property {string} [prefix] - 前缀(如货币符号)
+ * @property {string} [suffix] - 后缀(如百分号)
+ */
+
+/**
+ * 订单表单混入数据
+ * @typedef {Object} OrderFormMixinData
+ * @property {OrderFormModel} formData - 表单数据
+ * @property {boolean} saveLoading - 保存加载状态
+ * @property {MaterialDetailRecord[]} materialDetails - 物料明细列表
+ */
+

+ 76 - 26
src/components/order-form/material-detail-table.vue

@@ -60,7 +60,7 @@
             @input="validateFloatInput($event, scope.row, 'unitPrice')"
             @blur="handleUnitPriceChange(scope.row, scope.$index)"
           />
-          <span v-else>{{ formatQuantity(scope.row.unitPrice) }}</span>
+          <span v-else>{{ formatUnitPrice(scope.row.unitPrice) }}</span>
         </template>
         <!-- 订单数量自定义模板 -->
         <template slot="orderQuantity" slot-scope="scope">
@@ -73,7 +73,7 @@
             @input="validateIntegerInput($event, scope.row, 'orderQuantity')"
             @blur="handleQuantityChange(scope.row, scope.$index)"
           />
-          <span v-else>{{ formatQuantity(scope.row.orderQuantity, true) }}</span>
+          <span v-else>{{ formatIntegerNumber(scope.row.orderQuantity) }}</span>
         </template>
 
         <!-- 税率自定义模板 -->
@@ -101,7 +101,7 @@
             @input="validateFloatInput($event, scope.row, 'taxAmount')"
             @blur="handleTaxAmountChange(scope.row, scope.$index)"
           />
-          <span v-else>{{ formatQuantity(scope.row.taxAmount) }}</span>
+          <span v-else>{{ formatAmount(scope.row.taxAmount, false) }}</span>
         </template>
 
         <!-- 总金额自定义模板 -->
@@ -115,7 +115,7 @@
             @input="validateFloatInput($event, scope.row, 'totalAmount')"
             @blur="handleTotalAmountChange(scope.row, scope.$index)"
           />
-          <span v-else>{{ formatQuantity(scope.row.totalAmount) }}</span>
+          <span v-else>{{ formatAmount(scope.row.totalAmount, false) }}</span>
         </template>
 
         <!-- 明细状态列自定义渲染 -->
@@ -170,7 +170,22 @@ import {
   getMaterialDetailStatusColor
 } from './constants'
 import MaterialImportDialog from './material-import-dialog.vue'
-import { formatAmount, formatQuantity } from './utils'
+import { 
+  formatAmount as legacyFormatAmount, 
+  formatQuantity as legacyFormatQuantity 
+} from './utils'
+import { 
+  formatAmount,
+  formatFloatNumber,
+  formatIntegerNumber,
+  formatUnitPrice,
+  formatTaxRate,
+  preciseMultiply,
+  preciseDivide,
+  preciseRound,
+  validateNumber,
+  NUMBER_TYPES
+} from './number-format-utils'
 import { MaterialDetailDataSource } from './constants'
 
 /**
@@ -492,20 +507,44 @@ export default {
 
     /**
      * 格式化金额显示
-     * @description 使用公共工具函数格式化金额
+     * @description 格式化金额为带货币符号的字符串,保留2位小数
      * @param {number|string|null|undefined} amount - 金额数值
      * @returns {string} 格式化后的金额字符串
      */
-    formatAmount,
+    formatAmount(amount) {
+      return formatAmount(amount, true)
+    },
     
     /**
      * 格式化数量显示
-     * @description 使用公共工具函数格式化数量
+     * @description 格式化数量为4位浮点型字符串
      * @param {number|string|null|undefined} quantity - 数量数值
      * @param {boolean} [isInteger=false] - 是否为整数类型
      * @returns {string} 格式化后的数量字符串
      */
-    formatQuantity,
+    formatQuantity(quantity, isInteger = false) {
+      return isInteger ? formatIntegerNumber(quantity) : formatFloatNumber(quantity)
+    },
+    
+    /**
+     * 格式化单价显示
+     * @description 格式化单价为4位浮点型字符串
+     * @param {number|string|null|undefined} price - 单价数值
+     * @returns {string} 格式化后的单价字符串
+     */
+    formatUnitPrice(price) {
+      return formatUnitPrice(price)
+    },
+    
+    /**
+     * 格式化税率显示
+     * @description 格式化税率为百分比字符串
+     * @param {number|string|null|undefined} rate - 税率数值
+     * @returns {string} 格式化后的税率字符串
+     */
+    formatTaxRate(rate) {
+      return formatTaxRate(rate, true)
+    },
 
     /**
      * 获取状态标签类型
@@ -705,25 +744,29 @@ export default {
 
     /**
      * 自动计算金额
-     * @description 根据订单数量、单价和税率自动计算总金额和税额
+     * @description 根据订单数量、单价和税率自动计算总金额和税额,使用精确计算避免浮点数精度问题
      * @param {MaterialDetailRecord} row - 物料明细记录
      * @returns {MaterialDetailRecord} 计算后的物料明细记录
      */
     calculateAmounts(row) {
       const calculatedRow = { ...row }
       
-      // 获取数值,确保为有效数字
-      const orderQuantity = parseInt(calculatedRow.orderQuantity) || 0  // 订单数量为整数
-      const unitPrice = parseFloat(calculatedRow.unitPrice) || 0
-      const taxRate = parseFloat(calculatedRow.taxRate) || 0
+      // 验证并获取数值
+      const quantityValidation = validateNumber(calculatedRow.orderQuantity)
+      const priceValidation = validateNumber(calculatedRow.unitPrice)
+      const rateValidation = validateNumber(calculatedRow.taxRate)
       
-      // 计算总金额:订单数量 * 单价(保留2位小数)
-      const totalAmount = orderQuantity * unitPrice
-      calculatedRow.totalAmount = Math.round(totalAmount * 100) / 100
+      const orderQuantity = quantityValidation.isValid ? Math.round(quantityValidation.value) : 0
+      const unitPrice = priceValidation.isValid ? priceValidation.value : 0
+      const taxRate = rateValidation.isValid ? rateValidation.value : 0
       
-      // 计算税额:总金额 * 税率 / 100(保留2位小数)
-      const taxAmount = totalAmount * (taxRate / 100)
-      calculatedRow.taxAmount = Math.round(taxAmount * 100) / 100
+      // 使用精确计算:订单数量 * 单价
+      const totalAmount = preciseMultiply(orderQuantity, unitPrice)
+      calculatedRow.totalAmount = preciseRound(totalAmount, 2)
+      
+      // 使用精确计算:总金额 * 税率 / 100
+      const taxAmount = preciseMultiply(totalAmount, preciseDivide(taxRate, 100))
+      calculatedRow.taxAmount = preciseRound(taxAmount, 2)
       
       return calculatedRow
     },
@@ -732,16 +775,23 @@ export default {
 
     /**
      * 计算税额
-     * @description 根据总金额和税率计算税额
+     * @description 根据总金额和税率计算税额,使用精确计算避免浮点数精度问题
      * @param {MaterialDetailRecord} row - 物料明细记录
      * @returns {void}
      */
     calculateTaxAmount(row) {
-      if (row.totalAmount && row.taxRate) {
-        const totalAmount = parseFloat(row.totalAmount) || 0
-        const taxRate = parseFloat(row.taxRate) || 0
-        const taxAmount = totalAmount * (taxRate / 100)
-        row.taxAmount = Math.round(taxAmount * 100) / 100  // 保留2位小数
+      const amountValidation = validateNumber(row.totalAmount)
+      const rateValidation = validateNumber(row.taxRate)
+      
+      if (amountValidation.isValid && rateValidation.isValid) {
+        const totalAmount = amountValidation.value
+        const taxRate = rateValidation.value
+        
+        // 使用精确计算:总金额 * 税率 / 100
+        const taxAmount = preciseMultiply(totalAmount, preciseDivide(taxRate, 100))
+        row.taxAmount = preciseRound(taxAmount, 2)
+      } else {
+        row.taxAmount = 0
       }
     },
 

+ 33 - 22
src/components/order-form/material-import-dialog.vue

@@ -147,6 +147,12 @@ import { getMaterialImportOption, DEFAULT_QUERY_PARAMS } from './material-detail
 import { MaterialDetailStatus } from './constants'
 import { getItemList } from '@/api/common'
 import { generateUniqueId } from './utils'
+import {
+  formatFloatNumber,
+  formatIntegerNumber,
+  preciseRound,
+  validateNumber
+} from './number-format-utils'
 
 /**
  * @typedef {import('./material-detail-option').MaterialDetailItem} MaterialDetailItem
@@ -488,28 +494,33 @@ export default {
       try {
         this.confirmLoading = true
 
-        // 转换为物料明细格式
-        const importedMaterials = this.selectedMaterials.map((material) => ({
-          id: generateUniqueId(),
-          itemId: material.itemId || material.id,
-          itemCode: material.itemCode,
-          itemName: material.itemName,
-          specs: material.specification,
-          mainItemCategoryId: material.mainCategoryId,
-          mainItemCategoryName: material.mainCategoryName,
-          warehouseId: material.warehouseId,
-          warehouseName: material.warehouseName,
-          availableQuantity: material.availableQuantity || 0,
-          orderQuantity: 1, // 默认订单数量为1
-          confirmQuantity: 0,
-          unitPrice: 0,
-          taxRate: 0,
-          taxAmount: 0,
-          totalAmount: 0,
-          status: MaterialDetailStatus.PENDING,
-          createTime: new Date().toISOString(),
-          updateTime: new Date().toISOString()
-        }))
+        // 转换为物料明细格式,格式化数字字段
+        const importedMaterials = this.selectedMaterials.map((material) => {
+          // 验证和格式化可用数量
+          const availableQuantityValidation = validateNumber(material.availableQuantity)
+          
+          return {
+            id: generateUniqueId(),
+            itemId: material.itemId || material.id,
+            itemCode: material.itemCode,
+            itemName: material.itemName,
+            specs: material.specification,
+            mainItemCategoryId: material.mainCategoryId,
+            mainItemCategoryName: material.mainCategoryName,
+            warehouseId: material.warehouseId,
+            warehouseName: material.warehouseName,
+            availableQuantity: availableQuantityValidation.isValid ? preciseRound(availableQuantityValidation.value, 4) : 0,
+            orderQuantity: 1, // 默认订单数量为1
+            confirmQuantity: 0,
+            unitPrice: 0, // 默认单价为0,4位浮点型
+            taxRate: 0, // 默认税率为0,4位浮点型
+            taxAmount: 0, // 默认税额为0,2位小数
+            totalAmount: 0, // 默认总金额为0,2位小数
+            status: MaterialDetailStatus.PENDING,
+            createTime: new Date().toISOString(),
+            updateTime: new Date().toISOString()
+          }
+        })
 
         this.$emit('confirm', importedMaterials)
         this.dialogVisible = false

+ 361 - 0
src/components/order-form/number-format-utils.js

@@ -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
+}

+ 110 - 40
src/components/order-form/order-form-mixin.js

@@ -7,6 +7,18 @@ import {
   ORDER_STATUS_OPTIONS
 } from '@/constants/order'
 import { MaterialDetailDataSource } from './constants'
+import {
+  formatAmount,
+  formatFloatNumber,
+  formatIntegerNumber,
+  formatUnitPrice,
+  formatTaxRate,
+  preciseMultiply,
+  preciseDivide,
+  preciseRound,
+  validateNumber,
+  NUMBER_TYPES
+} from './number-format-utils'
 
 /**
  * @typedef {import('./constants').MaterialDetailRecord} MaterialDetailRecord
@@ -308,6 +320,9 @@ export default {
 
       try {
         // 并行加载订单详情和物料明细数据
+        /**
+         * @type {[AxiosResponse<ApiResponse<OrderItem>>, MaterialDetailRecord[]]}
+         */
         const [orderResponse, materialResponse] = await Promise.all([
           getDetail(orderId),
           this.loadMaterialDetails(orderId)
@@ -339,8 +354,18 @@ export default {
      * @returns {Promise<MaterialDetailRecord[]>} 物料明细记录列表,失败时返回空数组
      * @private
      */
+    /**
+     * 加载物料明细数据
+     * @description 根据订单ID获取物料明细列表,并为每个物料添加数据来源标识
+     * @param {string|number} orderId - 订单ID
+     * @returns {Promise<MaterialDetailRecord[]>} 处理后的物料明细数组
+     * @private
+     */
     async loadMaterialDetails(orderId) {
       try {
+        /**
+         * @type {AxiosResponse<ApiResponse<PageResult<OrderItemRecord>>>}
+         */
         const response = await getOrderItemList(1, 1000, { orderId })
 
         if (!response || !response.data || !response.data.data) {
@@ -348,13 +373,28 @@ export default {
         }
 
         const materialDetails = response.data.data.records || []
-        
-        // 为远程加载的物料数据添加数据来源标识
-        return materialDetails.map(material => ({
-          ...material,
-          dataSource: MaterialDetailDataSource.REMOTE,
-          isDeletable: false // 远程加载的数据不可删除
-        }))
+
+        // 为远程加载的物料数据添加数据来源标识并格式化数字字段
+        return materialDetails.map(material => {
+          // 验证和格式化数字字段
+          const orderQuantityValidation = validateNumber(material.orderQuantity)
+          const unitPriceValidation = validateNumber(material.unitPrice)
+          const taxRateValidation = validateNumber(material.taxRate)
+          const taxAmountValidation = validateNumber(material.taxAmount)
+          const totalAmountValidation = validateNumber(material.totalAmount)
+
+          return {
+            ...material,
+            dataSource: MaterialDetailDataSource.REMOTE,
+            isDeletable: false, // 远程加载的数据不可删除
+            // 格式化数字字段
+            orderQuantity: orderQuantityValidation.isValid ? Math.round(orderQuantityValidation.value) : 0,
+            unitPrice: unitPriceValidation.isValid ? preciseRound(unitPriceValidation.value, 2) : 0,
+            taxRate: taxRateValidation.isValid ? preciseRound(taxRateValidation.value, 4) : 0,
+            taxAmount: taxAmountValidation.isValid ? preciseRound(taxAmountValidation.value, 2) : 0,
+            totalAmount: totalAmountValidation.isValid ? preciseRound(totalAmountValidation.value, 2) : 0
+          }
+        })
       } catch (error) {
         this.$message.warning('加载物料明细失败,请稍后重试')
         console.error('加载物料明细失败:', error)
@@ -364,12 +404,17 @@ export default {
 
     /**
      * 映射订单数据到表单格式
-     * @description 将API返回的订单数据安全地映射为表单数据格式
-     * @param {Object} orderData - 从API获取的原始订单数据
+     * @description 将API返回的订单数据安全地映射为表单数据格式,并格式化数字字段
+     * @param {OrderItem} orderData - 从API获取的原始订单数据
      * @returns {OrderFormModel} 格式化后的表单数据
      * @private
      */
     mapOrderDataToForm(orderData) {
+      // 验证和格式化数字字段
+      const totalAmountValidation = validateNumber(orderData.totalAmount)
+      const totalQuantityValidation = validateNumber(orderData.totalQuantity)
+      const orderQuantityValidation = validateNumber(orderData.orderQuantity)
+
       return {
         id: orderData.id || null,
         orderCode: String(orderData.orderCode || ''),
@@ -378,8 +423,9 @@ export default {
         customerCode: String(orderData.customerCode || ''),
         customerName: String(orderData.customerName || ''),
         orderType: Number(orderData.orderType) || ORDER_TYPES.NORMAL,
-        totalAmount: orderData.totalAmount ? Number(orderData.totalAmount) : null,
-        totalQuantity: orderData.totalQuantity ? Number(orderData.totalQuantity) : null,
+        orderQuantity: orderQuantityValidation.isValid ? parseInt(orderQuantityValidation.value) : null,
+        totalAmount: totalAmountValidation.isValid ? preciseRound(totalAmountValidation.value, 2) : null,
+        totalQuantity: totalQuantityValidation.isValid ? preciseRound(totalQuantityValidation.value, 4) : null,
         receiverName: String(orderData.receiverName || ''),
         receiverPhone: String(orderData.receiverPhone || ''),
         receiverAddress: String(orderData.receiverAddress || ''),
@@ -459,8 +505,8 @@ export default {
     /**
      * 提交订单数据到服务器
      * @description 根据编辑模式调用相应的API接口
-     * @param {Object} submitData - 要提交的订单数据
-     * @returns {Promise<ApiResponse>} API响应结果
+     * @param {OrderForm} submitData - 要提交的订单数据
+     * @returns {Promise<AxiosResponse<ApiResponse<boolean>>>} API响应结果
      * @private
      */
     async submitOrderData(submitData) {
@@ -513,9 +559,9 @@ export default {
 
     /**
      * 准备提交数据
-     * @description 处理表单数据,移除空值字段并确保数据类型正确
-     * @returns {Object} 格式化后的提交数据对象
-     * @public
+     * @description 复制表单数据并进行清理和格式化处理
+     * @returns {OrderForm} 准备好的提交数据
+     * @private
      */
     prepareSubmitData() {
       const submitData = { ...this.formData }
@@ -526,9 +572,9 @@ export default {
 
     /**
      * 清理和格式化提交数据
-     * @description 移除空值字段并确保数据类型正确
+     * @description 移除空值字段并确保数据类型正确,使用精确的数字验证和格式化
      * @param {OrderFormModel} data - 原始表单数据
-     * @returns {Object} 清理后的数据对象
+     * @returns {OrderForm} 清理后的数据对象
      * @private
      */
     cleanAndFormatSubmitData(data) {
@@ -542,8 +588,14 @@ export default {
           return
         }
 
-        // 确保数字类型字段的正确性
-        if (['totalAmount', 'totalQuantity', 'orderType', 'status'].includes(key)) {
+        // 使用精确的数字验证和格式化
+        if (key === 'totalAmount') {
+          const validation = validateNumber(value)
+          cleanedData[key] = validation.isValid ? preciseRound(validation.value, 2) : 0
+        } else if (key === 'totalQuantity') {
+          const validation = validateNumber(value)
+          cleanedData[key] = validation.isValid ? Math.round(validation.value) : 0
+        } else if (['orderType', 'status'].includes(key)) {
           cleanedData[key] = Number(value) || 0
         } else {
           cleanedData[key] = value
@@ -555,13 +607,13 @@ export default {
 
     /**
       * 处理物料删除事件
-      * @description 从物料明细列表中删除指定的物料记录,仅允许删除可删除的物料
-      * @param {Object} deleteData - 删除数据对象
-      * @param {MaterialDetailRecord} deleteData.row - 要删除的物料记录
-      * @param {number} deleteData.index - 记录在当前页的索引
-      * @returns {void}
-      * @public
-      */
+       * @description 从物料明细列表中删除指定的物料记录,仅允许删除可删除的物料
+       * @param {MaterialDeleteEventData} deleteData - 删除数据对象
+       * @param {MaterialDetailRecord} deleteData.row - 要删除的物料记录
+       * @param {number} deleteData.index - 记录在当前页的索引
+       * @returns {void}
+       * @public
+       */
      handleMaterialDelete({ row, index }) {
        if (!row) {
          this.$message.warning('删除数据无效')
@@ -576,8 +628,8 @@ export default {
 
        try {
          // 从物料明细列表中移除该记录
-         const materialIndex = this.materialDetails.findIndex(item => 
-           item.itemCode === row.itemCode && 
+         const materialIndex = this.materialDetails.findIndex(item =>
+           item.itemCode === row.itemCode &&
            item.dataSource === row.dataSource
          )
 
@@ -595,7 +647,7 @@ export default {
 
     /**
      * 处理物料导入事件
-     * @description 将导入的物料数据添加到物料明细列表中,导入数据可删除
+     * @description 将导入的物料数据添加到物料明细列表中,格式化数字字段并标记为可删除
      * @param {MaterialDetailRecord[]} importedMaterials - 导入的物料数据数组
      * @returns {void}
      * @public
@@ -607,16 +659,34 @@ export default {
       }
 
       try {
-        // 为导入的物料添加数据来源标识
-        const processedMaterials = importedMaterials.map(material => ({
-          ...material,
-          dataSource: MaterialDetailDataSource.IMPORTED,
-          isDeletable: true // 导入的数据可以删除
-        }))
-
-        // 添加到现有物料列表中
-        this.materialDetails.push(...processedMaterials)
-        this.$message.success(`成功导入 ${importedMaterials.length} 条物料记录`)
+        // 为导入的物料添加数据来源标识并格式化数字字段
+        const formattedMaterials = importedMaterials.map(material => {
+          const formatted = {
+            ...material,
+            dataSource: MaterialDetailDataSource.IMPORTED,
+            isDeletable: true
+          }
+
+          // 格式化数字字段
+          const quantityValidation = validateNumber(formatted.orderQuantity)
+          const priceValidation = validateNumber(formatted.unitPrice)
+          const rateValidation = validateNumber(formatted.taxRate)
+          const amountValidation = validateNumber(formatted.totalAmount)
+          const taxAmountValidation = validateNumber(formatted.taxAmount)
+
+          formatted.orderQuantity = quantityValidation.isValid ? Math.round(quantityValidation.value) : 1
+          formatted.unitPrice = priceValidation.isValid ? preciseRound(priceValidation.value, 2) : 0
+          formatted.taxRate = rateValidation.isValid ? preciseRound(rateValidation.value, 4) : 0
+          formatted.taxAmount = taxAmountValidation.isValid ? preciseRound(taxAmountValidation.value, 2) : 0
+          formatted.totalAmount = amountValidation.isValid ? preciseRound(amountValidation.value, 2) : 0
+
+          return formatted
+        })
+
+        // 添加到物料明细列表
+        this.materialDetails.push(...formattedMaterials)
+
+        this.$message.success(`成功导入 ${importedMaterials.length} 条物料明细`)
       } catch (error) {
         this.$message.error('导入物料失败,请重试')
         console.error('导入物料失败:', error)

+ 0 - 39
src/components/order-form/utils.js

@@ -4,45 +4,6 @@
  */
 
 /**
- * 格式化金额显示
- * @description 将数字金额格式化为带货币符号的字符串,保留2位小数
- * @param {number|string|null|undefined} amount - 金额数值,支持数字、字符串或空值
- * @returns {string} 格式化后的金额字符串,格式为 ¥XX.XX
- * @example
- * formatAmount(123.456) // 返回 "¥123.46"
- * formatAmount(null) // 返回 "¥0.00"
- * formatAmount('100') // 返回 "¥100.00"
- */
-export function formatAmount(amount) {
-  const numAmount = Number(amount || 0)
-  return `¥${numAmount.toFixed(2)}`
-}
-
-/**
- * 格式化数量显示
- * @description 将数字数量格式化为字符串,整数不显示小数点,浮点数保留2位小数
- * @param {number|string|null|undefined} quantity - 数量数值
- * @param {boolean} [isInteger=false] - 是否为整数类型
- * @returns {string} 格式化后的数量字符串
- * @example
- * formatQuantity(123.4567) // 返回 "123.46"
- * formatQuantity(100.0000) // 返回 "100"
- * formatQuantity(123, true) // 返回 "123"
- * formatQuantity(null) // 返回 "0"
- */
-export function formatQuantity(quantity, isInteger = false) {
-  const numQuantity = Number(quantity || 0)
-  
-  if (isInteger) {
-    // 整数格式化:四舍五入到整数
-    return Math.round(numQuantity).toString()
-  } else {
-    // 浮点数格式化:保留2位小数
-    return numQuantity.toFixed(2)
-  }
-}
-
-/**
  * 格式化百分比显示
  * @description 将数字格式化为百分比字符串
  * @param {number|string|null|undefined} rate - 百分比数值(0-100)