Переглянути джерело

feat(forecast-form): 实现编辑态回显功能并优化表单提交逻辑

yz 2 тижнів тому
батько
коміт
cd9a398c2f

+ 78 - 30
src/components/forecast-form/forecast-form-mixin.js

@@ -59,7 +59,7 @@
 
 // API接口导入
 import { addForecast, updateForecast, getForecastDetail } from '@/api/forecast'
-import { addSalesForecastMain } from '@/api/forecast/forecast-summary'
+import { addSalesForecastMain, updateSalesForecastMain } from '@/api/forecast/forecast-summary'
 import { getUserLinkGoods } from '@/api/order/sales-order'
 
 // 常量和枚举导入
@@ -253,7 +253,7 @@ export default {
       brandOptions: [],
 
       /** 物料表格数据(来自用户关联商品 pjpfStockDescList),带预测数量字段 */
-      /** @type {Array<import('@/api/types/order').PjpfStockDesc & { forecastQuantity: number }>} */
+      /** @type {Array<import('@/api/types/order').PjpfStockDesc & { forecastQuantity: number, brandCode?: string }>} */
       stockTableData: [],
 
       /** 表格加载状态 */
@@ -375,6 +375,29 @@ export default {
             ...newData,
             year: newData.year ? newData.year.toString() : ''
           }
+          // 回显子项明细到物料表格:将 pcBladeSalesForecastSummaryList -> stockTableData
+          if (Array.isArray(newData.pcBladeSalesForecastSummaryList)) {
+            try {
+              this.stockTableData = newData.pcBladeSalesForecastSummaryList.map(item => ({
+                // 尽量保持与 PjpfStockDesc 结构一致,便于表格渲染
+                id: item.id != null ? item.id : undefined,
+                goodsId: item.itemId != null ? item.itemId : undefined,
+                code: item.itemCode || '',
+                cname: item.itemName || '',
+                brandId: item.brandId != null ? item.brandId : undefined,
+                brandCode: item.brandCode || '',
+                brandName: item.brandName || '',
+                typeNo: item.specs || '',
+                productDescription: item.pattern || '',
+                brandItem: item.pattern || '',
+                storeInventory: item.storeInventory || '0',
+                // 预测数量用于编辑
+                forecastQuantity: Number(item.forecastQuantity || 0)
+              }))
+            } catch (e) {
+              console.warn('映射回显明细失败:', e)
+            }
+          }
         }
       },
       immediate: true,
@@ -549,14 +572,30 @@ export default {
             await this.loadItemOption(this.formData.itemId, this.formData.itemName, this.formData.itemCode, this.formData.specs)
           }
 
-          // 触发加载完成事件
-          this.$emit(FORECAST_FORM_EVENTS.LOADED, this.formData)
-        } else {
-          this.$message.error(res.data?.message || '获取详情失败')
+          // 映射明细到表格:pcBladeSalesForecastSummaryList -> stockTableData
+          if (Array.isArray(detailData.pcBladeSalesForecastSummaryList)) {
+            try {
+              this.stockTableData = detailData.pcBladeSalesForecastSummaryList.map(item => ({
+                id: item.id != null ? item.id : undefined,
+                goodsId: item.itemId != null ? item.itemId : undefined,
+                code: item.itemCode || '',
+                cname: item.itemName || '',
+                brandId: item.brandId != null ? item.brandId : undefined,
+                brandCode: item.brandCode || '',
+                brandName: item.brandName || '',
+                typeNo: item.specs || '',
+                productDescription: item.pattern || '',
+                brandItem: item.pattern || '',
+                storeInventory: item.storeInventory || '0',
+                forecastQuantity: Number(item.forecastQuantity || 0)
+              }))
+            } catch (e) {
+              console.warn('映射详情明细失败:', e)
+            }
+          }
         }
       } catch (error) {
-        console.error('获取销售预测详情失败:', error)
-        this.$message.error('获取详情失败,请稍后重试')
+        console.error('加载销售预测详情失败:', error)
       } finally {
         this.formLoading = false
       }
@@ -848,6 +887,8 @@ export default {
             if (!valid) {
               // 校验失败时,如存在 loading 回调(部分版本提供),尝试恢复按钮状态
               if (typeof loading === 'function') loading()
+              // 通知父组件校验失败,便于父侧重置保存按钮loading
+              this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_ERROR, { message: '表单校验未通过' })
               return
             }
             // 校验通过后执行提交
@@ -884,6 +925,8 @@ export default {
         // 基础校验(客户必选)
         if (!this.formData.customerId) {
           this.$message && this.$message.warning('请选择客户')
+          // 通知父组件失败,重置保存按钮loading
+          this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_ERROR, { message: '未选择客户' })
           return
         }
 
