Преглед на файлове

feat(forecast-form): 添加物料选择导入功能并重构表单提交逻辑

yz преди 4 седмици
родител
ревизия
217db3f740
променени са 3 файла, в които са добавени 218 реда и са изтрити 206 реда
  1. 183 205
      src/components/forecast-form/forecast-form-mixin.js
  2. 29 1
      src/components/forecast-form/index.vue
  3. 6 0
      src/components/forecast-form/types.d.ts

+ 183 - 205
src/components/forecast-form/forecast-form-mixin.js

@@ -193,7 +193,6 @@ export default {
         itemSpecs: '',
         forecastQuantity: null,
         currentInventory: null,
-        approvalStatus: APPROVAL_STATUS.PENDING,
         approvedName: '',
         approvedTime: null,
         approvalRemark: '',
@@ -261,6 +260,24 @@ export default {
       /** @type {Array<import('@/api/types/order').PjpfBrandDesc>} */
       brandDescList: [],
 
+      /**
+       * 用户关联库存物料列表(不直接展示在表格中)
+       * @type {Array<import('@/api/types/order').PjpfStockDesc>}
+       */
+      stockDescList: [],
+
+      /**
+       * 物料选择下拉选项(通过 cname 搜索)
+       * @type {Array<SelectOption<string>>}
+       */
+      stockSelectOptions: [],
+
+      /**
+       * 当前选择待导入的物料ID
+       * @type {string | null}
+       */
+      selectedStockId: null,
+
       /** 当前库存 */
       currentInventory: null
     }
@@ -420,30 +437,32 @@ export default {
      * @private
      */
     createInitialFormData() {
-      return {
+      /** @type {ForecastFormModel} */
+      const initial = {
+        id: null,
         forecastCode: '',
-        year: new Date().getFullYear(),
+        year: new Date().getFullYear().toString(),
         month: new Date().getMonth() + 1,
-        customerId: 0,
+        customerId: null,
         customerCode: '',
         customerName: '',
-        brandId: 0,
+        brandId: null,
         brandCode: '',
         brandName: '',
-        itemId: 0,
+        itemId: null,
         itemCode: '',
         itemName: '',
         specs: '',
-        forecastQuantity: 0,
         itemSpecs: '',
+        forecastQuantity: null,
         currentInventory: null,
-        approvalStatus: APPROVAL_STATUS.PENDING,
         approvedName: '',
         approvedTime: null,
         approvalRemark: '',
         createTime: null,
         updateTime: null
       }
+      return initial
     },
 
     /**
@@ -810,6 +829,119 @@ export default {
     },
 
     /**
+     * 表单提交事件处理(Avue表单 @submit 入口)
+     * @description 响应 avue-form 的提交事件,统一走 submitForm 逻辑
+     * @returns {Promise<void>}
+     * @this {ForecastFormMixinComponent & Vue}
+     */
+    handleSubmit(form, done, loading) {
+      try {
+        // 先结束 Avue 内置的按钮loading,避免未调用 done 导致一直loading
+        if (typeof done === 'function') done()
+
+        // 采用旧实现风格:通过 this.$refs.forecastForm.validate 回调进行校验
+        if (this.$refs && this.$refs.forecastForm && typeof this.$refs.forecastForm.validate === 'function') {
+          this.$refs.forecastForm.validate((valid) => {
+            if (!valid) {
+              // 校验失败时,如存在 loading 回调(部分版本提供),尝试恢复按钮状态
+              if (typeof loading === 'function') loading()
+              return
+            }
+            // 校验通过后执行提交
+            this.submitForm()
+              .catch((e) => {
+                console.error('提交异常:', e)
+                this.$message && this.$message.error(e && e.message ? e.message : '提交失败,请稍后重试')
+              })
+          })
+        } else {
+          // 无法获取到 validate 时,直接尝试提交
+          this.submitForm()
+            .catch((e) => {
+              console.error('提交异常:', e)
+              this.$message && this.$message.error(e && e.message ? e.message : '提交失败,请稍后重试')
+            })
+        }
+      } catch (e) {
+        console.error('提交异常:', e)
+        this.$message && this.$message.error(e && e.message ? e.message : '提交异常,请稍后重试')
+      }
+    },
+
+    /**
+     * 提交表单数据(仅批量保存预测汇总)
+     * @returns {Promise<void>}
+     */
+    async submitForm() {
+      try {
+        // 基础校验(客户必选)
+        if (!this.formData.customerId) {
+          this.$message && this.$message.warning('请选择客户')
+          return
+        }
+
+        // 组装批量保存载荷,仅保留预测数量>0的行
+        const year = typeof this.formData.year === 'string' ? parseInt(this.formData.year, 10) : this.formData.year
+        const month = this.formData.month
+
+        const payload = this.stockTableData
+          .filter(row => Number(row.forecastQuantity) > 0)
+          .map(row => {
+            const matchedBrand = this.brandDescList.find(b => b.cname === row.brandName)
+
+            const brandIdRaw = row.brandId != null && row.brandId !== ''
+              ? row.brandId
+              : (matchedBrand ? matchedBrand.id : null)
+            const itemIdRaw = row.goodsId
+
+            const toSafeNumberOrString = (val) => {
+              if (val == null || val === '') return 0
+              if (typeof val === 'number') {
+                return Number.isSafeInteger(val) ? val : String(val)
+              }
+              const parsed = Number(val)
+              return Number.isSafeInteger(parsed) ? parsed : String(val)
+            }
+
+            const brandId = toSafeNumberOrString(brandIdRaw)
+            const itemId = toSafeNumberOrString(itemIdRaw)
+
+            return {
+              year: year || new Date().getFullYear(),
+              month: month || (new Date().getMonth() + 1),
+              brandId: brandId,
+              brandCode: '',
+              brandName: row.brandName || (matchedBrand ? matchedBrand.cname : ''),
+              itemId: itemId,
+              itemCode: row.code || '',
+              itemName: row.cname || '',
+              specs: row.typeNo || '',
+              pattern: row.productDescription || row.brandItem || '',
+              forecastQuantity: Number(row.forecastQuantity) || 0,
+              approvalStatus: this.formData.approvalStatus || 0
+            }
+          })
+
+        if (!payload.length) {
+          this.$message && this.$message.warning('请至少填写一条有效的预测数量')
+          return
+        }
+
+        const res = await batchSaveSalesForecastSummary(payload)
+        if (res && res.data && res.data.success) {
+          this.$message && this.$message.success('保存成功')
+          this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT, res.data)
+        } else {
+          const msg = (res && res.data && (res.data.msg || res.data.message)) || '保存失败'
+          this.$message && this.$message.error(msg)
+        }
+      } catch (error) {
+        console.error('提交表单失败:', error)
+        this.$message && this.$message.error(error && error.message ? error.message : '操作失败,请重试')
+      }
+    },
+
+    /**
      * 客户选择事件处理
      * @description 处理CustomerSelect组件的客户选择事件
      * @param {CustomerSelectData} customerData - 客户选择数据
@@ -840,15 +972,24 @@ export default {
     async loadUserLinkGoods() {
       try {
         this.tableLoading = true
+        // 初始化容器
         this.stockTableData = []
         this.brandDescList = []
+        this.stockDescList = []
+        this.stockSelectOptions = []
+        this.selectedStockId = null
         const res = await getUserLinkGoods()
         const payload = res && res.data && res.data.data ? res.data.data : null
         const brandList = (payload && payload.pjpfBrandDescList) || []
         const stockList = (payload && payload.pjpfStockDescList) || []
         this.brandDescList = brandList
-        // 将库存列表转为表格数据,并默认预测数量为1
-        this.stockTableData = stockList.map(row => ({ ...row, forecastQuantity: 1 }))
+        // 存储库存列表供选择用,不直接展示到表格
+        this.stockDescList = stockList
+        // 构造下拉选项,label 使用 cname,value 使用 id
+        this.stockSelectOptions = stockList.map(item => ({
+          label: item.cname,
+          value: item.id
+        }))
       } catch (e) {
         console.error('加载用户关联商品失败:', e)
         this.$message.error(e.message || '加载用户关联商品失败')
@@ -858,6 +999,37 @@ export default {
     },
 
     /**
+     * 导入所选物料到下方表格
+     * @description 仅在点击“导入物料”按钮后,将选择的物料行添加到表格,默认预测数量为 1
+     * @returns {void}
+     * @this {ForecastFormMixinComponent & Vue}
+     */
+    handleImportSelectedStock() {
+      // 未选择则提示
+      if (!this.selectedStockId) {
+        this.$message.warning('请先在上方选择要导入的物料')
+        return
+      }
+      // 查找明细
+      const stock = this.stockDescList.find(s => s.id === this.selectedStockId)
+      if (!stock) {
+        this.$message.error('未找到所选物料数据,请重新选择')
+        return
+      }
+      // 防止重复导入(按 id 去重)
+      const exists = this.stockTableData.some(row => row.id === stock.id)
+      if (exists) {
+        this.$message.warning('该物料已在列表中')
+        this.selectedStockId = null
+        return
+      }
+      // 添加到表格,默认预测数量为 1
+      this.stockTableData.push({ ...stock, forecastQuantity: 1 })
+      // 清空已选
+      this.selectedStockId = null
+    },
+
+    /**
      * 品牌变更处理
      * @param {number} brandId - 品牌ID
      * @returns {void}
@@ -898,200 +1070,6 @@ export default {
         this.formData.itemSpecs = ''
         this.currentInventory = null
       }
-    },
-
-    /**
-     * 获取当前库存
-     * @description 根据物料ID获取当前库存数量
-     * @param {string|number} itemId - 物料ID
-     * @returns {void}
-     * @this {ForecastFormMixinComponent & Vue}
-     */
-    getCurrentInventory(/** @type {string|number} */ itemId) {
-      // 简化实现,实际应该调用API
-      setTimeout(() => {
-        // 模拟随机库存数量
-        this.currentInventory = Math.floor(Math.random() * 1000)
-      }, 300)
-    },
-
-    /**
-     * 表单提交处理
-     * @description 验证表单并提交数据
-     * @returns {void}
-     * @this {ForecastFormMixinComponent & Vue & {$refs: {forecastForm: {validate: (callback: (valid: boolean) => void) => void}}}}
-     */
-    handleSubmit() {
-      // 表单验证
-      this.$refs.forecastForm.validate((/** @type {boolean} */ valid) => {
-        if (!valid) {
-          return
-        }
-        this.submitForm()
-      })
-    },
-
-    /**
-     * 表单重置处理
-     * @description 重置表单数据到初始状态
-     * @returns {void}
-     * @this {ForecastFormMixinComponent & Vue}
-     */
-    handleReset() {
-      this.initFormData()
-      this.currentInventory = null
-      this.$emit(FORECAST_FORM_EVENTS.RESET)
-    },
-
-    /**
-     * 提交表单数据
-     * @description 执行表单数据的提交操作,包括数据验证和API调用
-     * @this {ForecastFormMixinComponent & Vue}
-     * @returns {Promise<void>} 提交操作的Promise
-     * @throws {Error} 当提交失败时抛出错误
-     */
-    async submitForm() {
-      try {
-        // 校验基础表单(年份、月份、客户等)
-        const year = typeof this.formData.year === 'string' ? parseInt(this.formData.year, 10) : this.formData.year
-        const month = this.formData.month
-
-        // 组装批量保存载荷
-        /** @type {import('@/api/forecast/types').SalesForecastSummaryBatchSaveRequest} */
-        const payload = this.stockTableData
-          .filter(row => Number(row.forecastQuantity) > 0)
-          .map(row => {
-            // 使用品牌描述列表按名称匹配品牌,匹配不到则留空/置0
-            const matchedBrand = this.brandDescList.find(b => b.cname === row.brandName)
-
-            // 原始 id 值(可能为字符串或数字,且可能超出 JS 安全整数范围)
-            const brandIdRaw = row.brandId != null && row.brandId !== ''
-              ? row.brandId
-              : (matchedBrand ? matchedBrand.id : null)
-            const itemIdRaw = row.goodsId
-
-            // 将可能超出安全整数范围的数值以字符串形式透传,避免 Number 精度丢失
-            /**
-             * 将可能超出安全整数范围的 id 值转换为安全的 number 或保留为 string
-             * @param {string|number|null|undefined} val
-             * @returns {string|number}
-             */
-            const toSafeNumberOrString = (val) => {
-              if (val == null || val === '') return 0
-              if (typeof val === 'number') {
-                return Number.isSafeInteger(val) ? val : String(val)
-              }
-              // 字符串:尝试转为数字,若安全则用数字,否则保留为原字符串
-              const parsed = Number(val)
-              return Number.isSafeInteger(parsed) ? parsed : String(val)
-            }
-
-            const brandId = toSafeNumberOrString(brandIdRaw)
-            const itemId = toSafeNumberOrString(itemIdRaw)
-
-            /** @type {import('@/api/forecast/types').SalesForecastSummaryBatchSaveItem} */
-            const item = {
-              year: year || new Date().getFullYear(),
-              month: month || (new Date().getMonth() + 1),
-              brandId:  brandId,
-              brandCode: '', // 接口未返回品牌编码,按要求匹配不到留空
-              brandName: row.brandName || (matchedBrand ? matchedBrand.cname : ''),
-              itemId:  itemId,
-              itemCode: row.code || '',
-              itemName: row.cname || '',
-              specs: row.typeNo || '',
-              pattern: row.productDescription || '',
-              forecastQuantity: Number(row.forecastQuantity) || 0,
-              approvalStatus: this.formData.approvalStatus || 0
-            }
-            return item
-          })
-
-        if (!payload.length) {
-          this.$message.warning('请至少填写一条有效的预测数量')
-          return
-        }
-
-        // 提交批量保存
-        const res = await batchSaveSalesForecastSummary(payload)
-        this.$emit(FORECAST_FORM_EVENTS.SUBMIT, res.data)
-      } catch (error) {
-        console.error('提交表单失败:', error)
-        this.$message.error(error.message || '操作失败,请重试')
-      }
-    },
-
-    /**
-     * 准备提交数据
-     * @description 复制表单数据并进行清理和格式化处理
-     * @returns {ForecastFormModel} 准备好的提交数据
-     * @this {ForecastFormMixinComponent & Vue}
-     * @private
-     */
-    prepareSubmitData() {
-      const submitData = { ...this.formData }
-
-      // 转换年份为数字
-      if (submitData.year && typeof submitData.year === 'string') {
-        submitData.year = parseInt(submitData.year, 10)
-      }
-
-      // 确保数值字段为数字类型
-      if (submitData.forecastQuantity) {
-        submitData.forecastQuantity = Number(submitData.forecastQuantity)
-      }
-
-      if (submitData.currentInventory) {
-        submitData.currentInventory = Number(submitData.currentInventory)
-      }
-
-      return submitData
-    },
-
-    /**
-     * 关闭表单
-     * @description 关闭表单并重置数据
-     * @this {ForecastFormMixinComponent & Vue}
-     * @returns {void}
-     */
-    closeForm() {
-      // 触发取消事件
-      this.$emit(FORECAST_FORM_EVENTS.CANCEL)
-
-      // 更新可见性
-      this.$emit(FORECAST_FORM_EVENTS.UPDATE_VISIBLE, false)
-
-      // 重置表单数据
-      this.formData = this.createInitialFormData()
-
-      // 重置表单验证
-      if (this.$refs.form) {
-        /** @type {{resetFields: () => void}} */ (this.$refs.form).resetFields()
-      }
-    },
-
-    /**
-     * 获取审批状态标签
-     * @description 根据审批状态获取对应的标签文本
-     * @param {ApprovalStatus} status - 审批状态
-     * @returns {string} 状态标签
-     */
-    getApprovalStatusLabel,
-
-    /**
-     * 获取审批状态类型
-     * @description 根据审批状态获取对应的类型(用于标签样式)
-     * @param {ApprovalStatus} status - 审批状态
-     * @returns {string} 状态类型
-     */
-    getApprovalStatusType,
-
-    /**
-     * 判断是否可以编辑
-     * @description 根据审批状态判断记录是否可以编辑
-     * @param {ApprovalStatus} status - 审批状态
-     * @returns {boolean} 是否可编辑
-     */
-    canEdit
+    }
   }
 }

