Browse Source

feat(forecast-form): 允许新增态表单直接导入预测数据

yz 1 month ago
parent
commit
99458526ab

+ 189 - 39
src/components/forecast-form/forecast-form-mixin.js

@@ -59,7 +59,7 @@
 
 // API接口导入
 import { addForecast, updateForecast, getForecastDetail } from '@/api/forecast'
-import { addSalesForecastMain, updateSalesForecastMain, getSalesForecastSummaryByMonth, exportSalesForecastTemplate, exportSalesForecastSummaryByYearMonth, importSalesForecastSummaryById } from '@/api/forecast/forecast-summary'
+import { addSalesForecastMain, updateSalesForecastMain, getSalesForecastSummaryByMonth, exportSalesForecastTemplate, exportSalesForecastSummaryByYearMonth, importSalesForecastSummaryById, getSalesForecastMainList } from '@/api/forecast/forecast-summary'
 import { getUserLinkGoods } from '@/api/order/sales-order'
 
 // 常量和枚举导入
@@ -218,6 +218,8 @@ export default {
       dataExportLoading: false,
       /** 导入加载状态 */
       importLoading: false,
+      /** 导入前预保存标记(用于新增态先落主表再导入) */
+      preSavedForImport: false,
 
       /** 客户选项列表
        * @type {Array<CustomerOption>}
@@ -974,6 +976,7 @@ export default {
      */
     initFormData() {
       if (this.isEdit && this.editData) {
+        this.preSavedForImport = false
         // 编辑模式:使用传入的数据,确保year字段为字符串格式
         this.formData = {
           ...this.editData,
@@ -989,6 +992,7 @@ export default {
           // 非关键性异常,忽略
         }
       } else {
+        this.preSavedForImport = false
         // 新增模式:使用默认数据,自动填入下个月
         const now = new Date()
         const currentYear = now.getFullYear()
@@ -1181,34 +1185,7 @@ export default {
         }
 
         // 组装子项明细,仅保留预测数量>0的行
-        const items = this.stockTableData
-          .filter(row => Number(row.forecastQuantity) > 0)
-          .map(row => {
-            const matchedBrand = this.brandDescList.find(b => b.cname === row.brandName)
-            const rawBrandId = row.brandId != null && row.brandId !== '' ? row.brandId : (matchedBrand ? matchedBrand.id : '')
-            const rawItemId = row.goodsId
-
-            const brandId = toIdOutput(rawBrandId)
-            const itemId = toIdOutput(rawItemId)
-
-            const base = {
-              brandId,
-              brandCode: row.brandCode || '',
-              brandName: row.brandName || (matchedBrand ? matchedBrand.cname : ''),
-              itemId,
-              itemCode: row.code || '',
-              itemName: row.cname || '',
-              specs: row.typeNo || '',
-              pattern: row.productDescription || row.brandItem || '',
-              forecastQuantity: toSafeNumberOrString(row.forecastQuantity),
-              approvalStatus: Number(this.formData.approvalStatus ?? 0)
-            }
-            // 编辑模式下,如果明细有 id,带上给后端做区分
-            if (this.isEdit && (row.id != null && row.id !== '')) {
-              return { id: toIdOutput(row.id), ...base }
-            }
-            return base
-          })
+        const items = this.buildForecastItems({ includeRowId: this.isEdit })
 
         // 新增模式下需要至少一条有效明细;编辑模式下仅提交主表四个字段,不校验明细条数
         if (!this.isEdit && !items.length) {
@@ -1227,7 +1204,7 @@ export default {
         }
 
         let res
-        if (this.isEdit && this.formData.id) {
+        if ((this.isEdit || this.preSavedForImport) && this.formData.id) {
           // 更新:需要主表 id
           res = await updateSalesForecastMain({ id: toIdOutput(this.formData.id), ...payloadBase })
         } else {
@@ -1367,10 +1344,6 @@ export default {
      * @this {ForecastFormMixinComponent & Vue}
      */
     handleImportClick() {
-      if (!this.isEdit || !this.formData.id) {
-        this.$message && this.$message.warning('当前记录未保存,无法导入')
-        return
-      }
       const input = this.$refs && this.$refs.importInput
       if (input && input.click) {
         input.value = ''
@@ -1379,6 +1352,176 @@ export default {
     },
 
     /**
+     * 从保存接口响应中提取预测ID
+     * @param {any} res
+     * @returns {string|number|null}
+     */
+    extractForecastIdFromResponse(res) {
+      const data = res && res.data ? res.data.data : null
+      if (data == null) return null
+      if (typeof data === 'number') return data
+      if (typeof data === 'string') {
+        const trimmed = data.trim()
+        if (/^\d+$/.test(trimmed)) return trimmed
+        return null
+      }
+      if (typeof data === 'object') {
+        const raw = data.id || data.forecastId || data.forecastMainId || null
+        if (raw == null) return null
+        if (typeof raw === 'number') return raw
+        if (typeof raw === 'string') {
+          const trimmed = raw.trim()
+          return /^\d+$/.test(trimmed) ? trimmed : null
+        }
+        return null
+      }
+      return null
+    },
+
+    /**
+     * 通过年份/月和客户名称查询预测ID(用于新增保存后无返回ID的兜底)
+     * @param {number} year
+     * @param {number} month
+     * @returns {Promise<string|number|null>}
+     */
+    async fetchForecastIdByYearMonth(year, month) {
+      try {
+        const params = {
+          year,
+          month
+        }
+        if (this.formData && this.formData.customerName) {
+          params.customerName = this.formData.customerName
+        }
+        const res = await getSalesForecastMainList(1, 1, params)
+        const pageData = res && res.data && res.data.code === 200 ? res.data.data : null
+        const records = pageData && Array.isArray(pageData.records) ? pageData.records : []
+        if (!records.length) return null
+        const candidate = records[0] && records[0].id
+        if (typeof candidate === 'number') return candidate
+        if (typeof candidate === 'string') {
+          const trimmed = candidate.trim()
+          return /^\d+$/.test(trimmed) ? trimmed : null
+        }
+        return null
+      } catch (e) {
+        console.warn('根据年月查询预测ID失败:', e)
+        return null
+      }
+    },
+
+    /**
+     * 构建提交的预测明细列表
+     * @param {{ includeRowId?: boolean }} [options]
+     * @returns {Array<any>}
+     */
+    buildForecastItems(options = {}) {
+      const includeRowId = !!options.includeRowId
+
+      /** @param {unknown} val @returns {string|number|''} */
+      const toIdOutput = (val) => {
+        if (val == null || val === '') return ''
+        try {
+          const bi = BigInt(String(val))
+          const absBi = bi >= 0n ? bi : -bi
+          const maxSafe = BigInt(Number.MAX_SAFE_INTEGER)
+          if (absBi <= maxSafe) {
+            return Number(bi)
+          }
+          return String(bi)
+        } catch (e) {
+          return String(val)
+        }
+      }
+
+      /** @param {unknown} val @returns {number|string} */
+      const toSafeNumberOrString = (val) => {
+        if (val == null || val === '') return 0
+        if (typeof val === 'number') {
+          return Number.isFinite(val) ? val : String(val)
+        }
+        const parsed = Number(val)
+        return Number.isFinite(parsed) ? parsed : String(val)
+      }
+
+      return (Array.isArray(this.stockTableData) ? this.stockTableData : [])
+        .filter(row => Number(row.forecastQuantity) > 0)
+        .map(row => {
+          const matchedBrand = (this.brandDescList || []).find(b => b.cname === row.brandName)
+          const rawBrandId = row.brandId != null && row.brandId !== '' ? row.brandId : (matchedBrand ? matchedBrand.id : '')
+          const rawItemId = row.goodsId
+
+          const brandId = toIdOutput(rawBrandId)
+          const itemId = toIdOutput(rawItemId)
+
+          const base = {
+            brandId,
+            brandCode: row.brandCode || '',
+            brandName: row.brandName || (matchedBrand ? matchedBrand.cname : ''),
+            itemId,
+            itemCode: row.code || '',
+            itemName: row.cname || '',
+            specs: row.typeNo || '',
+            pattern: row.productDescription || row.brandItem || '',
+            forecastQuantity: toSafeNumberOrString(row.forecastQuantity),
+            approvalStatus: Number(this.formData.approvalStatus ?? 0)
+          }
+          if (includeRowId && (row.id != null && row.id !== '')) {
+            return { id: toIdOutput(row.id), ...base }
+          }
+          return base
+        })
+    },
+
+    /**
+     * 导入前预保存主表,确保拿到预测ID
+     * @returns {Promise<string|number|null>}
+     * @this {ForecastFormMixinComponent & Vue}
+     */
+    async ensureForecastIdForImport() {
+      if (this.formData.id) return this.formData.id
+      if (!this.formData.customerId) {
+        this.$message && this.$message.warning('请选择客户')
+        return null
+      }
+
+      const year = typeof this.formData.year === 'string' ? parseInt(this.formData.year, 10) : this.formData.year
+      const month = Number(this.formData.month)
+      if (!year || !month) {
+        this.$message && this.$message.warning('请先选择年份和月份')
+        return null
+      }
+
+      const items = this.buildForecastItems({ includeRowId: false })
+      const payloadBase = {
+        year,
+        month,
+        approvalStatus: Number(this.formData.approvalStatus ?? 0),
+        pcBladeSalesForecastSummaryList: items
+      }
+
+      const res = await addSalesForecastMain(payloadBase)
+      if (!(res && res.data && res.data.success)) {
+        const msg = (res && res.data && (res.data.msg || res.data.message)) || '保存失败,请稍后重试'
+        this.$message && this.$message.error(msg)
+        return null
+      }
+
+      let newId = this.extractForecastIdFromResponse(res)
+      if (!newId) {
+        newId = await this.fetchForecastIdByYearMonth(year, month)
+      }
+      if (!newId) {
+        this.$message && this.$message.error('保存成功但未查询到预测ID')
+        return null
+      }
+
+      this.formData.id = newId
+      this.preSavedForImport = true
+      return newId
+    },
+
+    /**
      * 处理导入文件选择
      * @param {Event} event
      * @returns {Promise<void>}
@@ -1388,14 +1531,21 @@ export default {
       const target = event && event.target
       const file = target && target.files ? target.files[0] : null
       if (!file) return
-      if (!this.formData.id) {
-        this.$message && this.$message.warning('未获取到当前记录ID')
-        return
-      }
 
       try {
         this.importLoading = true
-        const res = await importSalesForecastSummaryById(this.formData.id, file)
+        let forecastId = this.formData.id
+        if (!forecastId) {
+          forecastId = await this.ensureForecastIdForImport()
+          if (!forecastId) return
+        }
+
+        const res = await importSalesForecastSummaryById(forecastId, file)
+        if (!(res && res.data && res.data.success)) {
+          const msg = (res && res.data && (res.data.msg || res.data.message)) || '导入失败,请稍后重试'
+          this.$message && this.$message.error(msg)
+          return
+        }
         const list = res && res.data && res.data.data ? res.data.data : []
         if (!Array.isArray(list)) {
           this.$message && this.$message.warning('导入数据格式异常')

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

@@ -63,7 +63,6 @@
               size="small"
               icon="el-icon-upload2"
               :loading="importLoading"
-              v-if="isEdit"
               @click="handleImportClick"
             >导入</el-button>
             <el-button