12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349 |
- // @ts-check
- /* global BigInt */
- /**
- * @fileoverview 销售预测表单混入组件
- * @description 提供销售预测表单的数据管理、验证规则和业务逻辑的混入组件,支持新增和编辑模式
- * @this {ForecastFormMixinComponent & Vue}
- */
- /**
- * 类型定义导入
- * @description 导入所有必要的TypeScript类型定义,确保类型安全
- */
- /**
- * @typedef {import('./types').ForecastFormModel} ForecastFormModel
- * @description 销售预测表单数据模型类型
- */
- /**
- * @typedef {import('./types').ForecastFormMixinData} ForecastFormMixinData
- * @description 销售预测表单混入数据类型
- */
- /**
- * @typedef {import('./types').CustomerOption} CustomerOption
- * @description 客户选项类型
- */
- /**
- * @typedef {import('./types').ItemOption} ItemOption
- * @description 物料选项类型
- */
- /**
- * @typedef {import('./types').ApprovalStatusOption} ApprovalStatusOption
- * @description 审批状态选项类型
- */
- /**
- * @typedef {import('./types').ForecastFormRules} ForecastFormRules
- * @description 销售预测表单验证规则类型
- */
- /**
- * @typedef {import('./types').MaterialSelectData} MaterialSelectData
- * @description 物料选择数据类型
- */
- /**
- * @typedef {import('./types').CustomerSelectData} CustomerSelectData
- * @description 客户选择数据类型
- */
- /**
- * @typedef {import('./types').ForecastFormMixinComponent} ForecastFormMixinComponent
- * @description 销售预测表单混入组件类型
- */
- // API接口导入
- import { addForecast, updateForecast, getForecastDetail } from '@/api/forecast'
- import { addSalesForecastMain, updateSalesForecastMain } from '@/api/forecast/forecast-summary'
- import { getUserLinkGoods } from '@/api/order/sales-order'
- // 常量和枚举导入
- import {
- APPROVAL_STATUS,
- APPROVAL_STATUS_OPTIONS,
- FORECAST_FORM_RULES,
- DEFAULT_FORECAST_FORM,
- getApprovalStatusLabel,
- getApprovalStatusType,
- canEdit
- } from '@/constants/forecast'
- // 远程搜索API
- import { getCustomerList, getItemList, getCustomerInfo } from '@/api/common'
- // 表单配置导入
- import { getFormOption } from './form-option'
- import { safeBigInt } from '@/util/util'
- /**
- * 销售预测表单事件常量
- * @readonly
- */
- export const FORECAST_FORM_EVENTS = {
- /** 表单提交成功事件 */
- SUBMIT_SUCCESS: 'submit-success',
- /** 表单取消事件 */
- CANCEL: 'cancel',
- /** 表单加载完成事件 */
- LOADED: 'loaded',
- /** 客户选择变更事件 */
- CUSTOMER_CHANGE: 'customer-change',
- /** 物料选择变更事件 */
- ITEM_CHANGE: 'item-change',
- /** 表单重置事件 */
- RESET: 'reset',
- /** 表单提交事件 */
- SUBMIT: 'submit',
- /** 表单提交失败事件 */
- SUBMIT_ERROR: 'submit-error',
- /** 更新可见性事件 */
- UPDATE_VISIBLE: 'update:visible'
- }
- /**
- * 销售预测表单混入
- * @description 提供销售预测表单的数据管理、验证规则和业务逻辑
- * @mixin
- */
- export default {
- /**
- * 组件名称
- */
- name: 'ForecastFormMixin',
- /**
- * 组件属性定义
- * @description 定义组件接收的外部属性及其类型约束
- */
- props: {
- /**
- * 表单可见性控制
- * @description 控制表单的显示和隐藏
- */
- visible: {
- type: Boolean,
- default: false
- },
- /**
- * 编辑模式标识
- * @description 标识当前表单是否处于编辑模式
- */
- isEdit: {
- type: Boolean,
- default: false
- },
- /**
- * 初始表单数据
- * @description 用于表单初始化的数据对象
- */
- initialFormData: {
- type: Object,
- default: null
- },
- /**
- * 表单标题
- * @description 自定义表单标题,如果不提供则根据编辑模式自动生成
- */
- title: {
- type: String,
- default: ''
- },
- /**
- * 编辑时的表单数据
- */
- editData: {
- type: Object,
- default: () => ({})
- }
- },
- /**
- * 组件响应式数据
- * @description 定义组件的响应式数据状态
- * @this {ForecastFormMixinComponent & Vue}
- * @returns {ForecastFormMixinData} 组件数据对象
- */
- data() {
- return {
- /**
- * 销售预测表单数据模型
- * @description 存储销售预测表单的所有字段数据
- * @type {ForecastFormModel}
- */
- formData: {
- id: null,
- forecastCode: '',
- year: new Date().getFullYear().toString(),
- month: new Date().getMonth() + 1,
- customerId: null,
- customerCode: '',
- customerName: '',
- brandId: null,
- brandCode: '',
- brandName: '',
- itemId: null,
- itemCode: '',
- itemName: '',
- specs: '',
- itemSpecs: '',
- forecastQuantity: null,
- currentInventory: null,
- approvedName: '',
- approvedTime: null,
- approvalRemark: '',
- createTime: null,
- updateTime: null
- },
- /** 保存操作加载状态 */
- saveLoading: false,
- /** 表单加载状态 */
- formLoading: false,
- /** 客户选项列表
- * @type {Array<CustomerOption>}
- */
- customerOptions: [],
- /** 客户选项加载状态 */
- customerLoading: false,
- /** 物料选项列表
- * @type {Array<ItemOption>}
- */
- itemOptions: [],
- /** 物料选项加载状态 */
- itemLoading: false,
- /** 审批状态选项列表
- * @type {Array<ApprovalStatusOption>}
- */
- approvalStatusOptions: APPROVAL_STATUS_OPTIONS,
- /** 表单验证规则
- * @type {ForecastFormRules}
- */
- formRules: {
- ...FORECAST_FORM_RULES,
- year: [
- { required: true, message: '请选择年份', trigger: 'blur' }
- ]
- },
- /** 表单配置
- * @type {import('./types').FormOption}
- */
- formOption: {
- column: []
- },
- /** 品牌选项列表
- * @type {Array<SelectOption<number>>}
- */
- brandOptions: [],
- /** 物料表格数据(来自用户关联商品 pjpfStockDescList),带预测数量字段 */
- /** @type {Array<import('@/api/types/order').PjpfStockDesc & { forecastQuantity: number, brandCode?: string }>} */
- stockTableData: [],
- /** 表格加载状态 */
- tableLoading: false,
- /** 品牌描述列表(用于品牌信息匹配) */
- /** @type {Array<import('@/api/types/order').PjpfBrandDesc>} */
- brandDescList: [],
- /**
- * 用户关联库存物料列表(不直接展示在表格中)
- * @type {Array<import('@/api/types/order').PjpfStockDesc>}
- */
- stockDescList: [],
- /**
- * 物料选择下拉选项(通过 cname 搜索)
- * @type {Array<SelectOption<string>>}
- */
- stockSelectOptions: [],
- /**
- * 当前选择待导入的物料ID
- * @type {string | null}
- */
- selectedStockId: null,
- /** 当前库存 */
- currentInventory: null
- }
- },
- /**
- * 计算属性
- * @description 组件的响应式计算属性
- */
- computed: {
- /**
- * 表单标题
- * @description 根据编辑模式动态显示表单标题
- * @this {ForecastFormMixinComponent & Vue}
- * @returns {string} 表单标题文本
- */
- formTitle() {
- if (this.title) {
- return this.title
- }
- return this.isEdit ? '编辑销售预测' : '新增销售预测'
- }
- },
- /**
- * 侦听器
- * @description 监听属性变化并执行相应操作
- */
- watch: {
- /**
- * 监听表单可见性变化
- * @this {ForecastFormMixinComponent & Vue}
- */
- visible: {
- /**
- * @this {ForecastFormMixinComponent & Vue}
- * @param {boolean} val - 新的可见性值
- */
- handler(/** @type {boolean} */ 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()
- }
- // 新增模式下,自动获取并填充客户信息
- if (!this.isEdit) {
- this.loadCurrentCustomerInfo()
- }
- })
- }
- },
- immediate: true
- },
- /**
- * 监听初始表单数据变化
- * @param {ForecastFormModel} val - 新的初始表单数据
- * @this {ForecastFormMixinComponent & Vue}
- */
- initialFormData(/** @type {ForecastFormModel} */ val) {
- if (val) {
- this.formData = this.cleanAndFormatFormData(val)
- }
- },
- /**
- * 监听编辑数据变化
- * @this {ForecastFormMixinComponent & Vue}
- */
- editData: {
- /**
- * @this {ForecastFormMixinComponent & Vue}
- * @param {ForecastFormModel} newData
- */
- handler(/** @type {ForecastFormModel} */ newData) {
- if (newData && this.isEdit) {
- this.formData = {
- ...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 ? safeBigInt(item.id) : undefined,
- goodsId: item.itemId ? safeBigInt(item.itemId) : undefined,
- code: item.itemCode || '',
- cname: item.itemName || '',
- brandId: item.brandId ? safeBigInt(item.brandId) : undefined,
- brandCode: item.brandCode || '',
- brandName: item.brandName || '',
- typeNo: item.specs || '',
- productDescription: item.pattern || '',
- brandItem: item.pattern || '',
- // 回显数据可能无库存,先不默认写入 '0',留给后续合并方法填充
- storeInventory: undefined,
- // 预测数量用于编辑
- forecastQuantity: Number(item.forecastQuantity || 0)
- }))
- // 合并接口库存数据以支持回显
- this.mergeEchoStoreInventory && this.mergeEchoStoreInventory().catch(() => {})
- } catch (e) {
- console.warn('映射回显明细失败:', e)
- }
- }
- }
- },
- immediate: true,
- deep: true
- },
- /**
- * 监听编辑模式变化
- * @this {ForecastFormMixinComponent & Vue}
- */
- isEdit: {
- /**
- * @this {ForecastFormMixinComponent & Vue}
- * @param {boolean} newVal
- */
- handler(/** @type {boolean} */ newVal) {
- this.initFormOption()
- if (!newVal) {
- this.initFormData()
- }
- },
- immediate: true
- },
- /**
- * 监听预测ID变化
- * @param {string|number} val - 新的预测ID
- * @this {ForecastFormMixinComponent & Vue}
- */
- forecastId: {
- /**
- * @this {ForecastFormMixinComponent & Vue}
- * @param {string|number} val
- */
- handler(/** @type {string|number} */ val) {
- if (val && this.isEdit && this.visible) {
- this.loadForecastDetail(val)
- }
- },
- immediate: true
- }
- },
- /**
- * 组件创建时
- * @this {ForecastFormMixinComponent & Vue}
- */
- created() {
- this.initFormOption()
- this.initFormData()
- },
- /**
- * 组件方法
- * @description 组件的业务逻辑方法集合
- */
- methods: {
- /**
- * 创建初始表单数据
- * @description 创建销售预测表单的初始数据结构
- * @returns {ForecastFormModel} 初始化的表单数据对象
- * @this {ForecastFormMixinComponent & Vue}
- * @private
- */
- createInitialFormData() {
- /** @type {ForecastFormModel} */
- const initial = {
- id: null,
- forecastCode: '',
- year: new Date().getFullYear().toString(),
- month: new Date().getMonth() + 1,
- customerId: null,
- customerCode: '',
- customerName: '',
- brandId: null,
- brandCode: '',
- brandName: '',
- itemId: null,
- itemCode: '',
- itemName: '',
- specs: '',
- itemSpecs: '',
- forecastQuantity: null,
- currentInventory: null,
- approvedName: '',
- approvedTime: null,
- approvalRemark: '',
- createTime: null,
- updateTime: null
- }
- return initial
- },
- /**
- * 清理和格式化表单数据
- * @description 对表单数据进行清理和格式化处理
- * @param {Record<string, any>} data - 原始表单数据
- * @returns {ForecastFormModel} 清理和格式化后的表单数据
- * @this {ForecastFormMixinComponent & Vue}
- * @private
- */
- cleanAndFormatFormData(/** @type {Record<string, any>} */ 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 || ''),
- itemSpecs: String(data.itemSpecs || 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,
- approvalRemark: String(data.approvalRemark || ''),
- createTime: data.createTime || null,
- updateTime: data.updateTime || null
- }
- },
- /**
- * 加载销售预测详情
- * @description 根据ID加载销售预测详情数据
- * @param {string|number} id - 销售预测ID
- * @returns {Promise<void>}
- * @this {ForecastFormMixinComponent & Vue}
- * @private
- */
- async loadForecastDetail(/** @type {string|number} */ 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)
- }
- // 映射明细到表格: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 || '',
- // 回显数据可能无库存,先不默认写入 '0',留给后续合并方法填充
- storeInventory: (item.storeInventory !== undefined && item.storeInventory !== null && item.storeInventory !== '') ? String(item.storeInventory) : undefined,
- forecastQuantity: Number(item.forecastQuantity || 0)
- }))
- // 合并接口库存数据以支持回显
- this.mergeEchoStoreInventory && this.mergeEchoStoreInventory().catch(() => {})
- } catch (e) {
- console.warn('映射详情明细失败:', e)
- }
- }
- }
- } catch (error) {
- console.error('加载销售预测详情失败:', error)
- } finally {
- this.formLoading = false
- }
- },
- /**
- * 加载单个客户选项
- * @description 为编辑模式加载特定客户的选项数据
- * @param {string|number} customerId - 客户ID
- * @param {string} customerName - 客户名称
- * @param {string} [customerCode] - 客户编码(可选)
- * @returns {Promise<void>}
- * @this {ForecastFormMixinComponent & Vue}
- */
- async loadCustomerOption(/** @type {string|number} */ customerId, /** @type {string} */ customerName, /** @type {string} */ customerCode) {
- if (!customerId) return
- try {
- // customer-select组件会自动处理回显,我们只需要确保formData中有正确的值
- // 组件的watch会监听value变化并调用loadCustomerById方法
- } catch (error) {
- console.error('加载客户选项失败:', error)
- }
- },
- /**
- * 远程搜索客户
- * @description 根据关键字远程搜索客户数据
- * @param {string} query - 搜索关键字
- * @returns {Promise<void>}
- * @this {ForecastFormMixinComponent & Vue}
- */
- async remoteSearchCustomer(/** @type {string} */ query) {
- if (query === '') {
- this.customerOptions = []
- return
- }
- try {
- this.customerLoading = true
- const response = await getCustomerList(1, 20, {
- customerName: query
- })
- if (response.data && response.data.success && response.data.data) {
- const { records } = response.data.data
- this.customerOptions = records.map(item => ({
- value: item.Customer_ID,
- label: item.Customer_NAME,
- customerCode: item.Customer_CODE
- }))
- }
- } catch (error) {
- console.error('搜索客户失败:', error)
- } finally {
- this.customerLoading = false
- }
- },
- /**
- * 加载当前登录客户信息并填充表单
- * @this {ForecastFormMixinComponent & Vue}
- * @returns {Promise<void>}
- */
- async loadCurrentCustomerInfo() {
- try {
- const response = await getCustomerInfo()
- const ok = response && response.data && response.data.success
- const data = ok ? response.data.data : null
- if (ok && data) {
- // 根据接口common.d.ts中的CustomerInfoData结构进行赋值
- this.formData.customerId = data.Customer_ID ? Number(data.Customer_ID) : null
- this.formData.customerCode = data.Customer_CODE || ''
- this.formData.customerName = data.Customer_NAME || ''
- }
- } catch (e) {
- console.error('获取客户信息失败:', e)
- } finally {
- // 新增模式下,无论客户信息是否获取成功,都应确保物料明细加载一次。
- // 使用表格是否为空作为幂等保护,避免重复加载。
- if (!this.isEdit && Array.isArray(this.stockTableData) && this.stockTableData.length === 0) {
- try {
- await this.loadUserLinkGoods()
- } catch (err) {
- // loadUserLinkGoods 内部已做错误提示,这里静默即可
- }
- }
- }
- },
- /**
- * 加载单个物料选项(用于编辑时显示)
- * @param {string|number} itemId - 物料ID
- * @param {string} itemName - 物料名称
- * @param {string} itemCode - 物料编码
- * @param {string} specs - 物料规格
- * @returns {void}
- * @this {ForecastFormMixinComponent & Vue}
- */
- loadItemOption(/** @type {string|number} */ itemId, /** @type {string} */ itemName, /** @type {string} */ itemCode, /** @type {string} */ specs) {
- if (itemId && itemName && itemCode) {
- const option = {
- label: `${itemName} (${itemCode})`,
- value: itemId,
- itemName,
- itemCode,
- specs: specs || ''
- }
- // 检查是否已存在,避免重复添加
- const exists = this.itemOptions.some(opt => opt.value === itemId)
- if (!exists) {
- this.itemOptions.push(option)
- }
- }
- },
- /**
- * 远程搜索物料
- * @description 根据关键字远程搜索物料数据
- * @param {string} query - 搜索关键字
- * @returns {Promise<void>}
- * @this {ForecastFormMixinComponent & Vue}
- */
- async remoteSearchItem(/** @type {string} */ query) {
- if (query === '') {
- this.itemOptions = []
- return
- }
- try {
- this.itemLoading = true
- const res = await getItemList(1, 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.Item_Name} (${item.Item_Code})`,
- itemName: item.Item_Name,
- itemCode: item.Item_Code,
- specs: item.Item_PECS || '',
- id: item.id
- }))
- }
- } catch (error) {
- console.error('搜索物料失败:', error)
- } finally {
- this.itemLoading = false
- }
- },
- /**
- * 客户选择变化处理
- * @description 处理客户选择变化,更新表单中的客户相关字段
- * @param {string|number} customerId - 客户ID
- * @returns {void}
- * @this {ForecastFormMixinComponent & Vue}
- */
- handleCustomerChange(/** @type {string|number} */ customerId) {
- const customer = this.customerOptions.find(item => item.value === customerId)
- if (customer) {
- this.formData.customerId = typeof customer.value === 'string' ? parseInt(customer.value) || null : 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}
- * @this {ForecastFormMixinComponent & Vue}
- */
- handleItemChange(/** @type {string|number} */ itemId) {
- const item = this.itemOptions.find(option => option.value === itemId)
- if (item) {
- this.formData.itemId = typeof item.value === 'string' ? parseInt(item.value) || null : item.value
- this.formData.itemCode = item.itemCode
- this.formData.itemName = item.itemName
- this.formData.specs = item.specs
- // 触发物料变更事件
- this.$emit(FORECAST_FORM_EVENTS.ITEM_CHANGE, item)
- }
- },
- /**
- * 初始化表单配置
- * @description 根据编辑模式初始化表单配置选项
- * @returns {void}
- * @this {ForecastFormMixinComponent & Vue}
- */
- initFormOption() {
- this.formOption = getFormOption(this.isEdit)
- },
- /**
- * 初始化表单数据
- * @description 根据编辑模式初始化表单数据,新增模式自动填入下个月
- * @returns {void}
- * @this {ForecastFormMixinComponent & Vue}
- */
- initFormData() {
- if (this.isEdit && this.editData) {
- // 编辑模式:使用传入的数据,确保year字段为字符串格式
- this.formData = {
- ...this.editData,
- year: this.editData.year ? this.editData.year.toString() : ''
- }
- // 若编辑入参未包含预测编码,则根据id加载详情以保证回显
- try {
- const id = (this.editData && (this.editData.id || this.editData.Id)) || (this.formData && (this.formData.id || this.formData.Id))
- if (!this.formData.forecastCode && id) {
- this.loadForecastDetail(id)
- }
- } catch (e) {
- // 非关键性异常,忽略
- }
- } else {
- // 新增模式:使用默认数据,自动填入下个月
- const now = new Date()
- const currentYear = now.getFullYear()
- const currentMonth = now.getMonth() + 1
- let nextYear, nextMonth
- if (currentMonth === 12) {
- // 当前是12月,下个月是明年1月
- nextYear = currentYear + 1
- nextMonth = 1
- } else {
- // 其他月份,直接 +1
- nextYear = currentYear
- nextMonth = currentMonth + 1
- }
- this.formData = {
- id: null,
- forecastCode: '',
- year: nextYear.toString(),
- month: nextMonth,
- customerId: null,
- customerCode: '',
- customerName: '',
- brandId: null,
- brandCode: '',
- brandName: '',
- itemId: null,
- itemCode: '',
- itemName: '',
- specs: '',
- itemSpecs: '',
- forecastQuantity: null,
- currentInventory: null,
- approvalStatus: APPROVAL_STATUS.PENDING,
- approvedName: '',
- approvedTime: null,
- approvalRemark: '',
- createTime: null,
- updateTime: null
- }
- // 生成预测编码
- // this.generateForecastCode()
- }
- },
- /**
- * 收集当前可见表单项的必填与数值规则错误信息(用于控制台打印)
- * @this {ForecastFormMixinComponent & Vue}
- * @returns {string[]} 错误消息列表
- */
- collectValidationErrors() {
- try {
- const errors = []
- const option = this.formOption || {}
- const groups = Array.isArray(option.group) ? option.group : []
- const isEmpty = (v) => v === undefined || v === null || v === ''
- groups.forEach(group => {
- const columns = Array.isArray(group.column) ? group.column : []
- columns.forEach(field => {
- if (!field || !field.prop) return
- // 仅校验可见字段
- if (field.display === false) return
- const rules = Array.isArray(field.rules) ? field.rules : []
- const val = this.formData ? this.formData[field.prop] : undefined
- // 必填校验
- const requiredRule = rules.find(r => r && r.required)
- if (requiredRule && isEmpty(val)) {
- const label = field.label || field.prop
- const msg = requiredRule.message || `${label}为必填项`
- errors.push(`${label}: ${msg}`)
- }
- // 数值最小值校验
- const numberRule = rules.find(r => r && r.type === 'number' && (r.min !== undefined))
- if (numberRule && !isEmpty(val)) {
- const num = Number(val)
- if (!Number.isFinite(num) || num < numberRule.min) {
- const label = field.label || field.prop
- const msg = numberRule.message || `${label}必须不小于${numberRule.min}`
- errors.push(`${label}: ${msg}`)
- }
- }
- })
- })
- return errors
- } catch (e) {
- return []
- }
- },
- /**
- * 表单提交事件处理(Avue表单 @submit 入口)
- * @description 响应 avue-form 的提交事件,统一走 submitForm 逻辑
- * @returns {void}
- * @this {ForecastFormMixinComponent & Vue}
- */
- handleSubmit(form, done, loading) {
- try {
- // 先结束 Avue 内置的按钮loading,避免未调用 done 导致一直loading
- if (typeof done === 'function') done()
- console.log(this.formData)
- // 采用旧实现风格:通过 this.$refs.forecastForm.validate 回调进行校验
- if (this.$refs && this.$refs.forecastForm && typeof this.$refs.forecastForm.validate === 'function') {
- this.$refs.forecastForm.validate((valid) => {
- if (!valid) {
- // 编辑态下,收集并打印具体未通过原因
- if (this.isEdit && typeof console !== 'undefined') {
- const errors = this.collectValidationErrors ? this.collectValidationErrors() : []
- if (errors && errors.length) {
- console.group && console.group('表单校验未通过')
- errors.forEach(msg => console.error(msg))
- console.groupEnd && console.groupEnd()
- }
- }
- // 校验失败时,如存在 loading 回调(部分版本提供),尝试恢复按钮状态
- if (typeof loading === 'function') loading()
- // 通知父组件校验失败,便于父侧重置保存按钮loading
- this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_ERROR, { message: '表单校验未通过' })
- return
- }
- // 校验通过后执行提交
- this.submitForm()
- .catch((e) => {
- console.error('提交异常:', e)
- // 将错误交由父组件统一处理,避免重复toast
- this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_ERROR, e)
- })
- })
- } else {
- // 无法获取到 validate 时,直接尝试提交
- this.submitForm()
- .catch((e) => {
- console.error('提交异常:', e)
- // 将错误交由父组件统一处理,避免重复toast
- this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_ERROR, e)
- })
- }
- } catch (e) {
- console.error('提交异常:', e)
- // 将错误交由父组件统一处理,避免重复toast
- this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_ERROR, e)
- }
- },
- /**
- * 提交表单数据(main-add:提交销售预测主表及子项明细)
- * @returns {Promise<void>}
- * @this {ForecastFormMixinComponent & Vue}
- */
- async submitForm() {
- try {
- // 基础校验(客户必选)
- if (!this.formData.customerId) {
- this.$message && this.$message.warning('请选择客户')
- // 通知父组件失败,重置保存按钮loading
- this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_ERROR, { message: '未选择客户' })
- return
- }
- // 转换年份与月份
- const year = typeof this.formData.year === 'string' ? parseInt(this.formData.year, 10) : this.formData.year
- 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))
- 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)
- }
- }
- // 安全的数值转换(用于数量等非ID字段):若不可安全表示整数,仍以字符串传输
- /** @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)
- }
- // 组装子项明细,仅保留预测数量>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
- })
- // 新增模式下需要至少一条有效明细;编辑模式下仅提交主表四个字段,不校验明细条数
- if (!this.isEdit && !items.length) {
- this.$message && this.$message.warning('请至少填写一条有效的预测数量')
- // 通知父组件失败,便于父侧重置保存按钮loading
- this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_ERROR, { message: '未填写有效的预测明细' })
- return
- }
- // 组装载荷
- const payloadBase = {
- year: year || new Date().getFullYear(),
- month: month || (new Date().getMonth() + 1),
- approvalStatus: Number(this.formData.approvalStatus ?? 0),
- pcBladeSalesForecastSummaryList: items
- }
- 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.$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)) || '提交失败'
- if (typeof this.setYearMonthDisabled === 'function') {
- this.setYearMonthDisabled(false)
- } else if (this.$refs) {
- this.$nextTick(() => {
- try {
- if (this.$refs.yearPicker) this.$refs.yearPicker.disabled = false
- if (this.$refs.monthSelect) this.$refs.monthSelect.disabled = false
- } catch (e) { /* 忽略 */ }
- })
- }
- this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_ERROR, { message: msg, response: res })
- }
- } catch (error) {
- console.error('提交表单失败:', error)
- this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_ERROR, error)
- }
- },
- /**
- * 客户选择事件处理
- * @description 处理CustomerSelect组件的客户选择事件
- * @param {CustomerSelectData} customerData - 客户选择数据
- * @returns {void}
- * @this {ForecastFormMixinComponent & Vue}
- */
- handleCustomerSelected(/** @type {import('./types').CustomerSelectData} */ customerData) {
- if (customerData && customerData.customerId) {
- this.formData.customerId = Number(customerData.customerId)
- this.formData.customerCode = customerData.customerCode
- this.formData.customerName = customerData.customerName
- // 选中客户后加载该用户关联的品牌与库存物料(仅新增模式自动加载,编辑模式不覆盖回显数据)
- if (!this.isEdit) {
- 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 = []
- this.stockDescList = []
- this.stockSelectOptions = []
- this.selectedStockId = null
- 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
- // 存储库存列表供选择用,不直接展示到表格
- this.stockDescList = stockList
- // 构造下拉选项,label 使用 cname,value 使用 id
- this.stockSelectOptions = stockList.map(item => ({
- label: item.cname,
- value: item.id
- }))
- // 默认显示全部物料至下方表格,预测数量默认 0,用户可手动删除不需要的物料
- this.stockTableData = stockList.map(item => ({ ...item, forecastQuantity: 1 }))
- } catch (e) {
- console.error('加载用户关联商品失败:', e)
- this.$message.error(e.message || '加载用户关联商品失败')
- } finally {
- this.tableLoading = false
- }
- },
- /**
- * 导入所选物料到下方表格
- * @description 仅在点击"导入物料"按钮后,将选择的物料行添加到表格,默认预测数量为 1
- * @returns {void}
- * @this {ForecastFormMixinComponent & Vue}
- */
- handleImportSelectedStock() {
- // 未选择则提示
- if (!this.selectedStockId) {
- this.$message.warning('请先在上方选择要导入的物料')
- return
- }
- // 查找明细
- const stock = this.stockDescList.find(s => s.id === this.selectedStockId)
- if (!stock) {
- this.$message.error('未找到所选物料数据,请重新选择')
- return
- }
-
- // 防止重复导入 - 使用多个字段进行更全面的重复检查
- const exists = this.stockTableData.some(row => {
- // 优先使用 id 进行匹配
- if (row.id && stock.id && row.id === stock.id) {
- return true
- }
- // 使用 goodsId 进行匹配
- if (row.goodsId && stock.goodsId && row.goodsId === stock.goodsId) {
- return true
- }
- // 使用 code 进行匹配
- if (row.code && stock.code && row.code === stock.code) {
- return true
- }
- return false
- })
-
- if (exists) {
- this.$message.warning('该物料已在列表中')
- this.selectedStockId = null
- return
- }
-
- // 添加到表格,默认预测数量为 1
- this.stockTableData.push({ ...stock, forecastQuantity: 1 })
- // 清空已选
- this.selectedStockId = null
- },
- /**
- * 删除物料行
- * @description 在下方物料表格中删除指定行,包含二次确认流程;删除后保持数据与UI同步。
- * @param {import('./types').ForecastFormMixinData['stockTableData'][number]} row - 待删除的表格行数据
- * @param {number} index - 行索引
- * @returns {Promise<void>}
- * @this {import('./types').ForecastFormMixinComponent & Vue}
- */
- async handleDelete(row, index) {
- try {
- // 索引校验,必要时根据唯一标识兜底定位
- let removeIndex = typeof index === 'number' ? index : -1
- if (removeIndex < 0 || removeIndex >= this.stockTableData.length) {
- const keyId = row && (row.id != null ? row.id : row.goodsId)
- removeIndex = this.stockTableData.findIndex(r => (r.id != null ? r.id : r.goodsId) === keyId)
- }
- if (removeIndex < 0) {
- this.$message && this.$message.warning('未定位到要删除的记录')
- return
- }
- // 二次确认
- await this.$confirm('确认删除该物料吗?删除后可重新通过上方选择器导入。', '提示', {
- type: 'warning',
- confirmButtonText: '删除',
- cancelButtonText: '取消'
- })
- // 使用 Vue.set/delete 保持响应式
- this.$delete(this.stockTableData, removeIndex)
- // 如有需要,清理与该行相关的临时状态(当前实现无行级临时状态)
- // 例如:this.currentInventory = null
- this.$message && this.$message.success('已删除')
- } catch (e) {
- // 用户取消不提示为错误,其他情况做日志记录
- if (e && e !== 'cancel') {
- console.error('删除失败:', e)
- this.$message && this.$message.error('删除失败,请稍后重试')
- }
- }
- },
- /**
- * 品牌变更处理
- * @param {number} brandId - 品牌ID
- * @returns {void}
- * @this {ForecastFormMixinComponent & Vue}
- */
- handleBrandChange(/** @type {number} */ brandId) {
- const selectedBrand = this.brandOptions.find(brand => /** @type {any} */ (brand).id === brandId)
- if (selectedBrand) {
- this.formData.brandId = brandId
- this.formData.brandCode = /** @type {any} */ (selectedBrand).code
- this.formData.brandName = /** @type {any} */ (selectedBrand).name
- } else {
- this.formData.brandId = null
- this.formData.brandCode = ''
- this.formData.brandName = ''
- }
- },
- /**
- * 物料选择处理
- * @description 处理MaterialSelect组件的物料选择事件
- * @param {MaterialSelectData} materialData - 物料选择数据
- * @returns {void}
- * @this {ForecastFormMixinComponent & Vue}
- */
- handleMaterialSelected(/** @type {import('./types').MaterialSelectData} */ materialData) {
- if (materialData && materialData.itemId) {
- this.formData.itemId = Number(materialData.itemId)
- this.formData.itemCode = materialData.itemCode
- this.formData.itemName = materialData.itemName
- this.formData.itemSpecs = materialData.specification || ''
- // 获取当前库存
- this.getCurrentInventory(materialData.itemId)
- } else {
- this.formData.itemId = null
- this.formData.itemCode = ''
- this.formData.itemName = ''
- this.formData.itemSpecs = ''
- this.currentInventory = null
- }
- },
- /**
- * 合并回显行的库存数量
- * @description 使用 getUserLinkGoods 接口返回的库存数据,为编辑态回显的物料行补齐 storeInventory 字段
- * @returns {Promise<void>}
- */
- async mergeEchoStoreInventory() {
- try {
- if (!Array.isArray(this.stockTableData) || this.stockTableData.length === 0) return
- const res = await getUserLinkGoods()
- const payload = res && res.data && res.data.data ? res.data.data : null
- const stockList = (payload && payload.pjpfStockDescList) || []
- if (!Array.isArray(stockList) || stockList.length === 0) return
- // 在编辑模式下,确保"导入物料"的选择器有数据可选
- // 不修改现有表格数据,仅补齐选择来源
- this.stockDescList = stockList
- this.stockSelectOptions = stockList.map(item => ({
- label: item.cname,
- value: item.id
- }))
- // 构建基于 goodsId 与 code 的索引映射
- /** @type {Map<string, string|undefined>} */
- const invByGoodsId = new Map()
- /** @type {Map<string, string|undefined>} */
- const invByCode = new Map()
- stockList.forEach(s => {
- const inv = (s && s.storeInventory !== undefined && s.storeInventory !== null && s.storeInventory !== '') ? String(s.storeInventory) : undefined
- if (s && s.goodsId !== undefined && s.goodsId !== null) invByGoodsId.set(String(s.goodsId), inv)
- if (s && s.code) invByCode.set(String(s.code), inv)
- })
- // 合并库存到现有表格数据(仅填充缺失的库存字段)
- this.stockTableData = this.stockTableData.map(row => {
- const hasInv = !(row.storeInventory === undefined || row.storeInventory === null || row.storeInventory === '')
- if (hasInv) return row
- const keyGoodsId = row && row.goodsId !== undefined && row.goodsId !== null ? String(row.goodsId) : ''
- const keyCode = row && row.code ? String(row.code) : ''
- const fromGoods = keyGoodsId ? invByGoodsId.get(keyGoodsId) : undefined
- const fromCode = (!fromGoods && keyCode) ? invByCode.get(keyCode) : undefined
- const value = (fromGoods !== undefined && fromGoods !== null && fromGoods !== '') ? fromGoods : ((fromCode !== undefined && fromCode !== null && fromCode !== '') ? fromCode : '0')
- return { ...row, storeInventory: String(value) }
- })
- } catch (e) {
- console.warn('回显库存合并失败:', e)
- }
- }
- }
- }
|