Преглед изворни кода

feat(订单表单): 添加物料明细表格编辑功能并优化金额计算

yz пре 1 месец
родитељ
комит
7269886234

+ 17 - 17
src/components/order-form/constants.js

@@ -106,14 +106,13 @@ export const DEFAULT_PAGINATION_CONFIG = {
 }
 
 /**
- * 物料明细状态选项列表 - 与明细管理保持一致
- * @type {ReadonlyArray<{readonly label: string, readonly value: 0 | 1 | 2 | 3}>}
+ * 物料明细状态选项列表
+ * @type {ReadonlyArray<{readonly label: string, readonly value: string}>}
  */
 export const MATERIAL_DETAIL_STATUS_OPTIONS = [
-  { label: '未确认', value: MaterialDetailStatus.UNCONFIRMED },
+  { label: '待处理', value: MaterialDetailStatus.PENDING },
   { label: '已确认', value: MaterialDetailStatus.CONFIRMED },
-  { label: '部分发货', value: MaterialDetailStatus.PARTIAL_SHIPPED },
-  { label: '已完成', value: MaterialDetailStatus.COMPLETED }
+  { label: '已取消', value: MaterialDetailStatus.CANCELLED }
 ]
 
 // 导出明细状态工具函数供其他组件使用
@@ -129,24 +128,25 @@ export {
  * 物料明细记录
  * @typedef {Object} MaterialDetailRecord
  * @property {string} id - 记录ID
- * @property {string} materialCode - 物料编码
- * @property {string} materialName - 物料名称
+ * @property {string} itemId - 物料ID
+ * @property {string} itemCode - 物料编码
+ * @property {string} itemName - 物料名称
  * @property {string} specs - 规格型号
  * @property {string} unit - 单位
- * @property {number} quantity - 数量
+ * @property {number} availableQuantity - 可用数量
+ * @property {number} orderQuantity - 订单数量
+ * @property {number} confirmQuantity - 确认数量
  * @property {number} unitPrice - 单价
- * @property {number} totalPrice - 总价
+ * @property {number} taxRate - 税率(%)
+ * @property {number} taxAmount - 税额
+ * @property {number} totalAmount - 总金额
  * @property {string} remark - 备注
  * @property {string} status - 状态
- * @property {string} dataSource - 数据来源
- * @property {string} mainItemCategoryId - 主物料分类ID
+ * @property {string} dataSource - 数据来源 (REMOTE|IMPORTED)
+ * @property {string} mainCategoryId - 主物料分类ID
  * @property {string} mainItemCategoryName - 主物料分类名称
- * @property {string} subItemCategoryId - 子物料分类ID
- * @property {string} subItemCategoryName - 子物料分类名称
- * @property {string} brandId - 品牌ID
- * @property {string} brandName - 品牌名称
- * @property {string} supplierCode - 供应商编码
- * @property {string} supplierName - 供应商名称
+ * @property {string} warehouseId - 仓库ID
+ * @property {string} warehouseName - 仓库名称
  * @property {Date} createTime - 创建时间
  * @property {Date} updateTime - 更新时间
  */

+ 11 - 10
src/components/order-form/material-detail-option.js

@@ -83,7 +83,7 @@ export { MaterialDetailStatus, DEFAULT_PAGINATION_CONFIG, MATERIAL_DETAIL_STATUS
 /**
  * 获取物料明细表格配置
  * @param {boolean} [isEditMode=false] - 是否为编辑模式
- * @returns {Object} AvueJS表格配置对象
+ * @returns {AvueTableOption} AvueJS表格配置对象
  */
 export function getMaterialDetailOption(isEditMode = false) {
   return {
@@ -98,8 +98,10 @@ export function getMaterialDetailOption(isEditMode = false) {
     delBtn: false,
     viewBtn: false,
     searchShow: false,
+    cellBtn: isEditMode,
     page: DEFAULT_PAGINATION_CONFIG,
     menuWidth: 120,
+    rowKey: 'id',
     column: [
       {
         label: '物料编码',
@@ -148,14 +150,10 @@ export function getMaterialDetailOption(isEditMode = false) {
         label: '订单数量',
         prop: 'orderQuantity',
         type: 'number',
-        precision: 2,
+        precision: 4,
         width: 100,
         align: 'right',
-        rules: [{
-          required: true,
-          message: '请输入订单数量',
-          trigger: 'blur'
-        }]
+        slot: true
       },
       {
         label: '确认数量',
@@ -184,7 +182,8 @@ export function getMaterialDetailOption(isEditMode = false) {
         type: 'number',
         precision: 2,
         width: 100,
-        align: 'right'
+        align: 'right',
+        slot: true
       },
       {
         label: '税额',
@@ -192,7 +191,8 @@ export function getMaterialDetailOption(isEditMode = false) {
         type: 'number',
         precision: 2,
         width: 100,
-        align: 'right'
+        align: 'right',
+        slot: true
       },
       {
         label: '总金额',
@@ -200,7 +200,8 @@ export function getMaterialDetailOption(isEditMode = false) {
         type: 'number',
         precision: 2,
         width: 120,
-        align: 'right'
+        align: 'right',
+        slot: true
       },
       {
         label: '明细状态',

+ 331 - 7
src/components/order-form/material-detail-table.vue

@@ -30,7 +30,6 @@
     <div class="material-detail-content">
       <avue-crud
         ref="materialDetailCrud"
-        v-model="formData"
         :data="currentPageData"
         :option="tableOption"
         :page.sync="page"
@@ -38,11 +37,62 @@
         @current-change="handleCurrentChange"
         @size-change="handleSizeChange"
         @row-del="handleRowDelete"
+        @row-update="handleRowUpdate"
       >
+        <!-- 订单数量自定义模板 -->
+        <template slot="orderQuantity" slot-scope="scope">
+          <el-input
+            v-if="editMode"
+            v-model="scope.row.orderQuantity"
+            size="mini"
+            style="width: 100%"
+            placeholder="请输入订单数量"
+            @input="validateIntegerInput($event, scope.row, 'orderQuantity')"
+            @blur="handleQuantityChange(scope.row, scope.$index)"
+          />
+          <span v-else>{{ scope.row.orderQuantity }}</span>
+        </template>
+
+        <!-- 税率自定义模板 -->
+        <template slot="taxRate" slot-scope="scope">
+          <el-input
+            v-if="editMode"
+            v-model="scope.row.taxRate"
+            size="mini"
+            style="width: 100%"
+            placeholder="请输入税率"
+            @input="validateFloatInput($event, scope.row, 'taxRate', 0, 100)"
+            @blur="handleTaxRateChange(scope.row, scope.$index)"
+          />
+          <span v-else>{{ scope.row.taxRate }}%</span>
+        </template>
 
-        <!-- 总金额列自定义渲染 -->
-        <template slot="totalAmount" slot-scope="{ row }">
-          <span class="amount-text">{{ formatAmount(row.totalAmount) }}</span>
+        <!-- 税额自定义模板 -->
+        <template slot="taxAmount" slot-scope="scope">
+          <el-input
+            v-if="editMode"
+            v-model="scope.row.taxAmount"
+            size="mini"
+            style="width: 100%"
+            placeholder="请输入税额"
+            @input="validateFloatInput($event, scope.row, 'taxAmount')"
+            @blur="handleTaxAmountChange(scope.row, scope.$index)"
+          />
+          <span v-else>{{ scope.row.taxAmount }}</span>
+        </template>
+
+        <!-- 总金额自定义模板 -->
+        <template slot="totalAmount" slot-scope="scope">
+          <el-input
+            v-if="editMode"
+            v-model="scope.row.totalAmount"
+            size="mini"
+            style="width: 100%"
+            placeholder="请输入总金额"
+            @input="validateFloatInput($event, scope.row, 'totalAmount')"
+            @blur="handleTotalAmountChange(scope.row, scope.$index)"
+          />
+          <span v-else>{{ scope.row.totalAmount }}</span>
         </template>
 
         <!-- 明细状态列自定义渲染 -->
@@ -145,6 +195,14 @@ export default {
    */
   props: {
     /**
+     * 是否为编辑模式 - 控制表格是否可编辑
+     * @type {boolean}
+     */
+    editMode: {
+      type: Boolean,
+      default: false
+    },
+    /**
      * 订单ID - 关联的订单唯一标识符
      * @type {string|number|null}
      */
@@ -196,7 +254,19 @@ export default {
        * 导入弹窗显示状态 - 控制物料导入弹窗的显示/隐藏
        * @type {boolean}
        */
-      importDialogVisible: false
+      importDialogVisible: false,
+
+      /**
+       * 正在编辑的行数据 - 用于记录编辑前的状态
+       * @type {MaterialDetailRecord|null}
+       */
+      editingRow: null,
+
+      /**
+       * 正在编辑的属性名 - 用于记录当前编辑的字段
+       * @type {string|null}
+       */
+      editingProp: null
     }
   },
 
@@ -206,10 +276,10 @@ export default {
   computed: {
     /**
      * 表格配置选项 - 获取AvueJS表格的配置对象
-     * @returns {AvueTableOption} AvueJS表格配置对象,已配置为只读模式
+     * @returns {AvueTableOption} AvueJS表格配置对象,根据编辑模式配置
      */
     tableOption() {
-      return getMaterialDetailOption()
+      return getMaterialDetailOption(this.editMode)
     },
 
     /**
@@ -246,6 +316,93 @@ export default {
    */
   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 === '.') {
+          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)
+        }
+        
+        // 如果有有效数值,进行范围检查
+        if (cleanValue !== '' && cleanValue !== '-' && cleanValue !== '.') {
+          const numValue = parseFloat(cleanValue)
+          if (!isNaN(numValue)) {
+            if (numValue < min) {
+              row[field] = min
+            } else if (numValue > max) {
+              row[field] = max
+            } else {
+              // 限制为2位小数
+              const roundedValue = Math.round(numValue * 100) / 100
+              row[field] = roundedValue
+            }
+          }
+        }
+      },
+
+    /**
      * 处理导入物料按钮点击事件
      * @description 打开物料导入弹窗,允许用户选择和导入物料
      * @returns {void}
@@ -384,6 +541,173 @@ export default {
      */
     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-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}
+     */
+    handleQuantityChange(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}
+     */
+    handleTaxRateChange(row, index) {
+      this.calculateTaxAmount(row)
+      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-update', { row: calculatedRow, index: this.getCurrentRowIndex(row) })
+      }
+    },
+
+    /**
+     * 自动计算金额
+     * @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
+      
+      // 计算总金额:订单数量 * 单价(保留2位小数)
+      const totalAmount = orderQuantity * unitPrice
+      calculatedRow.totalAmount = Math.round(totalAmount * 100) / 100
+      
+      // 计算税额:总金额 * 税率 / 100(保留2位小数)
+      const taxAmount = totalAmount * (taxRate / 100)
+      calculatedRow.taxAmount = Math.round(taxAmount * 100) / 100
+      
+      return calculatedRow
+    },
+
+
+
+    /**
+     * 计算税额
+     * @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位小数
+      }
+    },
+
+    /**
+     * 获取当前行索引
+     * @description 根据行数据获取在当前页中的索引
+     * @param {MaterialDetailRecord} row - 行数据
+     * @returns {number} 行索引
+     */
+    getCurrentRowIndex(row) {
+      return this.currentPageData.findIndex(item => item.id === row.id)
     }
   }
 }

+ 28 - 3
src/components/order-form/order-form.vue

@@ -43,11 +43,12 @@
       <div class="material-detail-section">
         <material-detail-table
           :order-id="orderId"
-          :is-edit="isEdit"
+          :edit-mode="isEdit"
           :material-details="materialDetails"
           @material-change="handleMaterialChange"
           @material-import="handleMaterialImport"
           @material-delete="handleMaterialDelete"
+          @material-update="handleMaterialUpdate"
         />
       </div>
     </div>
@@ -260,6 +261,24 @@ export default {
     },
 
     /**
+     * 处理物料明细更新事件
+     * @description 当物料明细表格中的数据被编辑时的回调处理
+     * @param {Object} updateData - 更新数据对象
+     * @param {MaterialDetailRecord} updateData.row - 更新后的行数据
+     * @param {number} updateData.index - 行索引
+     * @returns {void}
+     */
+    handleMaterialUpdate({ row, index }) {
+      // 更新物料明细列表中对应的记录
+      if (index >= 0 && index < this.materialDetails.length) {
+        this.$set(this.materialDetails, index, { ...row })
+        
+        // 重新计算订单总金额
+        this.calculateOrderTotal()
+      }
+    },
+
+    /**
      * 计算订单总金额
      * @description 根据物料明细计算订单总金额并更新表单数据
      * @returns {void}
@@ -269,9 +288,15 @@ export default {
         return sum + (Number(item.totalAmount) || 0)
       }, 0)
       
-      // 更新表单中的总金额字段
+      // 计算总税额
+      const totalTaxAmount = this.materialDetails.reduce((sum, item) => {
+        return sum + (Number(item.taxAmount) || 0)
+      }, 0)
+      
+      // 更新表单中的总金额和税额字段
       if (this.formData) {
-        this.$set(this.formData, 'totalAmount', totalAmount)
+        this.$set(this.formData, 'totalAmount', Math.round(totalAmount * 100) / 100)
+        this.$set(this.formData, 'totalTaxAmount', Math.round(totalTaxAmount * 100) / 100)
       }
     }
   }