Quellcode durchsuchen

refactor(物料明细表格): 将业务逻辑提取到mixin中

yz vor 1 Monat
Ursprung
Commit
54336c17eb

+ 902 - 0
src/components/order-form/material-detail-mixin.js

@@ -0,0 +1,902 @@
+/**
+ * @fileoverview 物料明细表格组件
+ * @description 基于AvueJS的物料明细数据展示和操作组件,支持分页、搜索、删除等功能
+ * @typedef {import('./types').MaterialDetailTableComponent} MaterialDetailTableComponent
+ */
+
+import { getMaterialDetailOption, DEFAULT_PAGINATION_CONFIG } from './material-detail-option'
+import {
+  MaterialDetailStatus,
+  getOrderItemStatusLabel as getMaterialDetailStatusLabel,
+  getOrderItemStatusTagType as getMaterialDetailStatusTagType,
+  getOrderItemStatusColor as getMaterialDetailStatusColor,
+  MaterialDetailDataSource
+} from '@/constants/order'
+import { MATERIAL_DETAIL_EVENTS, DIALOG_EVENTS } from './events'
+import { getMaterialFullList } from '@/api/order/sales-order'
+import {
+  formatAmount,
+  formatFloatNumber,
+  formatIntegerNumber,
+  formatUnitPrice,
+  formatTaxRate,
+  preciseMultiply,
+  preciseDivide,
+  preciseRound,
+  validateNumber,
+  NUMBER_TYPES
+} from './number-format-utils'
+
+/**
+ * @typedef {import('./types').MaterialDetailRecord} MaterialDetailRecord
+ * @typedef {import('./types').MaterialUpdateEventData} MaterialUpdateEventData
+ * @typedef {import('./types').MaterialDeleteEventData} MaterialDeleteEventData
+ * @typedef {import('./types').MaterialDetailQueryParams} MaterialDetailQueryParams
+ * @typedef {import('smallwei__avue/crud').AvueCrudOption} AvueCrudOption
+ * @typedef {import('smallwei__avue/crud').AvueCrudColumn} AvueCrudColumn
+ * @typedef {import('smallwei__avue/crud').PageOption} PageOption
+ */
+
+
+
+// 使用@types/smallwei__avue/crud中的PageOption类型代替PaginationConfig
+
+/**
+ * 组件数据类型定义
+ * @typedef {Object} MaterialDetailTableData
+ * @property {Partial<MaterialDetailRecord>} formData - 表单数据
+ * @property {PageOption} page - 分页配置
+ * @property {boolean} importDialogVisible - 导入弹窗显示状态
+ */
+
+// 状态处理已统一使用明细管理中的工具函数,无需本地映射常量
+
+/**
+ * 物料明细表格组件
+ * @description 用于展示和编辑订单的物料明细信息,支持物料导入和实时编辑功能
+ * 当物料数量、单价、税率等字段变更时,自动计算总金额和税额,并触发父组件重新计算订单总计
+ * @emits {MaterialDetailRecord[]} material-import - 物料导入事件
+ * @emits {Object} material-update - 物料明细更新事件,包含更新的行数据和索引
+ * @emits {Object} material-delete - 物料明细删除事件
+ * @emits {void} refresh - 刷新事件
+ */
+export default {
+  name: 'MaterialDetailTable',
+  components: {},
+
+  /**
+   * 组件属性定义
+   * @description 定义组件接收的外部属性
+   */
+  props: {
+    /**
+     * 是否为编辑模式 - 控制表格是否可编辑
+     * @type {boolean}
+     */
+    editMode: {
+      type: Boolean,
+      default: false
+    },
+    /**
+     * 订单ID - 关联的订单唯一标识符
+     * @type {string|number|null}
+     */
+    orderId: {
+      type: [String, Number],
+      default: null,
+      validator: (value) => value === null || value === undefined || (typeof value === 'string' && value.length > 0) || (typeof value === 'number' && value > 0)
+    },
+
+    /**
+     * 物料明细列表 - 要展示的物料明细数据
+     * @type {MaterialDetailRecord[]} 物料明细数据数组,每个元素包含物料的详细信息
+     */
+    materialDetails: {
+      type: Array,
+      required: true,
+      default: () => [],
+      validator: (value) => Array.isArray(value) && value.every(item =>
+        typeof item === 'object' && item !== null &&
+        typeof item.id === 'string' &&
+        typeof item.itemCode === 'string'
+      )
+    }
+  },
+
+  /**
+   * 组件数据
+   * @returns {MaterialDetailTableData} 组件响应式数据对象
+   * @this {MaterialDetailTableComponent}
+   */
+  data() {
+    return {
+      /**
+       * 表单数据 - 当前编辑行的数据
+       * @type {Partial<MaterialDetailRecord>} 物料明细表单数据对象
+       */
+      formData: {},
+
+      /**
+       * 分页配置 - AvueJS表格分页相关配置
+       * @type {PaginationConfig} 包含currentPage、pageSize、total等属性的分页配置对象
+       */
+      page: {
+        currentPage: 1,
+        pageSize: DEFAULT_PAGINATION_CONFIG.pageSize,
+        total: 0
+      },
+
+      /**
+       * 选中的物料ID - 当前在下拉框中选中的物料ID
+       * @type {string|null}
+       */
+      selectedMaterialId: null,
+
+      /**
+       * 物料选项列表 - 远程搜索返回的物料选项
+       * @type {ItemRecord[]}
+       */
+      materialOptions: [],
+
+      /**
+       * 物料搜索加载状态 - 控制远程搜索时的加载状态
+       * @type {boolean}
+       */
+      materialLoading: false,
+
+      /**
+       * 搜索防抖定时器 - 用于防抖处理远程搜索
+       * @type {number|null}
+       */
+      searchTimer: null,
+
+      /**
+       * 事件常量
+       */
+      DIALOG_EVENTS,
+
+      /**
+       * 正在编辑的行数据 - 用于记录编辑前的状态
+       * @type {MaterialDetailRecord|null}
+       */
+      editingRow: null,
+
+      /**
+       * 正在编辑的属性名 - 用于记录当前编辑的字段
+       * @type {string|null}
+       */
+      editingProp: null
+    }
+  },
+
+  /**
+   * 计算属性
+   * @this {MaterialDetailTableComponent}
+   */
+  computed: {
+    /**
+     * 表格配置选项 - 获取AvueJS表格的配置对象
+     * @returns {AvueCrudOption} AvueJS表格配置对象,根据编辑模式配置
+     */
+    tableOption() {
+      return getMaterialDetailOption(this.editMode)
+    },
+
+    /**
+     * 当前页显示的数据 - 根据分页配置计算当前页应显示的数据
+     * @returns {MaterialDetailRecord[]} 当前页的物料明细数据
+     */
+    currentPageData() {
+      const { currentPage, pageSize } = this.page
+      const startIndex = (currentPage - 1) * pageSize
+      const endIndex = startIndex + pageSize
+      return this.materialDetails.slice(startIndex, endIndex)
+    }
+  },
+
+  /**
+   * 监听器
+   * @this {MaterialDetailTableComponent}
+   */
+  watch: {
+    /**
+     * 监听物料明细变化
+     * @param {MaterialDetailRecord[]} newVal - 新的物料明细列表
+     * @returns {void}
+     */
+    materialDetails: {
+      handler(newVal) {
+        this.page.total = newVal.length
+      },
+      immediate: true
+    }
+  },
+
+  /**
+   * 组件方法
+   * @this {MaterialDetailTableComponent}
+   */
+  methods: {
+    /**
+       * 验证整数输入
+       * @param {string} value - 输入值
+       * @param {Object} row - 当前行数据
+       * @param {string} field - 字段名
+       * @param {number} min - 最小值
+       * @param {number} max - 最大值
+       */
+      validateIntegerInput(value, row, field, min = 0, max = 999999) {
+        // 允许空值和部分输入(如正在输入的数字)
+        if (value === '' || value === '-') {
+          return
+        }
+
+        // 移除所有非数字字符(除了负号)
+        let cleanValue = value.replace(/[^-\d]/g, '')
+
+        // 确保负号只能在开头
+        if (cleanValue.indexOf('-') > 0) {
+          cleanValue = cleanValue.replace(/-/g, '')
+        }
+
+        // 如果有值,转换为整数
+        if (cleanValue !== '' && cleanValue !== '-') {
+          const numValue = parseInt(cleanValue, 10)
+
+          // 检查范围
+          if (numValue < min) {
+            row[field] = min
+          } else if (numValue > max) {
+            row[field] = max
+          } else {
+            row[field] = numValue
+          }
+        }
+      },
+
+      /**
+       * 验证浮点数输入(输入时验证)
+       * @param {string} value - 输入值
+       * @param {Object} row - 当前行数据
+       * @param {string} field - 字段名
+       * @param {number} min - 最小值
+       * @param {number} max - 最大值
+       */
+      validateFloatInput(value, row, field, min = 0, max = 999999.99) {
+        // 允许空值和部分输入(包括单独的小数点、负号等)
+        if (value === '' || value === '-' || value === '.' || value === '-.') {
+          row[field] = value
+          return
+        }
+
+        // 移除无效字符,只保留数字、小数点和负号
+        let cleanValue = value.replace(/[^-\d.]/g, '')
+
+        // 确保负号只能在开头
+        if (cleanValue.indexOf('-') > 0) {
+          cleanValue = cleanValue.replace(/-/g, '')
+        }
+
+        // 确保只有一个小数点
+        const parts = cleanValue.split('.')
+        if (parts.length > 2) {
+          cleanValue = parts[0] + '.' + parts.slice(1).join('')
+        }
+
+        // 限制小数位数为2位(但允许继续输入)
+        if (parts.length === 2 && parts[1].length > 2) {
+          cleanValue = parts[0] + '.' + parts[1].substring(0, 2)
+        }
+
+        // 更新字段值,但不进行范围检查(留到blur时处理)
+        row[field] = cleanValue
+      },
+
+      /**
+     * 验证并格式化浮点数(失焦时验证)
+     * @param {Object} row - 当前行数据
+     * @param {string} field - 字段名
+     * @param {number} min - 最小值
+     * @param {number} max - 最大值
+     * @param {number} precision - 小数位数,默认2位
+     */
+    validateAndFormatFloatOnBlur(row, field, min = 0, max = 999999.99, precision = 2) {
+      const value = row[field]
+
+      // 如果是空值或无效输入,设置为最小值
+      if (value === '' || value === '.' || value === '-' || value === '-.' || isNaN(parseFloat(value))) {
+        row[field] = min
+        return
+      }
+
+      const numValue = parseFloat(value)
+
+      // 范围检查
+      if (numValue < min) {
+        row[field] = min
+      } else if (numValue > max) {
+        row[field] = max
+      } else {
+        // 格式化为指定小数位数
+        const multiplier = Math.pow(10, precision)
+        const roundedValue = Math.round(numValue * multiplier) / multiplier
+        row[field] = roundedValue
+      }
+    },
+
+    /**
+     * 判断行是否可编辑
+     * @description 根据数据来源判断物料明细行是否允许编辑,远程数据(订单ID获取)不可编辑
+     * @param {MaterialDetailRecord} row - 物料明细行数据
+     * @returns {boolean} 是否可编辑,true表示可编辑,false表示不可编辑
+     */
+    isRowEditable(row) {
+      // 如果没有数据来源信息,默认可编辑
+      if (!row || !row.dataSource) {
+        return true
+      }
+
+      // 只有导入的物料可以编辑,远程数据(订单ID获取)不可编辑
+      return row.dataSource === MaterialDetailDataSource.IMPORTED
+    },
+
+    /**
+     * 远程搜索物料
+     * @description 根据关键词远程搜索物料数据,支持防抖处理
+     * @param {string} query - 搜索关键词
+     * @returns {void}
+     */
+    remoteSearchMaterial(query) {
+      // 清除之前的定时器
+      if (this.searchTimer) {
+        clearTimeout(this.searchTimer)
+      }
+
+      // 如果查询为空,清空选项
+      if (!query) {
+        this.materialOptions = []
+        return
+      }
+
+      // 设置防抖定时器
+      this.searchTimer = setTimeout(async () => {
+        await this.searchMaterials(query)
+      }, 300)
+    },
+
+    /**
+     * 搜索物料数据
+     * @description 调用API搜索物料数据
+     * @param {string} keyword - 搜索关键词
+     * @returns {Promise<void>}
+     * @throws {Error} 当API调用失败时抛出异常
+     */
+    async searchMaterials(keyword) {
+      try {
+        this.materialLoading = true
+
+        const response = await getMaterialFullList({
+          itemName: keyword
+        })
+
+        if (response?.data?.success && response.data.data) {
+          // 转换API返回的字段名称为组件所需的格式
+          // getMaterialFullList返回的是SalesOrderItemListRecord[]数组
+          this.materialOptions = response.data.data.map(item => ({
+            id: item.id,
+            itemId: item.Item_ID,
+            itemCode: item.Item_Code,
+            itemName: item.Item_Name,
+            specs: item.Item_PECS || '',
+            unit: item.InventoryInfo_Name,
+            mainItemCategoryName: item.MainItemCategory_Name,
+            mainItemCategoryId: item.MainItemCategory_ID,
+            mainItemCategoryCode: item.MainItemCategory_Code,
+            unitPrice: item.Item_Price || '0',
+            description: item.Item_Description || '',
+            warehouseId: item.Warehouse_ID,
+            warehouseCode: item.Warehouse_Code,
+            warehouseName: item.Warehouse_Name,
+            orgId: item.ORG_ID,
+            orgCode: item.ORG_CODE,
+            orgName: item.ORG_NAME,
+            // 保留原始数据以备后用
+            _raw: item
+          }))
+        } else {
+          this.materialOptions = []
+          const errorMsg = response?.data?.msg || '搜索物料失败'
+          this.$message.warning(errorMsg)
+        }
+      } catch (error) {
+        this.materialOptions = []
+        this.$message.error('网络错误,搜索物料失败')
+      } finally {
+        this.materialLoading = false
+      }
+    },
+
+    /**
+     * 处理导入选中物料
+     * @description 将选中的物料导入到物料明细表中
+     * @returns {void}
+     */
+    handleImportSelectedMaterial() {
+      if (!this.selectedMaterialId) {
+        this.$message.warning('请先选择要导入的物料')
+        return
+      }
+
+      // 查找选中的物料数据
+      const selectedMaterial = this.materialOptions.find(item => item.id === this.selectedMaterialId)
+      if (!selectedMaterial) {
+        this.$message.warning('未找到选中的物料数据')
+        return
+      }
+
+      // 检查是否已存在相同物料
+      const existingMaterial = this.materialDetails.find(item => item.itemCode === selectedMaterial.itemCode)
+      if (existingMaterial) {
+        this.$message.warning(`物料 ${selectedMaterial.itemName} 已存在,请勿重复导入`)
+        return
+      }
+
+      // 构造物料明细数据
+      let materialDetail = this.prepareMaterialDetailData(selectedMaterial)
+
+      // 导入时自动计算金额
+      materialDetail = this.calculateAmounts(materialDetail)
+
+      // 触发导入事件
+      this.$emit(MATERIAL_DETAIL_EVENTS.MATERIAL_IMPORT, [materialDetail])
+      this.$emit(MATERIAL_DETAIL_EVENTS.REFRESH)
+
+      // 清空选择
+      this.selectedMaterialId = null
+      this.materialOptions = []
+    },
+
+    /**
+     * 准备物料明细数据
+     * @description 将选中的物料数据转换为物料明细表所需的格式
+     * @param {import('@/api/types/order').SalesOrderItemListRecord} material - 物料数据(来自getMaterialFullList API)
+     * @returns {MaterialDetailRecord} 格式化后的物料明细数据
+     * @private
+     */
+    prepareMaterialDetailData(material) {
+      return {
+        itemId: material.itemId,
+        itemCode: material.itemCode,
+        itemName: material.itemName,
+        specs: material.specs || '',
+        specification: material.specs || '',
+        unit: material.unit || '',
+        mainItemCategoryName: material.mainItemCategoryName || material.MainItemCategory_Name || '',
+        mainItemCategoryId: material.mainItemCategoryId || material.MainItemCategory_ID,
+        mainItemCategoryCode: material.mainItemCategoryCode || material.MainItemCategory_Code || '',
+        description: material.description || '',
+        warehouseId: material.warehouseId,
+        warehouseCode: material.warehouseCode,
+        warehouseName: material.warehouseName,
+        orgId: material.orgId,
+        orgCode: material.orgCode,
+        orgName: material.orgName,
+        unitPrice: material.unitPrice || 0,
+        orderQuantity: 1,
+        confirmQuantity: 1,
+        availableQuantity: 0,
+        taxRate: 0,
+        taxAmount: 0,
+        totalAmount: 0,
+        itemStatus: MaterialDetailStatus.UNCONFIRMED,
+        dataSource: MaterialDetailDataSource.IMPORTED,
+        isDeletable: true,
+        remark: ''
+      }
+    },
+
+    /**
+     * 生成唯一ID
+     * @description 生成物料明细的唯一标识符
+     * @returns {string} 唯一ID
+     * @private
+     */
+    generateUniqueId() {
+      return 'material_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
+    },
+
+    /**
+     * 处理表格刷新事件
+     * @description 触发刷新事件,通知父组件重新加载数据
+     * @returns {void}
+     * @emits refresh
+     */
+    handleRefresh() {
+      this.$emit(MATERIAL_DETAIL_EVENTS.REFRESH)
+    },
+
+
+
+    /**
+     * 处理分页页码变化事件
+     * @description 当用户切换页码时触发,更新当前页码
+     * @param {number} currentPage - 新的页码,从1开始
+     * @returns {void}
+     */
+    handleCurrentChange(currentPage) {
+      this.page.currentPage = currentPage
+    },
+
+    /**
+     * 处理分页大小变化事件
+     * @description 当用户改变每页显示条数时触发,重置到第一页
+     * @param {number} pageSize - 新的每页显示条数
+     * @returns {void}
+     */
+    handleSizeChange(pageSize) {
+      this.page.pageSize = pageSize
+      this.page.currentPage = 1
+    },
+
+    /**
+     * 格式化浮点数显示
+     * @description 格式化浮点数为4位小数的字符串
+     * @param {number|string|null|undefined} value - 数值
+     * @returns {string} 格式化后的字符串
+     */
+    formatFloatNumber(value) {
+      return formatFloatNumber(value)
+    },
+
+    /**
+     * 格式化金额显示
+     * @description 格式化金额为带货币符号的字符串
+     * @param {number|string|null|undefined} amount - 金额数值
+     * @param {boolean} withSymbol - 是否显示货币符号
+     * @returns {string} 格式化后的金额字符串
+     */
+    formatAmount(amount, withSymbol = true) {
+      return formatAmount(amount, withSymbol)
+    },
+
+    formatUnitPrice,
+    formatTaxRate,
+
+    /**
+     * 格式化整数显示
+     * @description 格式化整数为字符串
+     * @param {number|string|null|undefined} value - 整数数值
+     * @returns {string} 格式化后的整数字符串
+     */
+    formatIntegerNumber(value) {
+      return formatIntegerNumber(value)
+    },
+
+    /**
+     * 获取状态标签类型
+     * @description 根据物料明细状态值返回对应的Element UI标签类型
+     * @param {typeof MaterialDetailStatus[keyof typeof MaterialDetailStatus]} itemStatus - 物料明细状态值
+     * @returns {string} Element UI标签类型
+     * @example
+     * getStatusTagType(0) // 返回 'warning'
+     * getStatusTagType(1) // 返回 'success'
+     */
+    getStatusTagType(itemStatus) {
+      return getMaterialDetailStatusTagType(itemStatus)
+    },
+
+    /**
+     * 获取状态文本
+     * @description 根据物料明细状态值返回对应的中文描述
+     * @param {number} itemStatus - 物料明细状态值
+     * @returns {string} 状态的中文描述文本
+     * @example
+     * getStatusText(0) // 返回 '待确认'
+     * getStatusText(1) // 返回 '已确认'
+     */
+    getStatusText(itemStatus) {
+      return getMaterialDetailStatusLabel(itemStatus)
+    },
+
+    /**
+      * 处理删除物料操作
+      * @description 删除指定的物料明细记录,仅允许删除可删除的物料
+      * @param {MaterialDetailRecord} row - 要删除的物料明细记录
+      * @param {number} index - 记录在当前页的索引位置
+      * @returns {void}
+      * @emits material-delete
+      */
+     async handleDeleteMaterial(row, index) {
+       try {
+         await this.$confirm(
+           `确定要删除物料 "${row.itemName}" 吗?`,
+           '删除确认',
+           {
+             confirmButtonText: '确定',
+             cancelButtonText: '取消',
+             type: 'warning'
+           }
+         )
+
+         // 触发删除事件,传递物料记录和索引
+         this.$emit(MATERIAL_DETAIL_EVENTS.MATERIAL_DELETE, { row, index })
+         this.$message.success('物料删除成功')
+       } catch (error) {
+         // 用户取消删除操作
+         if (error !== 'cancel') {
+           this.$message.error('删除操作失败')
+         }
+       }
+     },
+
+    /**
+     * 处理表格行删除事件
+     * @description AvueJS表格的删除事件处理器,委托给自定义删除方法
+     * @param {MaterialDetailRecord} row - 要删除的行数据
+     * @param {number} index - 行索引
+     * @returns {void}
+     */
+    handleRowDelete(row, index) {
+      this.handleDeleteMaterial(row, index)
+    },
+
+    /**
+     * 处理行更新事件
+     * @description 当用户编辑表格行数据时触发,执行自动计算逻辑
+     * @param {MaterialDetailRecord} row - 更新后的行数据
+     * @param {number} index - 行索引
+     * @param {boolean} done - 完成回调函数
+     * @returns {void}
+     * @emits material-update
+     */
+    async handleRowUpdate(row, index, done) {
+      try {
+        // 执行自动计算
+        const calculatedRow = this.calculateAmounts(row)
+
+        // 触发更新事件,传递计算后的数据
+        this.$emit(MATERIAL_DETAIL_EVENTS.MATERIAL_UPDATE, { row: calculatedRow, index })
+
+        // 完成编辑
+        done(calculatedRow)
+
+        this.$message.success('物料明细更新成功')
+      } catch (error) {
+        this.$message.error('更新失败:' + error.message)
+        done(false)
+      }
+    },
+
+    /**
+     * 处理订单数量失焦事件
+     * @description 当订单数量输入框失焦时,触发数量变更处理
+     * @param {MaterialDetailRecord} row - 行数据
+     * @param {number} index - 行索引
+     * @returns {void}
+     */
+    handleQuantityBlur(row, index) {
+      // 如果 index 无效,尝试通过 row 数据找到正确的索引
+      const actualIndex = this.findRowIndex(row, index)
+      this.handleQuantityChange(row, actualIndex)
+    },
+
+    /**
+     * 处理订单数量变更
+     * @description 当订单数量发生变化时,自动计算总金额和税额,并触发父组件重新计算订单总计
+     * @param {MaterialDetailRecord} row - 行数据
+     * @param {number} index - 行索引
+     * @returns {void}
+     */
+    handleQuantityChange(row, index) {
+      const calculatedRow = this.calculateAmounts(row)
+      Object.assign(row, calculatedRow)
+      this.$emit(MATERIAL_DETAIL_EVENTS.MATERIAL_UPDATE, { row, index })
+    },
+
+    /**
+     * 处理税率变更
+     * @description 当税率发生变化时,重新计算税额,并触发父组件重新计算订单总计
+     * @param {MaterialDetailRecord} row - 行数据
+     * @param {number} index - 行索引
+     * @returns {void}
+     */
+    handleTaxRateChange(row, index) {
+      this.calculateTaxAmount(row)
+      this.$emit('material-update', { row, index })
+    },
+
+    /**
+     * 处理单价失焦事件
+     * @description 当单价输入框失焦时,先格式化数值,再计算总金额和税额,并触发父组件重新计算订单总计
+     * @param {MaterialDetailRecord} row - 行数据
+     * @param {number} index - 行索引
+     * @returns {void}
+     */
+    handleUnitPriceBlur(row, index) {
+      // 先格式化数值
+      this.validateAndFormatFloatOnBlur(row, 'unitPrice')
+      // 如果 index 无效,尝试通过 row 数据找到正确的索引
+      const actualIndex = this.findRowIndex(row, index)
+      // 再处理单价变更
+      this.handleUnitPriceChange(row, actualIndex)
+    },
+
+    /**
+     * 处理单价变更
+     * @description 当单价发生变化时,自动计算总金额和税额,并触发父组件重新计算订单总计
+     * @param {MaterialDetailRecord} row - 行数据
+     * @param {number} index - 行索引
+     * @returns {void}
+     */
+    handleUnitPriceChange(row, index) {
+      const calculatedRow = this.calculateAmounts(row)
+      Object.assign(row, calculatedRow)
+      this.$emit('material-update', { row, index })
+    },
+
+    /**
+     * 处理税额变更
+     * @description 当税额手动修改时,反推税率,并触发父组件重新计算订单总计
+     * @param {MaterialDetailRecord} row - 行数据
+     * @param {number} index - 行索引
+     * @returns {void}
+     */
+    handleTaxAmountChange(row, index) {
+      // 当税额手动修改时,反推税率
+      if (row.totalAmount && row.totalAmount > 0) {
+        row.taxRate = ((row.taxAmount || 0) / row.totalAmount * 100).toFixed(2)
+      }
+      this.$emit('material-update', { row, index })
+    },
+
+    /**
+     * 处理总金额变更
+     * @description 当总金额手动修改时,重新计算税额,并触发父组件重新计算订单总计
+     * @param {MaterialDetailRecord} row - 行数据
+     * @param {number} index - 行索引
+     * @returns {void}
+     */
+    handleTotalAmountChange(row, index) {
+      // 当总金额手动修改时,重新计算税额
+      this.calculateTaxAmount(row)
+      this.$emit('material-update', { row, index })
+    },
+
+    /**
+     * 处理单元格编辑开始事件
+     * @description 当用户开始编辑单元格时触发
+     * @param {MaterialDetailRecord} row - 编辑的行数据
+     * @param {string} prop - 编辑的属性名
+     * @param {*} value - 当前值
+     * @returns {void}
+     */
+    handleCellEditStart(row, prop, value) {
+      // 记录编辑前的值,用于计算变化
+      this.editingRow = { ...row }
+      this.editingProp = prop
+    },
+
+    /**
+     * 处理单元格编辑结束事件
+     * @description 当用户结束编辑单元格时触发,执行实时计算
+     * @param {MaterialDetailRecord} row - 编辑后的行数据
+     * @param {string} prop - 编辑的属性名
+     * @param {*} value - 新值
+     * @returns {void}
+     */
+    handleCellEditEnd(row, prop, value) {
+      // 如果编辑的是影响计算的字段,执行自动计算
+      if (['orderQuantity', 'unitPrice', 'taxRate'].includes(prop)) {
+        const calculatedRow = this.calculateAmounts(row)
+
+        // 更新行数据
+        Object.assign(row, calculatedRow)
+
+        // 触发更新事件
+        this.$emit(MATERIAL_DETAIL_EVENTS.MATERIAL_UPDATE, { row: calculatedRow, index: this.getCurrentRowIndex(row) })
+      }
+    },
+
+    /**
+     * 自动计算金额
+     * @description 根据订单数量、单价和税率自动计算总金额和税额,使用精确计算避免浮点数精度问题
+     * @param {MaterialDetailRecord} row - 物料明细记录
+     * @returns {MaterialDetailRecord} 计算后的物料明细记录
+     */
+    calculateAmounts(row) {
+      const calculatedRow = { ...row }
+
+      // 验证并获取数值
+      const quantityValidation = validateNumber(calculatedRow.orderQuantity)
+      const priceValidation = validateNumber(calculatedRow.unitPrice)
+      const rateValidation = validateNumber(calculatedRow.taxRate)
+
+      const orderQuantity = quantityValidation.isValid ? Math.round(quantityValidation.value) : 0
+      const unitPrice = priceValidation.isValid ? priceValidation.value : 0
+      const taxRate = rateValidation.isValid ? rateValidation.value : 0
+
+      // 使用精确计算:订单数量 * 单价
+      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
+    },
+
+    /**
+     * 计算税额
+     * @description 根据总金额和税率计算税额,使用精确计算避免浮点数精度问题
+     * @param {MaterialDetailRecord} row - 物料明细记录
+     * @returns {void}
+     */
+    calculateTaxAmount(row) {
+      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
+      }
+    },
+
+    /**
+     * 获取当前行索引
+     * @description 根据行数据获取在当前页中的索引
+     * @param {MaterialDetailRecord} row - 行数据
+     * @returns {number} 行索引
+     */
+    getCurrentRowIndex(row) {
+      return this.currentPageData.findIndex(item => item.id === row.id)
+    },
+
+    /**
+      * 查找行索引
+      * @description 根据行数据查找在物料明细列表中的正确索引
+      * @param {MaterialDetailRecord} row - 行数据
+      * @param {number} providedIndex - 提供的索引
+      * @returns {number} 实际索引
+      */
+     findRowIndex(row, providedIndex) {
+       // 如果提供的索引有效,直接使用
+       if (providedIndex >= 0 && providedIndex < this.materialDetails.length) {
+         return providedIndex
+       }
+
+       // 否则通过行数据查找索引
+       const index = this.materialDetails.findIndex(item => {
+         // 优先使用 id 进行匹配
+         if (row.id && item.id) {
+           return row.id === item.id
+         }
+         // 如果没有 id,使用物料编码进行匹配
+         if (row.itemCode && item.itemCode) {
+           return row.itemCode === item.itemCode
+         }
+         // 最后使用对象引用进行匹配
+         return row === item
+       })
+
+       return index >= 0 ? index : -1
+     }
+  },
+
+  /**
+   * 组件销毁前的清理工作
+   * @description 清除定时器,避免内存泄漏
+   */
+  beforeDestroy() {
+    if (this.searchTimer) {
+      clearTimeout(this.searchTimer)
+      this.searchTimer = null
+    }
+  }
+}

