/** * @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 * @type {Object} */ 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} * @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} */ 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} */ 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} */ 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} */ 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} */ 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) { this.$message.success(res.data.msg || (this.isEdit ? '修改成功' : '添加成功')) // 触发提交成功事件 this.$emit(FORECAST_FORM_EVENTS.SUBMIT_SUCCESS, res.data.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 } }