Procházet zdrojové kódy

refactor(forecast-form): 重构销售预测表单组件并优化文档

yz před 1 týdnem
rodič
revize
e45645262a

+ 633 - 0
src/components/forecast-form/forecast-form-mixin.js

@@ -0,0 +1,633 @@
+/**
+ * @fileoverview 销售预测表单混入组件
+ * @description 提供销售预测表单的数据管理、验证规则和业务逻辑的混入组件,支持新增和编辑模式
+ */
+
+// API接口导入
+import { addForecast, updateForecast, getForecastDetail } from '@/api/forecast'
+
+// 常量和枚举导入
+import {
+  APPROVAL_STATUS,
+  APPROVAL_STATUS_OPTIONS,
+  FORECAST_FORM_RULES,
+  DEFAULT_FORECAST_FORM,
+  getApprovalStatusLabel,
+  getApprovalStatusType,
+  canEdit
+} from '@/constants/forecast'
+
+// 远程搜索API
+import { getCustomerList, getItemList } from '@/api/common'
+
+/**
+ * 销售预测表单事件常量
+ * @readonly
+ */
+export const FORECAST_FORM_EVENTS = {
+  /** 表单提交成功事件 */
+  SUBMIT_SUCCESS: 'submit-success',
+  /** 表单取消事件 */
+  CANCEL: 'cancel',
+  /** 表单加载完成事件 */
+  LOADED: 'loaded',
+  /** 客户选择变更事件 */
+  CUSTOMER_CHANGE: 'customer-change',
+  /** 物料选择变更事件 */
+  ITEM_CHANGE: 'item-change'
+}
+
+/**
+ * 销售预测表单混入
+ * @description 提供销售预测表单的数据管理、验证规则和业务逻辑
+ */
+export default {
+  /**
+   * 组件名称
+   */
+  name: 'ForecastFormMixin',
+
+  /**
+   * 组件属性定义
+   * @description 定义组件接收的外部属性及其类型约束
+   */
+  props: {
+    /**
+     * 表单可见性控制
+     * @description 控制表单的显示和隐藏
+     * @type {boolean}
+     * @default false
+     */
+    visible: {
+      type: Boolean,
+      default: false
+    },
+
+    /**
+     * 编辑模式标识
+     * @description 标识当前表单是否处于编辑模式
+     * @type {boolean}
+     * @default false
+     */
+    isEdit: {
+      type: Boolean,
+      default: false
+    },
+
+    /**
+     * 初始表单数据
+     * @description 用于表单初始化的数据对象
+     * @type {Object}
+     * @default null
+     */
+    initialFormData: {
+      type: Object,
+      default: null
+    },
+
+    /**
+     * 表单标题
+     * @description 自定义表单标题,如果不提供则根据编辑模式自动生成
+     * @type {string}
+     * @default ''
+     */
+    title: {
+      type: String,
+      default: ''
+    }
+  },
+
+  /**
+   * 组件响应式数据
+   * @description 定义组件的响应式数据状态
+   */
+  data() {
+    return {
+      /**
+       * 销售预测表单数据模型
+       * @description 存储销售预测表单的所有字段数据
+       * @type {Object}
+       */
+      formData: this.createInitialFormData(),
+
+      /**
+       * 保存操作加载状态
+       * @description 控制保存按钮的加载状态,防止重复提交
+       * @type {boolean}
+       */
+      saveLoading: false,
+
+      /**
+       * 表单加载状态
+       * @description 控制表单的加载状态,用于编辑模式下的数据加载
+       * @type {boolean}
+       */
+      formLoading: false,
+
+      /**
+       * 客户选项列表
+       * @description 客户下拉选择器的选项数据
+       * @type {Array<{value: string|number, label: string, customerCode: string}>}
+       */
+      customerOptions: [],
+
+      /**
+       * 客户选项加载状态
+       * @description 控制客户选项的加载状态
+       * @type {boolean}
+       */
+      customerLoading: false,
+
+      /**
+       * 物料选项列表
+       * @description 物料下拉选择器的选项数据
+       * @type {Array<{value: string|number, label: string, itemCode: string, specs: string}>}
+       */
+      itemOptions: [],
+
+      /**
+       * 物料选项加载状态
+       * @description 控制物料选项的加载状态
+       * @type {boolean}
+       */
+      itemLoading: false,
+
+      /**
+       * 审批状态选项列表
+       * @description 审批状态下拉选择器的选项数据
+       * @type {typeof APPROVAL_STATUS_OPTIONS}
+       */
+      approvalStatusOptions: APPROVAL_STATUS_OPTIONS,
+
+      /**
+       * 表单验证规则
+       * @description 表单字段的验证规则
+       * @type {typeof FORECAST_FORM_RULES}
+       */
+      formRules: FORECAST_FORM_RULES
+    }
+  },
+
+  /**
+   * 计算属性
+   * @description 组件的响应式计算属性
+   */
+  computed: {
+    /**
+     * 表单标题
+     * @description 根据编辑模式动态显示表单标题
+     * @returns {string} 表单标题文本
+     */
+    formTitle() {
+      if (this.title) {
+        return this.title
+      }
+      return this.isEdit ? '编辑销售预测' : '新增销售预测'
+    }
+  },
+
+  /**
+   * 侦听器
+   * @description 监听属性变化并执行相应操作
+   */
+  watch: {
+    /**
+     * 监听表单可见性变化
+     * @param {boolean} val - 新的可见性值
+     */
+    visible(val) {
+      if (val) {
+        this.$nextTick(() => {
+          // 表单显示时,初始化表单数据
+          if (this.initialFormData) {
+            this.formData = this.cleanAndFormatFormData(this.initialFormData)
+          } else {
+            this.formData = this.createInitialFormData()
+          }
+
+          // 如果是编辑模式且有ID,则加载详情数据
+          if (this.isEdit && this.formData.id) {
+            this.loadForecastDetail(this.formData.id)
+          }
+
+          // 如果不是编辑模式,则生成预测编码
+          if (!this.isEdit && !this.formData.forecastCode) {
+            this.generateForecastCode()
+          }
+        })
+      }
+    },
+
+    /**
+     * 监听初始表单数据变化
+     * @param {Object} val - 新的初始表单数据
+     */
+    initialFormData(val) {
+      if (val && this.visible) {
+        this.formData = this.cleanAndFormatFormData(val)
+      }
+    },
+
+    /**
+     * 监听预测ID变化
+     * @param {string|number} val - 新的预测ID
+     */
+    forecastId: {
+      handler(val) {
+        if (val && this.isEdit && this.visible) {
+          this.loadForecastDetail(val)
+        }
+      },
+      immediate: true
+    }
+  },
+
+  /**
+   * 组件方法
+   * @description 组件的业务逻辑方法集合
+   */
+  methods: {
+    /**
+     * 创建初始表单数据
+     * @description 创建销售预测表单的初始数据结构
+     * @returns {Object} 初始化的表单数据对象
+     * @private
+     */
+    createInitialFormData() {
+      return { ...DEFAULT_FORECAST_FORM }
+    },
+
+    /**
+     * 清理和格式化表单数据
+     * @description 对表单数据进行清理和格式化处理
+     * @param {Object} data - 原始表单数据
+     * @returns {Object} 清理和格式化后的表单数据
+     * @private
+     */
+    cleanAndFormatFormData(data) {
+      // 获取下个月的年份和月份作为默认值
+      const now = new Date()
+      const currentYear = now.getFullYear()
+      const currentMonth = now.getMonth() + 1
+      
+      let defaultYear, defaultMonth
+      if (currentMonth === 12) {
+        // 当前是12月,下个月是明年1月
+        defaultYear = currentYear + 1
+        defaultMonth = 1
+      } else {
+        // 其他月份,直接 +1
+        defaultYear = currentYear
+        defaultMonth = currentMonth + 1
+      }
+      
+      return {
+        id: data.id || null,
+        forecastCode: String(data.forecastCode || ''),
+        year: data.year ? data.year.toString() : defaultYear.toString(),
+        month: Number(data.month) || defaultMonth,
+        customerId: data.customerId ? data.customerId.toString() : null,
+        customerCode: String(data.customerCode || ''),
+        customerName: String(data.customerName || ''),
+        brandId: Number(data.brandId) || null,
+        brandCode: String(data.brandCode || ''),
+        brandName: String(data.brandName || ''),
+        itemId: data.itemId ? data.itemId.toString() : null,
+        itemCode: String(data.itemCode || ''),
+        itemName: String(data.itemName || ''),
+        specs: String(data.specs || ''),
+        forecastQuantity: data.forecastQuantity !== undefined && data.forecastQuantity !== null && data.forecastQuantity !== '' ? Number(data.forecastQuantity) : null,
+        currentInventory: Number(data.currentInventory) || null,
+        approvalStatus: Number(data.approvalStatus) || APPROVAL_STATUS.PENDING,
+        approvedName: String(data.approvedName || ''),
+        approvedTime: data.approvedTime || null,
+        createTime: data.createTime || null,
+        updateTime: data.updateTime || null
+      }
+    },
+
+    /**
+     * 加载销售预测详情
+     * @description 根据ID加载销售预测详情数据
+     * @param {string|number} id - 销售预测ID
+     * @returns {Promise<void>}
+     * @private
+     */
+    async loadForecastDetail(id) {
+      if (!id) return
+
+      try {
+        this.formLoading = true
+        const res = await getForecastDetail(id)
+
+        if (res.data && res.data.success && res.data.data) {
+          const detailData = res.data.data
+          this.formData = this.cleanAndFormatFormData(detailData)
+
+          // 加载客户选项数据,确保客户下拉框能正确显示
+          if (this.formData.customerId) {
+            await this.loadCustomerOption(this.formData.customerId, this.formData.customerName)
+          }
+
+          // 加载物料选项数据,确保物料下拉框能正确显示
+          if (this.formData.itemId) {
+            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?.msg || '获取详情失败')
+        }
+      } catch (error) {
+        console.error('获取销售预测详情失败:', error)
+        this.$message.error('获取详情失败,请稍后重试')
+      } finally {
+        this.formLoading = false
+      }
+    },
+
+    /**
+     * 加载单个客户选项
+     * @description 为编辑模式加载特定客户的选项数据
+     * @param {string|number} customerId - 客户ID
+     * @param {string} customerName - 客户名称
+     * @returns {Promise<void>}
+     */
+    async loadCustomerOption(customerId, customerName) {
+      if (!customerId) return
+
+      try {
+        // customer-select组件会自动处理回显,我们只需要确保formData中有正确的值
+        // 组件的watch会监听value变化并调用loadCustomerById方法
+    
+      } catch (error) {
+        console.error('加载客户选项失败:', error)
+      }
+    },
+
+    /**
+     * 远程搜索客户
+     * @description 根据关键字远程搜索客户数据
+     * @param {string} query - 搜索关键字
+     * @returns {Promise<void>}
+     */
+    async remoteSearchCustomer(query) {
+      if (query === '') {
+        this.customerOptions = []
+        return
+      }
+
+      try {
+        this.customerLoading = true
+        const res = await getCustomerList({
+          current: 1,
+          size: 10,
+          customerName: query
+        })
+
+        if (res.data && res.data.success && res.data.data) {
+          const { records } = res.data.data
+          this.customerOptions = records.map(item => ({
+            value: item.id,
+            label: item.customerName,
+            customerCode: item.customerCode
+          }))
+        }
+      } catch (error) {
+        console.error('搜索客户失败:', error)
+      } finally {
+        this.customerLoading = false
+      }
+    },
+
+    /**
+     * 加载单个物料选项
+     * @description 为编辑模式加载特定物料的选项数据
+     * @param {string|number} itemId - 物料ID
+     * @param {string} itemName - 物料名称
+     * @param {string} itemCode - 物料编码
+     * @param {string} specs - 规格
+     * @returns {Promise<void>}
+     */
+    async loadItemOption(itemId, itemName, itemCode, specs) {
+      if (!itemId) return
+
+      try {
+        // 如果选项中已经存在该物料,则不需要重新加载
+        const existingOption = this.itemOptions.find(option => option.id === itemId)
+        if (existingOption) return
+
+        // 添加当前物料到选项中
+        this.itemOptions = [{
+          id: itemId,
+          code: itemCode,
+          name: itemName,
+          specs: specs
+        }]
+      } catch (error) {
+        console.error('加载物料选项失败:', error)
+      }
+    },
+
+    /**
+     * 远程搜索物料
+     * @description 根据关键字远程搜索物料数据
+     * @param {string} query - 搜索关键字
+     * @returns {Promise<void>}
+     */
+    async remoteSearchItem(query) {
+      if (query === '') {
+        this.itemOptions = []
+        return
+      }
+
+      try {
+        this.itemLoading = true
+        const res = await getItemList({
+          current: 1,
+          size: 10,
+          itemName: query
+        })
+
+        if (res.data && res.data.success && res.data.data) {
+          const { records } = res.data.data
+          this.itemOptions = records.map(item => ({
+            value: item.id,
+            label: item.itemName,
+            itemCode: item.itemCode,
+            specs: item.specs
+          }))
+        }
+      } catch (error) {
+        console.error('搜索物料失败:', error)
+      } finally {
+        this.itemLoading = false
+      }
+    },
+
+    /**
+     * 客户选择变化处理
+     * @description 处理客户选择变化,更新表单中的客户相关字段
+     * @param {string|number} customerId - 客户ID
+     * @returns {void}
+     */
+    handleCustomerChange(customerId) {
+      const customer = this.customerOptions.find(item => item.value === customerId)
+      if (customer) {
+        this.formData.customerId = customer.value
+        this.formData.customerCode = customer.customerCode
+        this.formData.customerName = customer.label
+
+        // 触发客户变更事件
+        this.$emit(FORECAST_FORM_EVENTS.CUSTOMER_CHANGE, customer)
+      }
+    },
+
+    /**
+     * 物料选择变化处理
+     * @description 处理物料选择变化,更新表单中的物料相关字段
+     * @param {string|number} itemId - 物料ID
+     * @returns {void}
+     */
+    handleItemChange(itemId) {
+      const item = this.itemOptions.find(option => option.value === itemId)
+      if (item) {
+        this.formData.itemId = item.value
+        this.formData.itemCode = item.itemCode
+        this.formData.itemName = item.label
+        this.formData.specs = item.specs
+
+        // 触发物料变更事件
+        this.$emit(FORECAST_FORM_EVENTS.ITEM_CHANGE, item)
+      }
+    },
+
+    /**
+     * 生成预测编码
+     * @description 自动生成销售预测编码
+     * @returns {void}
+     */
+    generateForecastCode() {
+      const now = new Date()
+      const year = now.getFullYear()
+      const month = String(now.getMonth() + 1).padStart(2, '0')
+      const timestamp = now.getTime().toString().slice(-6)
+      this.formData.forecastCode = `FC-${year}-${month}-${timestamp}`
+    },
+
+    /**
+     * 表单提交
+     * @description 提交表单数据,根据编辑模式调用不同的API
+     * @returns {Promise<void>}
+     */
+    async submitForm() {
+      try {
+        // 表单验证
+        await this.$refs.form.validate()
+
+        this.saveLoading = true
+
+        // 准备提交数据
+        const submitData = this.prepareSubmitData()
+
+        let res
+        if (this.isEdit) {
+          res = await updateForecast(submitData)
+        } else {
+          res = await addForecast(submitData)
+        }
+
+        if (res.data && res.data.success) {
+          // 触发提交成功事件,传递API响应数据
+          this.$emit(FORECAST_FORM_EVENTS.SUBMIT_SUCCESS, res.data)
+          
+          // 关闭表单
+          this.closeForm()
+        } else {
+          this.$message.error(res.data?.msg || (this.isEdit ? '修改失败' : '添加失败'))
+        }
+      } catch (error) {
+        if (error.fields) {
+          // 表单验证失败
+          return
+        }
+        console.error('保存失败:', error)
+        this.$message.error('保存失败,请稍后重试')
+      } finally {
+        this.saveLoading = false
+      }
+    },
+
+    /**
+     * 准备提交数据
+     * @description 复制表单数据并进行清理和格式化处理
+     * @returns {Object} 准备好的提交数据
+     * @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 关闭表单并重置数据
+     * @returns {void}
+     */
+    closeForm() {
+      // 触发取消事件
+      this.$emit(FORECAST_FORM_EVENTS.CANCEL)
+      
+      // 更新可见性
+      this.$emit('update:visible', false)
+      
+      // 重置表单数据
+      this.formData = this.createInitialFormData()
+      
+      // 重置表单验证
+      if (this.$refs.form) {
+        this.$refs.form.resetFields()
+      }
+    },
+
+    /**
+     * 获取审批状态标签
+     * @description 根据审批状态获取对应的标签文本
+     * @param {number} status - 审批状态
+     * @returns {string} 状态标签
+     */
+    getApprovalStatusLabel,
+
+    /**
+     * 获取审批状态类型
+     * @description 根据审批状态获取对应的类型(用于标签样式)
+     * @param {number} status - 审批状态
+     * @returns {string} 状态类型
+     */
+    getApprovalStatusType,
+
+    /**
+     * 判断是否可以编辑
+     * @description 根据审批状态判断记录是否可以编辑
+     * @param {number} status - 审批状态
+     * @returns {boolean} 是否可编辑
+     */
+    canEdit
+  }
+}

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

