Bläddra i källkod

feat(forecast-form): 重构预测表单为批量提交模式并添加物料表格

yz 3 veckor sedan
förälder
incheckning
9e5a60e354

+ 105 - 8
src/components/forecast-form/forecast-form-mixin.js

@@ -58,6 +58,8 @@
 
 // API接口导入
 import { addForecast, updateForecast, getForecastDetail } from '@/api/forecast'
+import { batchSaveSalesForecastSummary } from '@/api/forecast/forecast-summary'
+import { getUserLinkGoods } from '@/api/order/sales-order'
 
 // 常量和枚举导入
 import {
@@ -248,6 +250,17 @@ export default {
        */
       brandOptions: [],
 
+      /** 物料表格数据(来自用户关联商品 pjpfStockDescList),带预测数量字段 */
+      /** @type {Array<import('@/api/types/order').PjpfStockDesc & { forecastQuantity: number }>} */
+      stockTableData: [],
+
+      /** 表格加载状态 */
+      tableLoading: false,
+
+      /** 品牌描述列表(用于品牌信息匹配) */
+      /** @type {Array<import('@/api/types/order').PjpfBrandDesc>} */
+      brandDescList: [],
+
       /** 当前库存 */
       currentInventory: null
     }
@@ -584,6 +597,7 @@ export default {
 
     /**
      * 加载当前登录客户信息并填充表单
+     * @this {ForecastFormMixinComponent & Vue}
      * @returns {Promise<void>}
      */
     async loadCurrentCustomerInfo() {
@@ -596,6 +610,9 @@ export default {
           this.formData.customerId = data.Customer_ID ? Number(data.Customer_ID) : null
           this.formData.customerCode = data.Customer_CODE || ''
           this.formData.customerName = data.Customer_NAME || ''
+
+          // 成功填充客户信息后,自动加载用户关联的品牌与库存物料,用于渲染下方表格
+          await this.loadUserLinkGoods()
         }
       } catch (e) {
         console.error('获取客户信息失败:', e)
@@ -804,10 +821,39 @@ export default {
         this.formData.customerId = Number(customerData.customerId)
         this.formData.customerCode = customerData.customerCode
         this.formData.customerName = customerData.customerName
+        // 选中客户后加载该用户关联的品牌与库存物料
+        this.loadUserLinkGoods()
       } else {
         this.formData.customerId = null
         this.formData.customerCode = ''
         this.formData.customerName = ''
+        // 清空表格
+        this.stockTableData = []
+      }
+    },
+
+    /**
+     * 加载用户关联商品(品牌与库存)
+     * @returns {Promise<void>}
+     * @this {ForecastFormMixinComponent & Vue}
+     */
+    async loadUserLinkGoods() {
+      try {
+        this.tableLoading = true
+        this.stockTableData = []
+        this.brandDescList = []
+        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 }))
+      } catch (e) {
+        console.error('加载用户关联商品失败:', e)
+        this.$message.error(e.message || '加载用户关联商品失败')
+      } finally {
+        this.tableLoading = false
       }
     },
 
@@ -906,17 +952,68 @@ export default {
      */
     async submitForm() {
       try {
-        // 提交数据
-        const submitData = { ...this.formData }
+        // 校验基础表单(年份、月份、客户等)
+        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)
+            }
 
-        let res
-        if (this.isEdit) {
-          res = await updateForecast(submitData)
-        } else {
-          res = await addForecast(submitData)
+            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
         }
 
-        // 触发提交成功事件,传递API响应数据
+        // 提交批量保存
+        const res = await batchSaveSalesForecastSummary(payload)
         this.$emit(FORECAST_FORM_EVENTS.SUBMIT, res.data)
       } catch (error) {
         console.error('提交表单失败:', error)

+ 2 - 4
src/components/forecast-form/form-option.js

@@ -122,7 +122,9 @@ export const forecastFormOption = {
       label: '产品信息',
       icon: 'el-icon-goods',
       prop: 'product',
+      display: false, // 隐藏产品信息分组
       column: [
+        // 原有字段保留以兼容,但分组已隐藏
         {
           label: '品牌ID',
           prop: 'brandId',
@@ -336,10 +338,6 @@ function adjustFieldsForEditMode(option) {
       if (field.prop === 'year' || field.prop === 'month') {
         field.disabled = true
       }
-      // 品牌在编辑模式下禁用
-      if (field.prop === 'brandId') {
-        field.disabled = true
-      }
     })
   })
 }

+ 61 - 48
src/components/forecast-form/index.vue

@@ -5,51 +5,53 @@
     <div class="forecast-form-container basic-container">
       <!-- 表单内容区域 -->
       <div class="forecast-form-content">
-      <avue-form
-        ref="forecastForm"
-        v-model="formData"
-        :option="formOption"
-        @submit="handleSubmit"
-        @reset-change="handleReset"
-      >
-        <!-- 客户选择器插槽 -->
-        <template #customerId>
-          <customer-select
-            v-model="formData.customerId"
-            placeholder="请选择客户"
-            @customer-selected="handleCustomerSelected"
-          />
-        </template>
+        <avue-form
+          ref="forecastForm"
+          v-model="formData"
+          :option="formOption"
+          @submit="handleSubmit"
+          @reset-change="handleReset"
+        >
+          <!-- 客户选择器插槽 -->
+          <template #customerId>
+            <customer-select
+              v-model="formData.customerId"
+              placeholder="请选择客户"
+              @customer-selected="handleCustomerSelected"
+            />
+          </template>
+        </avue-form>
 