@@ -892,26 +935,24 @@ export default {
         const month = Number(this.formData.month)
 
         // 安全的ID转换:优先使用 BigInt 校验范围,再决定以 number 还是 string 传输
+        /** @param {unknown} val @returns {string|number|''} */
         const toIdOutput = (val) => {
           if (val == null || val === '') return ''
           try {
             const bi = BigInt(String(val))
-            // 在 JS 中,超过 MAX_SAFE_INTEGER 的数字使用字符串传输,避免精度丢失
             const absBi = bi >= 0n ? bi : -bi
             const maxSafe = BigInt(Number.MAX_SAFE_INTEGER)
             if (absBi <= maxSafe) {
-              // 安全范围内,返回 number
               return Number(bi)
             }
-            // 超出安全范围,返回字符串
             return String(bi)
           } catch (e) {
-            // 不是纯数字,兜底返回原字符串
             return String(val)
           }
         }
 
         // 安全的数值转换(用于数量等非ID字段):若不可安全表示整数,仍以字符串传输
+        /** @param {unknown} val @returns {number|string} */
         const toSafeNumberOrString = (val) => {
           if (val == null || val === '') return 0
           if (typeof val === 'number') {
@@ -932,46 +973,54 @@ export default {
             const brandId = toIdOutput(rawBrandId)
             const itemId = toIdOutput(rawItemId)
 
-            return {
-              // ID与编码/名称:缺失则置空
-              brandId: brandId,
+            const base = {
+              brandId,
               brandCode: row.brandCode || '',
               brandName: row.brandName || (matchedBrand ? matchedBrand.cname : ''),
-              itemId: itemId,
+              itemId,
               itemCode: row.code || '',
               itemName: row.cname || '',
-              // 规格与花纹:无则空
               specs: row.typeNo || '',
               pattern: row.productDescription || row.brandItem || '',
-              // 数量:保留为 number(有限数),否则以字符串兜底
               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
           })
 
         if (!items.length) {
           this.$message && this.$message.warning('请至少填写一条有效的预测数量')
+          // 通知父组件失败,重置保存按钮loading
+          this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_ERROR, { message: '未填写有效的预测明细' })
           return
         }
 
-        // 组装 main-add 载荷
-        const payload = {
+        // 组装载荷
+        const payloadBase = {
           year: year || new Date().getFullYear(),
           month: month || (new Date().getMonth() + 1),
           approvalStatus: Number(this.formData.approvalStatus ?? 0),
           pcBladeSalesForecastSummaryList: items
         }
 
-        const res = await addSalesForecastMain(payload)
+        let res
+        if (this.isEdit && this.formData.id) {
+          // 更新:需要主表 id
+          res = await updateSalesForecastMain({ id: toIdOutput(this.formData.id), ...payloadBase })
+        } else {
+          // 新增
+          res = await addSalesForecastMain(payloadBase)
+        }
+
         if (res && res.data && res.data.success) {
-        //   this.$message && this.$message.success('提交成功')
           this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT, res.data)
           this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_SUCCESS, res.data)
         } else {
           const msg = (res && res.data && (res.data.msg || res.data.message)) || '提交失败'
-        //   this.$message && this.$message.error(msg)
-          // 提交业务失败时,解禁年份与月份两个表单项(直接操作 $refs)
           if (typeof this.setYearMonthDisabled === 'function') {
             this.setYearMonthDisabled(false)
           } else if (this.$refs) {
@@ -982,13 +1031,10 @@ export default {
               } catch (e) { /* 忽略 */ }
             })
           }
-          // 通知父组件:提交失败(业务失败)
           this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_ERROR, { message: msg, response: res })
         }
       } catch (error) {
         console.error('提交表单失败:', error)
-        // this.$message && this.$message.error(error && error.message ? error.message : '操作失败,请重试')
-        // 通知父组件:提交失败(异常)
         this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_ERROR, error)
       }
     },
