1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348 |
- // @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()
- }
- },
- /**
- * 收集当前可见表单项的必填与数值规则错误信息(用于控制台打印)
- * @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)
- }
- }
- }
- }
|