@@ -279,8 +279,16 @@ export const forecastFormOption = {
 
 /**
  * 获取表单配置
- * @param {boolean} isEdit 是否为编辑模式
- * @returns {Object} 表单配置对象
+ * @description 根据编辑模式动态生成AvueJS表单配置,支持字段显示/隐藏和禁用状态调整
+ * @param {boolean} [isEdit=false] - 是否为编辑模式
+ * @returns {import('smallwei__avue/form').AvueFormOption<import('@/api/types/forecast').ForecastForm>} AvueJS表单配置对象
+ * @throws {Error} 当配置对象结构异常时抛出错误
+ * @example
+ * // 获取新增模式的表单配置
+ * const createOption = getFormOption(false)
+ *
+ * // 获取编辑模式的表单配置
+ * const editOption = getFormOption(true)
  */
 export function getFormOption(isEdit = false) {
   // 深拷贝配置对象
@@ -297,7 +305,10 @@ export function getFormOption(isEdit = false) {
 
 /**
  * 调整编辑模式下的字段配置
- * @param {Object} option 表单配置对象
+ * @description 修改表单配置以适应编辑模式,显示审批信息、预测编码,禁用关键字段
+ * @param {import('smallwei__avue/form').AvueFormOption<import('@/api/types/forecast').ForecastForm>} option - 表单配置对象
+ * @returns {void} 直接修改传入的配置对象,无返回值
+ * @private
  */
 function adjustFieldsForEditMode(option) {
   option.group.forEach(group => {
@@ -325,7 +336,10 @@ function adjustFieldsForEditMode(option) {
 
 /**
  * 调整新增模式下的字段配置
- * @param {Object} option 表单配置对象
+ * @description 修改表单配置以适应新增模式,隐藏审批信息和预测编码字段
+ * @param {import('smallwei__avue/form').AvueFormOption<import('@/api/types/forecast').ForecastForm>} option - 表单配置对象
+ * @returns {void} 直接修改传入的配置对象,无返回值
+ * @private
  */
 function adjustFieldsForCreateMode(option) {
   option.group.forEach(group => {

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

@@ -23,7 +23,6 @@ import { getCustomerList, getItemList } from '@/api/common'
 /**
  * 销售预测表单事件常量
  * @readonly
- * @type {Object}
  */
 export const FORECAST_FORM_EVENTS = {
   /** 表单提交成功事件 */

+ 49 - 0
src/components/forecast-form/index.scss

@@ -0,0 +1,49 @@
+/**
+ * 销售预测表单样式文件
+ * @description 销售预测表单组件的样式定义,包含表单容器、字段样式和响应式设计
+ */
+
+.forecast-form-container {
+  .forecast-form-content {
+    padding: 20px;
+    background: #fff;
+    border-radius: 4px;
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+  }
+
+  .forecast-quantity-section {
+    .quantity-input {
+      margin-bottom: 8px;
+    }
+
+    .inventory-display {
+      font-size: 12px;
+      color: #909399;
+      line-height: 1.5;
+    }
+  }
+}
+
+// AvueJS 表单样式调整
+:deep(.avue-form) {
+  .el-form-item__label {
+    font-weight: 500;
+    color: #303133;
+  }
+
+  .avue-group {
+    margin-bottom: 20px;
+
+    .avue-group__header {
+      margin-bottom: 15px;
+      padding-bottom: 8px;
+      border-bottom: 1px solid #ebeef5;
+
+      .avue-group__title {
+        font-size: 16px;
+        font-weight: 600;
+        color: #303133;
+      }
+    }
+  }
+}

+ 2 - 45
src/components/forecast-form/forecast-form-avue.vue → src/components/forecast-form/index.vue

@@ -53,7 +53,7 @@
 </template>
 
 <script>
-import forecastFormMixin from './index.js'
+import forecastFormMixin from './forecast-form-mixin.js'
 import { getFormOption } from './form-option'
 import { getApprovalStatusLabel, getApprovalStatusType } from '@/constants/forecast'
 import { addForecast, updateForecast } from '@/api/forecast/index'
@@ -380,48 +380,5 @@ export default {
 </script>
 
 <style lang="scss" scoped>
-.forecast-form-container {
-  .forecast-form-content {
-    padding: 20px;
-    background: #fff;
-    border-radius: 4px;
-    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
-  }
-
-  .forecast-quantity-section {
-    .quantity-input {
-      margin-bottom: 8px;
-    }
-
-    .inventory-display {
-      font-size: 12px;
-      color: #909399;
-      line-height: 1.5;
-    }
-  }
-}
-
-// AvueJS 表单样式调整
-:deep(.avue-form) {
-  .el-form-item__label {
-    font-weight: 500;
-    color: #303133;
-  }
-
-  .avue-group {
-    margin-bottom: 20px;
-
-    .avue-group__header {
-      margin-bottom: 15px;
-      padding-bottom: 8px;
-      border-bottom: 1px solid #ebeef5;
-
-      .avue-group__title {
-        font-size: 16px;
-        font-weight: 600;
-        color: #303133;
-      }
-    }
-  }
-}
+@import './index.scss';
 </style>

+ 1 - 1
src/views/forecast/forecastIndex.js

@@ -12,7 +12,7 @@ import {
   DEFAULT_FORECAST_FORM
 } from '@/constants/forecast'
 import { mapGetters } from 'vuex'
-import ForecastFormAvue from '@/components/forecast-form/forecast-form-avue.vue'
+import ForecastFormAvue from '@/components/forecast-form/index.vue'
 
 /**
  * 经销商销售预测申报页面业务逻辑混入