@@ -1005,8 +1051,10 @@ export default {
         this.formData.customerId = Number(customerData.customerId)
         this.formData.customerCode = customerData.customerCode
         this.formData.customerName = customerData.customerName
-        // 选中客户后加载该用户关联的品牌与库存物料
-        this.loadUserLinkGoods()
+        // 选中客户后加载该用户关联的品牌与库存物料(仅新增模式自动加载,编辑模式不覆盖回显数据)
+        if (!this.isEdit) {
+          this.loadUserLinkGoods()
+        }
       } else {
         this.formData.customerId = null
         this.formData.customerCode = ''

+ 15 - 19
src/components/forecast-form/types.d.ts

@@ -70,6 +70,11 @@ export interface ForecastFormModel {
   createTime?: string | null;
   /** 更新时间 */
   updateTime?: string | null;
+  /**
+   * 编辑态回显的子项明细列表(来源于主表列表记录中的 pcBladeSalesForecastSummaryList)
+   * 仅在编辑场景使用,因此为可选
+   */
+  pcBladeSalesForecastSummaryList?: Array<import('@/api/forecast/types').SalesForecastMainListItemRecord | import('@/api/forecast/types').SalesForecastMainUpdateItem>;
 }
 
 /**
@@ -215,6 +220,8 @@ export interface ForecastFormProps {
   title: string;
   /** 编辑时的表单数据 */
   editData: ForecastFormModel;
+  /** 列表页传入的主表ID(可选),用于在弹窗内按需加载详情 */
+  forecastId?: string | number;
 }
 
 /**
@@ -263,59 +270,48 @@ export interface ForecastFormMethods {
   handleMaterialSelected(materialData: MaterialSelectData): void;
   /** 获取当前库存 */
   getCurrentInventory(itemId: string | number): void;
-  /** 表单提交处理 */
+  /** 提交(Avue 回调) */
   handleSubmit(): void;
-  /** 表单重置处理 */
+  /** 重置 */
   handleReset(): void;
-  /** 提交表单数据 */
+  /** 提交 */
   submitForm(): Promise<void>;
-  /** 加载当前登录客户信息并填充表单 */
+  /** 加载当前用户客户信息(新增态) */
   loadCurrentCustomerInfo(): Promise<void>;
 }
 
 /**
- * 销售预测表单组件完整接口
+ * 组件合成接口
  */
 export interface ForecastFormComponent extends ForecastFormProps, ForecastFormMixinData, ForecastFormComputed, ForecastFormMethods {
-  /** 组件引用 */
   $refs: {
     forecastForm?: any;
     [key: string]: any;
   };
-  /** 事件发射器 */
   $emit: <K extends keyof ForecastFormEvents>(event: K, ...args: Parameters<ForecastFormEvents[K]>) => void;
 }
 
 /**
- * 物料选择数据接口
+ * 物料选择组件回调数据
  */
 export interface MaterialSelectData {
-  /** 物料ID */
   itemId: string | number;
-  /** 物料编码 */
   itemCode: string;
-  /** 物料名称 */
   itemName: string;
-  /** 物料规格 */
   specification: string;
-  /** API返回的物料数据对象 */
   materialData: ItemRecord | null;
 }
 
 /**
- * 客户选择数据接口
+ * 客户选择组件回调数据
  */
 export interface CustomerSelectData {
-  /** 客户ID */
   customerId: string | number;
-  /** 客户编码 */
   customerCode: string;
-  /** 客户名称 */
   customerName: string;
 }
 
 /**
- * 销售预测表单混入组件类型
- * @description 定义销售预测表单混入组件的完整类型,包含Vue实例的所有属性和方法
+ * 混入组件最终类型(给 JSDoc 使用)
  */
 export interface ForecastFormMixinComponent extends ForecastFormProps, ForecastFormMixinData, ForecastFormComputed, ForecastFormMethods, Vue {}

+ 17 - 5
src/views/forecast/index.vue

@@ -43,9 +43,15 @@
           </el-button>
         </template>
 
-        <!-- 新增:操作列(禁用“不可编辑”按钮) -->
+        <!-- 自定义操作列:根据可编辑状态显示按钮 -->
         <template slot="menu" slot-scope="{ row, index }">
-          <el-button type="text" size="small" disabled>不可编辑</el-button>
+          <el-button
+            v-if="canEditRow(row)"
+            type="text"
+            size="small"
+            @click="handleEdit(row, index)"
+          >编辑</el-button>
+          <el-button v-else type="text" size="small" disabled>不可编辑</el-button>
         </template>
 
         <!-- 自定义审批状态显示 -->
@@ -78,9 +84,15 @@
           </avue-crud>
         </template>
 
-        <!-- 自定义操作列:不可编辑按钮 -->
+        <!-- 自定义操作列:与菜单保持一致的编辑按钮 -->
         <template slot="editBtn" slot-scope="{ row, index }">
-          <el-button type="text" size="small" disabled>不可编辑</el-button>
+          <el-button
+            v-if="canEditRow(row)"
+            type="text"
+            size="small"
+            @click="handleEdit(row, index)"
+          >编辑</el-button>
+          <el-button v-else type="text" size="small" disabled>不可编辑</el-button>
         </template>
       </avue-crud>
 
@@ -198,7 +210,7 @@ export default {
   border-bottom: 1px solid #e4e7ed;
 
   .form-title {
-    font-size: 16px;
+    font-size: 14px;
     font-weight: 600;
     color: #303133;
     margin-left: 8px;