+ 2 - 899
src/components/order-form/material-detail-table.vue

@@ -181,908 +181,11 @@
 </template>
 
 <script>
-/**
- * @fileoverview 物料明细表格组件
- * @description 基于AvueJS的物料明细数据展示和操作组件,支持分页、搜索、删除等功能
- * @typedef {import('./types').MaterialDetailTableComponent} MaterialDetailTableComponent
- */
+import materialDetailMixin from './material-detail-mixin';
 
-import { getMaterialDetailOption, DEFAULT_PAGINATION_CONFIG } from './material-detail-option'
-import {
-  MaterialDetailStatus,
-  getOrderItemStatusLabel as getMaterialDetailStatusLabel,
-  getOrderItemStatusTagType as getMaterialDetailStatusTagType,
-  getOrderItemStatusColor as getMaterialDetailStatusColor,
-  MaterialDetailDataSource
-} from '@/constants/order'
-import { MATERIAL_DETAIL_EVENTS, DIALOG_EVENTS } from './events'
-import { getMaterialFullList } from '@/api/order/sales-order'
-import {
-  formatAmount,
-  formatFloatNumber,
-  formatIntegerNumber,
-  formatUnitPrice,
-  formatTaxRate,
-  preciseMultiply,
-  preciseDivide,
-  preciseRound,
-  validateNumber,
-  NUMBER_TYPES
-} from './number-format-utils'
-
-/**
- * @typedef {import('./types').MaterialDetailRecord} MaterialDetailRecord
- * @typedef {import('./types').MaterialUpdateEventData} MaterialUpdateEventData
- * @typedef {import('./types').MaterialDeleteEventData} MaterialDeleteEventData
- * @typedef {import('./types').MaterialDetailQueryParams} MaterialDetailQueryParams
- * @typedef {import('smallwei__avue/crud').AvueCrudOption} AvueCrudOption
- * @typedef {import('smallwei__avue/crud').AvueCrudColumn} AvueCrudColumn
- * @typedef {import('smallwei__avue/crud').PageOption} PageOption
- */
-
-
-
-// 使用@types/smallwei__avue/crud中的PageOption类型代替PaginationConfig
-
-/**
- * 组件数据类型定义
- * @typedef {Object} MaterialDetailTableData
- * @property {Partial<MaterialDetailRecord>} formData - 表单数据
- * @property {PageOption} page - 分页配置
- * @property {boolean} importDialogVisible - 导入弹窗显示状态
- */
-
-// 状态处理已统一使用明细管理中的工具函数,无需本地映射常量
-
-/**
- * 物料明细表格组件
- * @description 用于展示和编辑订单的物料明细信息,支持物料导入和实时编辑功能
- * 当物料数量、单价、税率等字段变更时,自动计算总金额和税额,并触发父组件重新计算订单总计
- * @emits {MaterialDetailRecord[]} material-import - 物料导入事件
- * @emits {Object} material-update - 物料明细更新事件,包含更新的行数据和索引
- * @emits {Object} material-delete - 物料明细删除事件
- * @emits {void} refresh - 刷新事件
- */
 export default {
   name: 'MaterialDetailTable',
-  components: {},
-
-  /**
-   * 组件属性定义
-   * @description 定义组件接收的外部属性
-   */
-  props: {
-    /**
-     * 是否为编辑模式 - 控制表格是否可编辑
-     * @type {boolean}
-     */
-    editMode: {
-      type: Boolean,
-      default: false
-    },
-    /**
-     * 订单ID - 关联的订单唯一标识符
-     * @type {string|number|null}
-     */
-    orderId: {
-      type: [String, Number],
-      default: null,
-      validator: (value) => value === null || value === undefined || (typeof value === 'string' && value.length > 0) || (typeof value === 'number' && value > 0)
-    },
-
-    /**
-     * 物料明细列表 - 要展示的物料明细数据
-     * @type {MaterialDetailRecord[]} 物料明细数据数组,每个元素包含物料的详细信息
-     */
-    materialDetails: {
-      type: Array,
-      required: true,
-      default: () => [],
-      validator: (value) => Array.isArray(value) && value.every(item =>
-        typeof item === 'object' && item !== null &&
-        typeof item.id === 'string' &&
-        typeof item.itemCode === 'string'
-      )
-    }
-  },
-
-  /**
-   * 组件数据
-   * @returns {MaterialDetailTableData} 组件响应式数据对象
-   * @this {MaterialDetailTableComponent}
-   */
-  data() {
-    return {
-      /**
-       * 表单数据 - 当前编辑行的数据
-       * @type {Partial<MaterialDetailRecord>} 物料明细表单数据对象
-       */
-      formData: {},
-
-      /**
-       * 分页配置 - AvueJS表格分页相关配置
-       * @type {PaginationConfig} 包含currentPage、pageSize、total等属性的分页配置对象
-       */
-      page: {
-        currentPage: 1,
-        pageSize: DEFAULT_PAGINATION_CONFIG.pageSize,
-        total: 0
-      },
-
-      /**
-       * 选中的物料ID - 当前在下拉框中选中的物料ID
-       * @type {string|null}
-       */
-      selectedMaterialId: null,
-
-      /**
-       * 物料选项列表 - 远程搜索返回的物料选项
-       * @type {ItemRecord[]}
-       */
-      materialOptions: [],
-
-      /**
-       * 物料搜索加载状态 - 控制远程搜索时的加载状态
-       * @type {boolean}
-       */
-      materialLoading: false,
-
-      /**
-       * 搜索防抖定时器 - 用于防抖处理远程搜索
-       * @type {number|null}
-       */
-      searchTimer: null,
-
-      /**
-       * 事件常量
-       */
-      DIALOG_EVENTS,
-
-      /**
-       * 正在编辑的行数据 - 用于记录编辑前的状态
-       * @type {MaterialDetailRecord|null}
-       */
-      editingRow: null,
-
-      /**
-       * 正在编辑的属性名 - 用于记录当前编辑的字段
-       * @type {string|null}
-       */
-      editingProp: null
-    }
-  },
-
-  /**
-   * 计算属性
-   * @this {MaterialDetailTableComponent}
-   */
-  computed: {
-    /**
-     * 表格配置选项 - 获取AvueJS表格的配置对象
-     * @returns {AvueCrudOption} AvueJS表格配置对象,根据编辑模式配置
-     */
-    tableOption() {
-      return getMaterialDetailOption(this.editMode)
-    },
-
-    /**
-     * 当前页显示的数据 - 根据分页配置计算当前页应显示的数据
-     * @returns {MaterialDetailRecord[]} 当前页的物料明细数据
-     */
-    currentPageData() {
-      const { currentPage, pageSize } = this.page
-      const startIndex = (currentPage - 1) * pageSize
-      const endIndex = startIndex + pageSize
-      return this.materialDetails.slice(startIndex, endIndex)
-    }
-  },
-
-  /**
-   * 监听器
-   * @this {MaterialDetailTableComponent}
-   */
-  watch: {
-    /**
-     * 监听物料明细变化
-     * @param {MaterialDetailRecord[]} newVal - 新的物料明细列表
-     * @returns {void}
-     */
-    materialDetails: {
-      handler(newVal) {
-        this.page.total = newVal.length
-      },
-      immediate: true
-    }
-  },
-
-  /**
-   * 组件方法
-   * @this {MaterialDetailTableComponent}
-   */
-  methods: {
-    /**
-       * 验证整数输入
-       * @param {string} value - 输入值
-       * @param {Object} row - 当前行数据
-       * @param {string} field - 字段名
-       * @param {number} min - 最小值
-       * @param {number} max - 最大值
-       */
-      validateIntegerInput(value, row, field, min = 0, max = 999999) {
-        // 允许空值和部分输入(如正在输入的数字)
-        if (value === '' || value === '-') {
-          return
-        }
-
-        // 移除所有非数字字符(除了负号)
-        let cleanValue = value.replace(/[^-\d]/g, '')
-
-        // 确保负号只能在开头
-        if (cleanValue.indexOf('-') > 0) {
-          cleanValue = cleanValue.replace(/-/g, '')
-        }
-
-        // 如果有值,转换为整数
-        if (cleanValue !== '' && cleanValue !== '-') {
-          const numValue = parseInt(cleanValue, 10)
-
-          // 检查范围
-          if (numValue < min) {
-            row[field] = min
-          } else if (numValue > max) {
-            row[field] = max
-          } else {
-            row[field] = numValue
-          }
-        }
-      },
-
-      /**
-       * 验证浮点数输入(输入时验证)
-       * @param {string} value - 输入值
-       * @param {Object} row - 当前行数据
-       * @param {string} field - 字段名
-       * @param {number} min - 最小值
-       * @param {number} max - 最大值
-       */
-      validateFloatInput(value, row, field, min = 0, max = 999999.99) {
-        // 允许空值和部分输入(包括单独的小数点、负号等)
-        if (value === '' || value === '-' || value === '.' || value === '-.') {
-          row[field] = value
-          return
-        }
-
-        // 移除无效字符,只保留数字、小数点和负号
-        let cleanValue = value.replace(/[^-\d.]/g, '')
-
-        // 确保负号只能在开头
-        if (cleanValue.indexOf('-') > 0) {
-          cleanValue = cleanValue.replace(/-/g, '')
-        }
-
-        // 确保只有一个小数点
-        const parts = cleanValue.split('.')
-        if (parts.length > 2) {
-          cleanValue = parts[0] + '.' + parts.slice(1).join('')
-        }
-
-        // 限制小数位数为2位(但允许继续输入)
-        if (parts.length === 2 && parts[1].length > 2) {
-          cleanValue = parts[0] + '.' + parts[1].substring(0, 2)
-        }
-
-        // 更新字段值,但不进行范围检查(留到blur时处理)
-        row[field] = cleanValue
-      },
-
-      /**
-     * 验证并格式化浮点数(失焦时验证)
-     * @param {Object} row - 当前行数据
-     * @param {string} field - 字段名
-     * @param {number} min - 最小值
-     * @param {number} max - 最大值
-     * @param {number} precision - 小数位数,默认2位
-     */
-    validateAndFormatFloatOnBlur(row, field, min = 0, max = 999999.99, precision = 2) {
-      const value = row[field]
-
-      // 如果是空值或无效输入,设置为最小值
-      if (value === '' || value === '.' || value === '-' || value === '-.' || isNaN(parseFloat(value))) {
-        row[field] = min
-        return
-      }
-
-      const numValue = parseFloat(value)
-
-      // 范围检查
-      if (numValue < min) {
-        row[field] = min
-      } else if (numValue > max) {
-        row[field] = max
-      } else {
-        // 格式化为指定小数位数
-        const multiplier = Math.pow(10, precision)
-        const roundedValue = Math.round(numValue * multiplier) / multiplier
-        row[field] = roundedValue
-      }
-    },
-
-    /**
-     * 判断行是否可编辑
-     * @description 根据数据来源判断物料明细行是否允许编辑,远程数据(订单ID获取)不可编辑
-     * @param {MaterialDetailRecord} row - 物料明细行数据
-     * @returns {boolean} 是否可编辑,true表示可编辑,false表示不可编辑
-     */
-    isRowEditable(row) {
-      // 如果没有数据来源信息,默认可编辑
-      if (!row || !row.dataSource) {
-        return true
-      }
-
-      // 只有导入的物料可以编辑,远程数据(订单ID获取)不可编辑
-      return row.dataSource === MaterialDetailDataSource.IMPORTED
-    },
-
-    /**
-     * 远程搜索物料
-     * @description 根据关键词远程搜索物料数据,支持防抖处理
-     * @param {string} query - 搜索关键词
-     * @returns {void}
-     */
-    remoteSearchMaterial(query) {
-      // 清除之前的定时器
-      if (this.searchTimer) {
-        clearTimeout(this.searchTimer)
-      }
-
-      // 如果查询为空,清空选项
-      if (!query) {
-        this.materialOptions = []
-        return
-      }
-
-      // 设置防抖定时器
-      this.searchTimer = setTimeout(async () => {
-        await this.searchMaterials(query)
-      }, 300)
-    },
-
-    /**
-     * 搜索物料数据
-     * @description 调用API搜索物料数据
-     * @param {string} keyword - 搜索关键词
-     * @returns {Promise<void>}
-     * @throws {Error} 当API调用失败时抛出异常
-     */
-    async searchMaterials(keyword) {
-      try {
-        this.materialLoading = true
-
-        const response = await getMaterialFullList({
-          itemName: keyword
-        })
-
-        if (response?.data?.success && response.data.data) {
-          // 转换API返回的字段名称为组件所需的格式
-          // getMaterialFullList返回的是SalesOrderItemListRecord[]数组
-          this.materialOptions = response.data.data.map(item => ({
-            id: item.id,
-            itemId: item.Item_ID,
-            itemCode: item.Item_Code,
-            itemName: item.Item_Name,
-            specs: item.Item_PECS || '',
-            unit: item.InventoryInfo_Name,
-            mainItemCategoryName: item.MainItemCategory_Name,
-            mainItemCategoryId: item.MainItemCategory_ID,
-            mainItemCategoryCode: item.MainItemCategory_Code,
-            unitPrice: item.Item_Price || '0',
-            description: item.Item_Description || '',
-            warehouseId: item.Warehouse_ID,
-            warehouseCode: item.Warehouse_Code,
-            warehouseName: item.Warehouse_Name,
-            orgId: item.ORG_ID,
-            orgCode: item.ORG_CODE,
-            orgName: item.ORG_NAME,
-            // 保留原始数据以备后用
-            _raw: item
-          }))
-        } else {
-          this.materialOptions = []
-          const errorMsg = response?.data?.msg || '搜索物料失败'
-          this.$message.warning(errorMsg)
-        }
-      } catch (error) {
-        this.materialOptions = []
-        this.$message.error('网络错误,搜索物料失败')
-      } finally {
-        this.materialLoading = false
-      }
-    },
-
-    /**
-     * 处理导入选中物料
-     * @description 将选中的物料导入到物料明细表中
-     * @returns {void}
-     */
-    handleImportSelectedMaterial() {
-      if (!this.selectedMaterialId) {
-        this.$message.warning('请先选择要导入的物料')
-        return
-      }
-  
-      // 查找选中的物料数据
-      const selectedMaterial = this.materialOptions.find(item => item.id === this.selectedMaterialId)
-      if (!selectedMaterial) {
-        this.$message.warning('未找到选中的物料数据')
-        return
-      }
-  
-      // 检查是否已存在相同物料
-      const existingMaterial = this.materialDetails.find(item => item.itemCode === selectedMaterial.itemCode)
-      if (existingMaterial) {
-        this.$message.warning(`物料 ${selectedMaterial.itemName} 已存在,请勿重复导入`)
-        return
-      }
-  
-      // 构造物料明细数据
-      let materialDetail = this.prepareMaterialDetailData(selectedMaterial)
-      
-      // 导入时自动计算金额
-      materialDetail = this.calculateAmounts(materialDetail)
-  
-      // 触发导入事件
-      this.$emit(MATERIAL_DETAIL_EVENTS.MATERIAL_IMPORT, [materialDetail])
-      this.$emit(MATERIAL_DETAIL_EVENTS.REFRESH)
-  
-      // 清空选择
-      this.selectedMaterialId = null
-      this.materialOptions = []
-    },
-
-    /**
-     * 准备物料明细数据
-     * @description 将选中的物料数据转换为物料明细表所需的格式
-     * @param {import('@/api/types/order').SalesOrderItemListRecord} material - 物料数据(来自getMaterialFullList API)
-     * @returns {MaterialDetailRecord} 格式化后的物料明细数据
-     * @private
-     */
-    prepareMaterialDetailData(material) {
-      return {
-        id: this.generateUniqueId(),
-        itemId: material.itemId,
-        itemCode: material.itemCode,
-        itemName: material.itemName,
-        specs: material.specs || '',
-        specification: material.specs || '',
-        unit: material.unit || '',
-        mainItemCategoryName: material.mainItemCategoryName || material.MainItemCategory_Name || '',
-        mainItemCategoryId: material.mainItemCategoryId || material.MainItemCategory_ID,
-        mainItemCategoryCode: material.mainItemCategoryCode || material.MainItemCategory_Code || '',
-        description: material.description || '',
-        warehouseId: material.warehouseId,
-        warehouseCode: material.warehouseCode,
-        warehouseName: material.warehouseName,
-        orgId: material.orgId,
-        orgCode: material.orgCode,
-        orgName: material.orgName,
-        unitPrice: material.unitPrice || 0,
-        orderQuantity: 1,
-        confirmQuantity: 1,
-        availableQuantity: 0,
-        taxRate: 0,
-        taxAmount: 0,
-        totalAmount: 0,
-        itemStatus: MaterialDetailStatus.UNCONFIRMED,
-        dataSource: MaterialDetailDataSource.IMPORTED,
-        isDeletable: true,
-        remark: ''
-      }
-    },
-
-    /**
-     * 生成唯一ID
-     * @description 生成物料明细的唯一标识符
-     * @returns {string} 唯一ID
-     * @private
-     */
-    generateUniqueId() {
-      return 'material_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
-    },
-
-    /**
-     * 处理表格刷新事件
-     * @description 触发刷新事件,通知父组件重新加载数据
-     * @returns {void}
-     * @emits refresh
-     */
-    handleRefresh() {
-      this.$emit(MATERIAL_DETAIL_EVENTS.REFRESH)
-    },
-
-
-
-    /**
-     * 处理分页页码变化事件
-     * @description 当用户切换页码时触发,更新当前页码
-     * @param {number} currentPage - 新的页码,从1开始
-     * @returns {void}
-     */
-    handleCurrentChange(currentPage) {
-      this.page.currentPage = currentPage
-    },
-
-    /**
-     * 处理分页大小变化事件
-     * @description 当用户改变每页显示条数时触发,重置到第一页
-     * @param {number} pageSize - 新的每页显示条数
-     * @returns {void}
-     */
-    handleSizeChange(pageSize) {
-      this.page.pageSize = pageSize
-      this.page.currentPage = 1
-    },
-
-    /**
-     * 格式化浮点数显示
-     * @description 格式化浮点数为4位小数的字符串
-     * @param {number|string|null|undefined} value - 数值
-     * @returns {string} 格式化后的字符串
-     */
-    formatFloatNumber(value) {
-      return formatFloatNumber(value)
-    },
-
-    /**
-     * 格式化金额显示
-     * @description 格式化金额为带货币符号的字符串
-     * @param {number|string|null|undefined} amount - 金额数值
-     * @param {boolean} withSymbol - 是否显示货币符号
-     * @returns {string} 格式化后的金额字符串
-     */
-    formatAmount(amount, withSymbol = true) {
-      return formatAmount(amount, withSymbol)
-    },
-
-    formatUnitPrice,
-    formatTaxRate,
-
-    /**
-     * 格式化整数显示
-     * @description 格式化整数为字符串
-     * @param {number|string|null|undefined} value - 整数数值
-     * @returns {string} 格式化后的整数字符串
-     */
-    formatIntegerNumber(value) {
-      return formatIntegerNumber(value)
-    },
-
-    /**
-     * 获取状态标签类型
-     * @description 根据物料明细状态值返回对应的Element UI标签类型
-     * @param {typeof MaterialDetailStatus[keyof typeof MaterialDetailStatus]} itemStatus - 物料明细状态值
-     * @returns {string} Element UI标签类型
-     * @example
-     * getStatusTagType(0) // 返回 'warning'
-     * getStatusTagType(1) // 返回 'success'
-     */
-    getStatusTagType(itemStatus) {
-      return getMaterialDetailStatusTagType(itemStatus)
-    },
-
-    /**
-     * 获取状态文本
-     * @description 根据物料明细状态值返回对应的中文描述
-     * @param {number} itemStatus - 物料明细状态值
-     * @returns {string} 状态的中文描述文本
-     * @example
-     * getStatusText(0) // 返回 '待确认'
-     * getStatusText(1) // 返回 '已确认'
-     */
-    getStatusText(itemStatus) {
-      return getMaterialDetailStatusLabel(itemStatus)
-    },
-
-    /**
-      * 处理删除物料操作
-      * @description 删除指定的物料明细记录,仅允许删除可删除的物料
-      * @param {MaterialDetailRecord} row - 要删除的物料明细记录
-      * @param {number} index - 记录在当前页的索引位置
-      * @returns {void}
-      * @emits material-delete
-      */
-     async handleDeleteMaterial(row, index) {
-       try {
-         await this.$confirm(
-           `确定要删除物料 "${row.itemName}" 吗?`,
-           '删除确认',
-           {
-             confirmButtonText: '确定',
-             cancelButtonText: '取消',
-             type: 'warning'
-           }
-         )
-
-         // 触发删除事件,传递物料记录和索引
-         this.$emit(MATERIAL_DETAIL_EVENTS.MATERIAL_DELETE, { row, index })
-         this.$message.success('物料删除成功')
-       } catch (error) {
-         // 用户取消删除操作
-         if (error !== 'cancel') {
-           this.$message.error('删除操作失败')
-         }
-       }
-     },
-
-    /**
-     * 处理表格行删除事件
-     * @description AvueJS表格的删除事件处理器,委托给自定义删除方法
-     * @param {MaterialDetailRecord} row - 要删除的行数据
-     * @param {number} index - 行索引
-     * @returns {void}
-     */
-    handleRowDelete(row, index) {
-      this.handleDeleteMaterial(row, index)
-    },
-
-    /**
-     * 处理行更新事件
-     * @description 当用户编辑表格行数据时触发,执行自动计算逻辑
-     * @param {MaterialDetailRecord} row - 更新后的行数据
-     * @param {number} index - 行索引
-     * @param {boolean} done - 完成回调函数
-     * @returns {void}
-     * @emits material-update
-     */
-    async handleRowUpdate(row, index, done) {
-      try {
-        // 执行自动计算
-        const calculatedRow = this.calculateAmounts(row)
-
-        // 触发更新事件,传递计算后的数据
-        this.$emit(MATERIAL_DETAIL_EVENTS.MATERIAL_UPDATE, { row: calculatedRow, index })
-
-        // 完成编辑
-        done(calculatedRow)
-
-        this.$message.success('物料明细更新成功')
-      } catch (error) {
-        this.$message.error('更新失败:' + error.message)
-        done(false)
-      }
-    },
-
-    /**
-     * 处理订单数量失焦事件
-     * @description 当订单数量输入框失焦时,触发数量变更处理
-     * @param {MaterialDetailRecord} row - 行数据
-     * @param {number} index - 行索引
-     * @returns {void}
-     */
-    handleQuantityBlur(row, index) {
-      // 如果 index 无效,尝试通过 row 数据找到正确的索引
-      const actualIndex = this.findRowIndex(row, index)
-      this.handleQuantityChange(row, actualIndex)
-    },
-
-    /**
-     * 处理订单数量变更
-     * @description 当订单数量发生变化时,自动计算总金额和税额,并触发父组件重新计算订单总计
-     * @param {MaterialDetailRecord} row - 行数据
-     * @param {number} index - 行索引
-     * @returns {void}
-     */
-    handleQuantityChange(row, index) {
-      const calculatedRow = this.calculateAmounts(row)
-      Object.assign(row, calculatedRow)
-      this.$emit(MATERIAL_DETAIL_EVENTS.MATERIAL_UPDATE, { row, index })
-    },
-
-    /**
-     * 处理税率变更
-     * @description 当税率发生变化时,重新计算税额,并触发父组件重新计算订单总计
-     * @param {MaterialDetailRecord} row - 行数据
-     * @param {number} index - 行索引
-     * @returns {void}
-     */
-    handleTaxRateChange(row, index) {
-      this.calculateTaxAmount(row)
-      this.$emit('material-update', { row, index })
-    },
-
-    /**
-     * 处理单价失焦事件
-     * @description 当单价输入框失焦时,先格式化数值,再计算总金额和税额,并触发父组件重新计算订单总计
-     * @param {MaterialDetailRecord} row - 行数据
-     * @param {number} index - 行索引
-     * @returns {void}
-     */
-    handleUnitPriceBlur(row, index) {
-      // 先格式化数值
-      this.validateAndFormatFloatOnBlur(row, 'unitPrice')
-      // 如果 index 无效,尝试通过 row 数据找到正确的索引
-      const actualIndex = this.findRowIndex(row, index)
-      // 再处理单价变更
-      this.handleUnitPriceChange(row, actualIndex)
-    },
-
-    /**
-     * 处理单价变更
-     * @description 当单价发生变化时,自动计算总金额和税额,并触发父组件重新计算订单总计
-     * @param {MaterialDetailRecord} row - 行数据
-     * @param {number} index - 行索引
-     * @returns {void}
-     */
-    handleUnitPriceChange(row, index) {
-      const calculatedRow = this.calculateAmounts(row)
-      Object.assign(row, calculatedRow)
-      this.$emit('material-update', { row, index })
-    },
-
-    /**
-     * 处理税额变更
-     * @description 当税额手动修改时,反推税率,并触发父组件重新计算订单总计
-     * @param {MaterialDetailRecord} row - 行数据
-     * @param {number} index - 行索引
-     * @returns {void}
-     */
-    handleTaxAmountChange(row, index) {
-      // 当税额手动修改时,反推税率
-      if (row.totalAmount && row.totalAmount > 0) {
-        row.taxRate = ((row.taxAmount || 0) / row.totalAmount * 100).toFixed(2)
-      }
-      this.$emit('material-update', { row, index })
-    },
-
-    /**
-     * 处理总金额变更
-     * @description 当总金额手动修改时,重新计算税额,并触发父组件重新计算订单总计
-     * @param {MaterialDetailRecord} row - 行数据
-     * @param {number} index - 行索引
-     * @returns {void}
-     */
-    handleTotalAmountChange(row, index) {
-      // 当总金额手动修改时,重新计算税额
-      this.calculateTaxAmount(row)
-      this.$emit('material-update', { row, index })
-    },
-
-    /**
-     * 处理单元格编辑开始事件
-     * @description 当用户开始编辑单元格时触发
-     * @param {MaterialDetailRecord} row - 编辑的行数据
-     * @param {string} prop - 编辑的属性名
-     * @param {*} value - 当前值
-     * @returns {void}
-     */
-    handleCellEditStart(row, prop, value) {
-      // 记录编辑前的值,用于计算变化
-      this.editingRow = { ...row }
-      this.editingProp = prop
-    },
-
-    /**
-     * 处理单元格编辑结束事件
-     * @description 当用户结束编辑单元格时触发,执行实时计算
-     * @param {MaterialDetailRecord} row - 编辑后的行数据
-     * @param {string} prop - 编辑的属性名
-     * @param {*} value - 新值
-     * @returns {void}
-     */
-    handleCellEditEnd(row, prop, value) {
-      // 如果编辑的是影响计算的字段,执行自动计算
-      if (['orderQuantity', 'unitPrice', 'taxRate'].includes(prop)) {
-        const calculatedRow = this.calculateAmounts(row)
-
-        // 更新行数据
-        Object.assign(row, calculatedRow)
-
-        // 触发更新事件
-        this.$emit(MATERIAL_DETAIL_EVENTS.MATERIAL_UPDATE, { row: calculatedRow, index: this.getCurrentRowIndex(row) })
-      }
-    },
-
-    /**
-     * 自动计算金额
-     * @description 根据订单数量、单价和税率自动计算总金额和税额,使用精确计算避免浮点数精度问题
-     * @param {MaterialDetailRecord} row - 物料明细记录
-     * @returns {MaterialDetailRecord} 计算后的物料明细记录
-     */
-    calculateAmounts(row) {
-      const calculatedRow = { ...row }
-
-      // 验证并获取数值
-      const quantityValidation = validateNumber(calculatedRow.orderQuantity)
-      const priceValidation = validateNumber(calculatedRow.unitPrice)
-      const rateValidation = validateNumber(calculatedRow.taxRate)
-
-      const orderQuantity = quantityValidation.isValid ? Math.round(quantityValidation.value) : 0
-      const unitPrice = priceValidation.isValid ? priceValidation.value : 0
-      const taxRate = rateValidation.isValid ? rateValidation.value : 0
-
-      // 使用精确计算:订单数量 * 单价
-      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
-    },
-
-    /**
-     * 计算税额
-     * @description 根据总金额和税率计算税额,使用精确计算避免浮点数精度问题
-     * @param {MaterialDetailRecord} row - 物料明细记录
-     * @returns {void}
-     */
-    calculateTaxAmount(row) {
-      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
-      }
-    },
-
-    /**
-     * 获取当前行索引
-     * @description 根据行数据获取在当前页中的索引
-     * @param {MaterialDetailRecord} row - 行数据
-     * @returns {number} 行索引
-     */
-    getCurrentRowIndex(row) {
-      return this.currentPageData.findIndex(item => item.id === row.id)
-    },
-
-    /**
-      * 查找行索引
-      * @description 根据行数据查找在物料明细列表中的正确索引
-      * @param {MaterialDetailRecord} row - 行数据
-      * @param {number} providedIndex - 提供的索引
-      * @returns {number} 实际索引
-      */
-     findRowIndex(row, providedIndex) {
-       // 如果提供的索引有效,直接使用
-       if (providedIndex >= 0 && providedIndex < this.materialDetails.length) {
-         return providedIndex
-       }
-
-       // 否则通过行数据查找索引
-       const index = this.materialDetails.findIndex(item => {
-         // 优先使用 id 进行匹配
-         if (row.id && item.id) {
-           return row.id === item.id
-         }
-         // 如果没有 id,使用物料编码进行匹配
-         if (row.itemCode && item.itemCode) {
-           return row.itemCode === item.itemCode
-         }
-         // 最后使用对象引用进行匹配
-         return row === item
-       })
-
-       return index >= 0 ? index : -1
-     }
-  },
-
-  /**
-   * 组件销毁前的清理工作
-   * @description 清除定时器,避免内存泄漏
-   */
-  beforeDestroy() {
-    if (this.searchTimer) {
-      clearTimeout(this.searchTimer)
-      this.searchTimer = null
-    }
-  }
+  mixins: [materialDetailMixin],
 }
 </script>