-
-
-        <!-- 物料选择器插槽 -->
-        <template #itemId>
-          <material-select
-            v-model="formData.itemId"
-            placeholder="请选择物料"
-            @material-selected="handleMaterialSelected"
-          />
-        </template>
-
-        <!-- 预测数量字段的库存显示 -->
-        <template #forecastQuantityForm>
-          <div class="forecast-quantity-section">
-            <el-input-number
-              v-model="formData.forecastQuantity"
-              :min="1"
-              :precision="0"
-              :step="1"
-              controls-position="right"
-              style="width: 100%"
-              class="quantity-input"
-            ></el-input-number>
-            <div class="inventory-display" v-if="currentInventory !== null">
-              当前库存: {{ currentInventory }}
-            </div>
-          </div>
-        </template>
-      </avue-form>
+        <!-- 物料表格区域 -->
+        <div class="forecast-goods-table">
+          <div class="table-title">物料列表(来自用户关联商品 pjpfStockDescList)</div>
+          <el-table
+            :data="stockTableData"
+            border
+            stripe
+            height="360"
+            v-loading="tableLoading"
+          >
+            <el-table-column prop="code" label="物料编码" min-width="140" show-overflow-tooltip />
+            <el-table-column prop="cname" label="物料名称" min-width="160" show-overflow-tooltip />
+            <el-table-column prop="brandName" label="品牌名称" min-width="120" show-overflow-tooltip />
+            <el-table-column prop="typeNo" label="规格" min-width="120" show-overflow-tooltip />
+            <!-- Removed specs/description column: productDescription -->
+            <el-table-column label="预测数量" min-width="140">
+              <template #default="{ row }">
+                <el-input-number
+                  v-model="row.forecastQuantity"
+                  :min="0"
+                  :max="999999"
+                  :step="1"
+                  controls-position="right"
+                  style="width: 120px"
+                />
+              </template>
+            </el-table-column>
+          </el-table>
+          <div class="table-tip">提示:仅可编辑预测数量,其他字段自动填充并在保存时一并提交。</div>
+        </div>
       </div>
     </div>
   </div>
@@ -58,7 +60,6 @@
 <script>
 import forecastFormMixin from './forecast-form-mixin.js'
 import CustomerSelect from '@/components/common/customer-select.vue'
-import MaterialSelect from '@/components/common/material-select.vue'
 
 /**
  * 销售预测表单组件实例类型定义
@@ -67,7 +68,6 @@ import MaterialSelect from '@/components/common/material-select.vue'
  * @property {import('./types').FormOption} formOption - 表单配置选项
  * @property {boolean} saveLoading - 保存加载状态
  * @property {import('./types').CustomerOption[]} customerOptions - 客户选项列表
- * @property {import('./types').ItemOption[]} itemOptions - 物料选项列表
  * @property {number|null} currentInventory - 当前库存数量
  * @property {import('./types').ApprovalStatusOption[]} approvalStatusOptions - 审批状态选项
  * @property {import('./types').ForecastFormRules} formRules - 表单验证规则
@@ -91,12 +91,25 @@ export default {
    * 组件注册
    */
   components: {
-    CustomerSelect,
-    MaterialSelect
+    CustomerSelect
   }
 }
 </script>
 
 <style lang="scss" scoped>
 @import './index.scss';
+
+.forecast-goods-table {
+  margin-top: 12px;
+}
+.table-title {
+  font-size: 14px;
+  color: #333;
+  margin-bottom: 8px;
+}
+.table-tip {
+  margin-top: 8px;
+  font-size: 12px;
+  color: #909399;
+}
 </style>

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

@@ -152,6 +152,12 @@ export interface ForecastFormMixinData {
   currentInventory: number | null;
   /** 品牌选项列表 */
   brandOptions: Array<SelectOption<number>>;
+  /** 物料表格数据(来自用户关联商品 pjpfStockDescList),带预测数量字段 */
+  stockTableData: Array<import('@/api/types/order').PjpfStockDesc & { forecastQuantity: number }>;
+  /** 表格加载状态 */
+  tableLoading: boolean;
+  /** 品牌描述列表(用于品牌信息匹配) */
+  brandDescList: Array<import('@/api/types/order').PjpfBrandDesc>;
 }
 
 /**
@@ -243,6 +249,8 @@ export interface ForecastFormMethods {
   generateForecastCode(): void;
   /** 客户选择事件处理 */
   handleCustomerSelected(customerData: CustomerSelectData): void;
+  /** 加载用户关联商品(品牌与库存) */
+  loadUserLinkGoods(): Promise<void>;
   /** 品牌变更处理 */
   handleBrandChange(brandId: number): void;
   /** 物料选择处理 */
@@ -255,6 +263,8 @@ export interface ForecastFormMethods {
   handleReset(): void;
   /** 提交表单数据 */
   submitForm(): Promise<void>;
+  /** 加载当前登录客户信息并填充表单 */
+  loadCurrentCustomerInfo(): Promise<void>;
 }
 
 /**