+ 29 - 1
src/components/forecast-form/index.vue

@@ -25,6 +25,31 @@
         <!-- 物料表格区域 -->
         <div class="forecast-goods-table">
           <div class="table-title">物料列表(来自用户关联商品 pjpfStockDescList)</div>
+          <!-- 选择并导入物料工具栏 -->
+          <div class="table-toolbar">
+            <el-select
+              v-model="selectedStockId"
+              filterable
+              clearable
+              :disabled="stockSelectOptions.length === 0"
+              placeholder="通过名称(cname)搜索可导入物料"
+              style="width: 360px"
+            >
+              <el-option
+                v-for="opt in stockSelectOptions"
+                :key="opt.value"
+                :label="opt.label"
+                :value="opt.value"
+              />
+            </el-select>
+            <el-button
+              type="primary"
+              :disabled="!selectedStockId"
+              style="margin-left: 8px"
+              @click="handleImportSelectedStock"
+            >导入物料</el-button>
+          </div>
+
           <el-table
             :data="stockTableData"
             border
@@ -50,7 +75,7 @@
               </template>
             </el-table-column>
           </el-table>
-          <div class="table-tip">提示:仅可编辑预测数量,其他字段自动填充并在保存时一并提交。</div>
+          <div class="table-tip">提示:先在上方选择物料并点击“导入物料”,导入后的数据将显示在表格并参与保存流程。</div>
         </div>
       </div>
     </div>
@@ -107,6 +132,9 @@ export default {
   color: #333;
   margin-bottom: 8px;
 }
+.table-toolbar {
+  margin-bottom: 8px;
+}
 .table-tip {
   margin-top: 8px;
   font-size: 12px;

+ 6 - 0
src/components/forecast-form/types.d.ts

@@ -158,6 +158,12 @@ export interface ForecastFormMixinData {
   tableLoading: boolean;
   /** 品牌描述列表(用于品牌信息匹配) */
   brandDescList: Array<import('@/api/types/order').PjpfBrandDesc>;
+  /** 用户关联库存物料列表(不直接展示在表格中) */
+  stockDescList: Array<import('@/api/types/order').PjpfStockDesc>;
+  /** 物料选择下拉选项(通过 cname 搜索) */
+  stockSelectOptions: Array<SelectOption<string>>;
+  /** 当前选择待导入的物料ID */
+  selectedStockId: string | null;
 }
 
 /**