forecast-form-mixin.js 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348
  1. // @ts-check
  2. /* global BigInt */
  3. /**
  4. * @fileoverview 销售预测表单混入组件
  5. * @description 提供销售预测表单的数据管理、验证规则和业务逻辑的混入组件,支持新增和编辑模式
  6. * @this {ForecastFormMixinComponent & Vue}
  7. */
  8. /**
  9. * 类型定义导入
  10. * @description 导入所有必要的TypeScript类型定义,确保类型安全
  11. */
  12. /**
  13. * @typedef {import('./types').ForecastFormModel} ForecastFormModel
  14. * @description 销售预测表单数据模型类型
  15. */
  16. /**
  17. * @typedef {import('./types').ForecastFormMixinData} ForecastFormMixinData
  18. * @description 销售预测表单混入数据类型
  19. */
  20. /**
  21. * @typedef {import('./types').CustomerOption} CustomerOption
  22. * @description 客户选项类型
  23. */
  24. /**
  25. * @typedef {import('./types').ItemOption} ItemOption
  26. * @description 物料选项类型
  27. */
  28. /**
  29. * @typedef {import('./types').ApprovalStatusOption} ApprovalStatusOption
  30. * @description 审批状态选项类型
  31. */
  32. /**
  33. * @typedef {import('./types').ForecastFormRules} ForecastFormRules
  34. * @description 销售预测表单验证规则类型
  35. */
  36. /**
  37. * @typedef {import('./types').MaterialSelectData} MaterialSelectData
  38. * @description 物料选择数据类型
  39. */
  40. /**
  41. * @typedef {import('./types').CustomerSelectData} CustomerSelectData
  42. * @description 客户选择数据类型
  43. */
  44. /**
  45. * @typedef {import('./types').ForecastFormMixinComponent} ForecastFormMixinComponent
  46. * @description 销售预测表单混入组件类型
  47. */
  48. // API接口导入
  49. import { addForecast, updateForecast, getForecastDetail } from '@/api/forecast'
  50. import { addSalesForecastMain, updateSalesForecastMain } from '@/api/forecast/forecast-summary'
  51. import { getUserLinkGoods } from '@/api/order/sales-order'
  52. // 常量和枚举导入
  53. import {
  54. APPROVAL_STATUS,
  55. APPROVAL_STATUS_OPTIONS,
  56. FORECAST_FORM_RULES,
  57. DEFAULT_FORECAST_FORM,
  58. getApprovalStatusLabel,
  59. getApprovalStatusType,
  60. canEdit
  61. } from '@/constants/forecast'
  62. // 远程搜索API
  63. import { getCustomerList, getItemList, getCustomerInfo } from '@/api/common'
  64. // 表单配置导入
  65. import { getFormOption } from './form-option'
  66. import { safeBigInt } from '@/util/util'
  67. /**
  68. * 销售预测表单事件常量
  69. * @readonly
  70. */
  71. export const FORECAST_FORM_EVENTS = {
  72. /** 表单提交成功事件 */
  73. SUBMIT_SUCCESS: 'submit-success',
  74. /** 表单取消事件 */
  75. CANCEL: 'cancel',
  76. /** 表单加载完成事件 */
  77. LOADED: 'loaded',
  78. /** 客户选择变更事件 */
  79. CUSTOMER_CHANGE: 'customer-change',
  80. /** 物料选择变更事件 */
  81. ITEM_CHANGE: 'item-change',
  82. /** 表单重置事件 */
  83. RESET: 'reset',
  84. /** 表单提交事件 */
  85. SUBMIT: 'submit',
  86. /** 表单提交失败事件 */
  87. SUBMIT_ERROR: 'submit-error',
  88. /** 更新可见性事件 */
  89. UPDATE_VISIBLE: 'update:visible'
  90. }
  91. /**
  92. * 销售预测表单混入
  93. * @description 提供销售预测表单的数据管理、验证规则和业务逻辑
  94. * @mixin
  95. */
  96. export default {
  97. /**
  98. * 组件名称
  99. */
  100. name: 'ForecastFormMixin',
  101. /**
  102. * 组件属性定义
  103. * @description 定义组件接收的外部属性及其类型约束
  104. */
  105. props: {
  106. /**
  107. * 表单可见性控制
  108. * @description 控制表单的显示和隐藏
  109. */
  110. visible: {
  111. type: Boolean,
  112. default: false
  113. },
  114. /**
  115. * 编辑模式标识
  116. * @description 标识当前表单是否处于编辑模式
  117. */
  118. isEdit: {
  119. type: Boolean,
  120. default: false
  121. },
  122. /**
  123. * 初始表单数据
  124. * @description 用于表单初始化的数据对象
  125. */
  126. initialFormData: {
  127. type: Object,
  128. default: null
  129. },
  130. /**
  131. * 表单标题
  132. * @description 自定义表单标题,如果不提供则根据编辑模式自动生成
  133. */
  134. title: {
  135. type: String,
  136. default: ''
  137. },
  138. /**
  139. * 编辑时的表单数据
  140. */
  141. editData: {
  142. type: Object,
  143. default: () => ({})
  144. }
  145. },
  146. /**
  147. * 组件响应式数据
  148. * @description 定义组件的响应式数据状态
  149. * @this {ForecastFormMixinComponent & Vue}
  150. * @returns {ForecastFormMixinData} 组件数据对象
  151. */
  152. data() {
  153. return {
  154. /**
  155. * 销售预测表单数据模型
  156. * @description 存储销售预测表单的所有字段数据
  157. * @type {ForecastFormModel}
  158. */
  159. formData: {
  160. id: null,
  161. forecastCode: '',
  162. year: new Date().getFullYear().toString(),
  163. month: new Date().getMonth() + 1,
  164. customerId: null,
  165. customerCode: '',
  166. customerName: '',
  167. brandId: null,
  168. brandCode: '',
  169. brandName: '',
  170. itemId: null,
  171. itemCode: '',
  172. itemName: '',
  173. specs: '',
  174. itemSpecs: '',
  175. forecastQuantity: null,
  176. currentInventory: null,
  177. approvedName: '',
  178. approvedTime: null,
  179. approvalRemark: '',
  180. createTime: null,
  181. updateTime: null
  182. },
  183. /** 保存操作加载状态 */
  184. saveLoading: false,
  185. /** 表单加载状态 */
  186. formLoading: false,
  187. /** 客户选项列表
  188. * @type {Array<CustomerOption>}
  189. */
  190. customerOptions: [],
  191. /** 客户选项加载状态 */
  192. customerLoading: false,
  193. /** 物料选项列表
  194. * @type {Array<ItemOption>}
  195. */
  196. itemOptions: [],
  197. /** 物料选项加载状态 */
  198. itemLoading: false,
  199. /** 审批状态选项列表
  200. * @type {Array<ApprovalStatusOption>}
  201. */
  202. approvalStatusOptions: APPROVAL_STATUS_OPTIONS,
  203. /** 表单验证规则
  204. * @type {ForecastFormRules}
  205. */
  206. formRules: {
  207. ...FORECAST_FORM_RULES,
  208. year: [
  209. { required: true, message: '请选择年份', trigger: 'blur' }
  210. ]
  211. },
  212. /** 表单配置
  213. * @type {import('./types').FormOption}
  214. */
  215. formOption: {
  216. column: []
  217. },
  218. /** 品牌选项列表
  219. * @type {Array<SelectOption<number>>}
  220. */
  221. brandOptions: [],
  222. /** 物料表格数据(来自用户关联商品 pjpfStockDescList),带预测数量字段 */
  223. /** @type {Array<import('@/api/types/order').PjpfStockDesc & { forecastQuantity: number, brandCode?: string }>} */
  224. stockTableData: [],
  225. /** 表格加载状态 */
  226. tableLoading: false,
  227. /** 品牌描述列表(用于品牌信息匹配) */
  228. /** @type {Array<import('@/api/types/order').PjpfBrandDesc>} */
  229. brandDescList: [],
  230. /**
  231. * 用户关联库存物料列表(不直接展示在表格中)
  232. * @type {Array<import('@/api/types/order').PjpfStockDesc>}
  233. */
  234. stockDescList: [],
  235. /**
  236. * 物料选择下拉选项(通过 cname 搜索)
  237. * @type {Array<SelectOption<string>>}
  238. */
  239. stockSelectOptions: [],
  240. /**
  241. * 当前选择待导入的物料ID
  242. * @type {string | null}
  243. */
  244. selectedStockId: null,
  245. /** 当前库存 */
  246. currentInventory: null
  247. }
  248. },
  249. /**
  250. * 计算属性
  251. * @description 组件的响应式计算属性
  252. */
  253. computed: {
  254. /**
  255. * 表单标题
  256. * @description 根据编辑模式动态显示表单标题
  257. * @this {ForecastFormMixinComponent & Vue}
  258. * @returns {string} 表单标题文本
  259. */
  260. formTitle() {
  261. if (this.title) {
  262. return this.title
  263. }
  264. return this.isEdit ? '编辑销售预测' : '新增销售预测'
  265. }
  266. },
  267. /**
  268. * 侦听器
  269. * @description 监听属性变化并执行相应操作
  270. */
  271. watch: {
  272. /**
  273. * 监听表单可见性变化
  274. * @this {ForecastFormMixinComponent & Vue}
  275. */
  276. visible: {
  277. /**
  278. * @this {ForecastFormMixinComponent & Vue}
  279. * @param {boolean} val - 新的可见性值
  280. */
  281. handler(/** @type {boolean} */ val) {
  282. if (val) {
  283. this.$nextTick(() => {
  284. // 表单显示时,初始化表单数据
  285. if (this.initialFormData) {
  286. this.formData = this.cleanAndFormatFormData(this.initialFormData)
  287. } else {
  288. this.formData = this.createInitialFormData()
  289. }
  290. // 如果是编辑模式且有ID,则加载详情数据
  291. if (this.isEdit && this.formData.id) {
  292. this.loadForecastDetail(this.formData.id)
  293. }
  294. // 如果不是编辑模式,则生成预测编码
  295. if (!this.isEdit && !this.formData.forecastCode) {
  296. // this.generateForecastCode()
  297. }
  298. // 新增模式下,自动获取并填充客户信息
  299. if (!this.isEdit) {
  300. this.loadCurrentCustomerInfo()
  301. }
  302. })
  303. }
  304. },
  305. immediate: true
  306. },
  307. /**
  308. * 监听初始表单数据变化
  309. * @param {ForecastFormModel} val - 新的初始表单数据
  310. * @this {ForecastFormMixinComponent & Vue}
  311. */
  312. initialFormData(/** @type {ForecastFormModel} */ val) {
  313. if (val) {
  314. this.formData = this.cleanAndFormatFormData(val)
  315. }
  316. },
  317. /**
  318. * 监听编辑数据变化
  319. * @this {ForecastFormMixinComponent & Vue}
  320. */
  321. editData: {
  322. /**
  323. * @this {ForecastFormMixinComponent & Vue}
  324. * @param {ForecastFormModel} newData
  325. */
  326. handler(/** @type {ForecastFormModel} */ newData) {
  327. if (newData && this.isEdit) {
  328. this.formData = {
  329. ...newData,
  330. year: newData.year ? newData.year.toString() : ''
  331. }
  332. // 回显子项明细到物料表格:将 pcBladeSalesForecastSummaryList -> stockTableData
  333. if (Array.isArray(newData.pcBladeSalesForecastSummaryList)) {
  334. try {
  335. this.stockTableData = newData.pcBladeSalesForecastSummaryList.map(item => ({
  336. // 尽量保持与 PjpfStockDesc 结构一致,便于表格渲染
  337. id: item.id ? safeBigInt(item.id) : undefined,
  338. goodsId: item.itemId ? safeBigInt(item.itemId) : undefined,
  339. code: item.itemCode || '',
  340. cname: item.itemName || '',
  341. brandId: item.brandId ? safeBigInt(item.brandId) : undefined,
  342. brandCode: item.brandCode || '',
  343. brandName: item.brandName || '',
  344. typeNo: item.specs || '',
  345. productDescription: item.pattern || '',
  346. brandItem: item.pattern || '',
  347. // 回显数据可能无库存,先不默认写入 '0',留给后续合并方法填充
  348. storeInventory: undefined,
  349. // 预测数量用于编辑
  350. forecastQuantity: Number(item.forecastQuantity || 0)
  351. }))
  352. // 合并接口库存数据以支持回显
  353. this.mergeEchoStoreInventory && this.mergeEchoStoreInventory().catch(() => {})
  354. } catch (e) {
  355. console.warn('映射回显明细失败:', e)
  356. }
  357. }
  358. }
  359. },
  360. immediate: true,
  361. deep: true
  362. },
  363. /**
  364. * 监听编辑模式变化
  365. * @this {ForecastFormMixinComponent & Vue}
  366. */
  367. isEdit: {
  368. /**
  369. * @this {ForecastFormMixinComponent & Vue}
  370. * @param {boolean} newVal
  371. */
  372. handler(/** @type {boolean} */ newVal) {
  373. this.initFormOption()
  374. if (!newVal) {
  375. this.initFormData()
  376. }
  377. },
  378. immediate: true
  379. },
  380. /**
  381. * 监听预测ID变化
  382. * @param {string|number} val - 新的预测ID
  383. * @this {ForecastFormMixinComponent & Vue}
  384. */
  385. forecastId: {
  386. /**
  387. * @this {ForecastFormMixinComponent & Vue}
  388. * @param {string|number} val
  389. */
  390. handler(/** @type {string|number} */ val) {
  391. if (val && this.isEdit && this.visible) {
  392. this.loadForecastDetail(val)
  393. }
  394. },
  395. immediate: true
  396. }
  397. },
  398. /**
  399. * 组件创建时
  400. * @this {ForecastFormMixinComponent & Vue}
  401. */
  402. created() {
  403. this.initFormOption()
  404. this.initFormData()
  405. },
  406. /**
  407. * 组件方法
  408. * @description 组件的业务逻辑方法集合
  409. */
  410. methods: {
  411. /**
  412. * 创建初始表单数据
  413. * @description 创建销售预测表单的初始数据结构
  414. * @returns {ForecastFormModel} 初始化的表单数据对象
  415. * @this {ForecastFormMixinComponent & Vue}
  416. * @private
  417. */
  418. createInitialFormData() {
  419. /** @type {ForecastFormModel} */
  420. const initial = {
  421. id: null,
  422. forecastCode: '',
  423. year: new Date().getFullYear().toString(),
  424. month: new Date().getMonth() + 1,
  425. customerId: null,
  426. customerCode: '',
  427. customerName: '',
  428. brandId: null,
  429. brandCode: '',
  430. brandName: '',
  431. itemId: null,
  432. itemCode: '',
  433. itemName: '',
  434. specs: '',
  435. itemSpecs: '',
  436. forecastQuantity: null,
  437. currentInventory: null,
  438. approvedName: '',
  439. approvedTime: null,
  440. approvalRemark: '',
  441. createTime: null,
  442. updateTime: null
  443. }
  444. return initial
  445. },
  446. /**
  447. * 清理和格式化表单数据
  448. * @description 对表单数据进行清理和格式化处理
  449. * @param {Record<string, any>} data - 原始表单数据
  450. * @returns {ForecastFormModel} 清理和格式化后的表单数据
  451. * @this {ForecastFormMixinComponent & Vue}
  452. * @private
  453. */
  454. cleanAndFormatFormData(/** @type {Record<string, any>} */ data) {
  455. // 获取下个月的年份和月份作为默认值
  456. const now = new Date()
  457. const currentYear = now.getFullYear()
  458. const currentMonth = now.getMonth() + 1
  459. let defaultYear, defaultMonth
  460. if (currentMonth === 12) {
  461. // 当前是12月,下个月是明年1月
  462. defaultYear = currentYear + 1
  463. defaultMonth = 1
  464. } else {
  465. // 其他月份,直接 +1
  466. defaultYear = currentYear
  467. defaultMonth = currentMonth + 1
  468. }
  469. return {
  470. id: data.id || null,
  471. forecastCode: String(data.forecastCode || ''),
  472. year: data.year ? data.year.toString() : defaultYear.toString(),
  473. month: Number(data.month) || defaultMonth,
  474. customerId: data.customerId ? data.customerId.toString() : null,
  475. customerCode: String(data.customerCode || ''),
  476. customerName: String(data.customerName || ''),
  477. brandId: Number(data.brandId) || null,
  478. brandCode: String(data.brandCode || ''),
  479. brandName: String(data.brandName || ''),
  480. itemId: data.itemId ? data.itemId.toString() : null,
  481. itemCode: String(data.itemCode || ''),
  482. itemName: String(data.itemName || ''),
  483. specs: String(data.specs || ''),
  484. itemSpecs: String(data.itemSpecs || data.specs || ''),
  485. forecastQuantity: data.forecastQuantity !== undefined && data.forecastQuantity !== null && data.forecastQuantity !== '' ? Number(data.forecastQuantity) : null,
  486. currentInventory: Number(data.currentInventory) || null,
  487. approvalStatus: Number(data.approvalStatus) || APPROVAL_STATUS.PENDING,
  488. approvedName: String(data.approvedName || ''),
  489. approvedTime: data.approvedTime || null,
  490. approvalRemark: String(data.approvalRemark || ''),
  491. createTime: data.createTime || null,
  492. updateTime: data.updateTime || null
  493. }
  494. },
  495. /**
  496. * 加载销售预测详情
  497. * @description 根据ID加载销售预测详情数据
  498. * @param {string|number} id - 销售预测ID
  499. * @returns {Promise<void>}
  500. * @this {ForecastFormMixinComponent & Vue}
  501. * @private
  502. */
  503. async loadForecastDetail(/** @type {string|number} */ id) {
  504. if (!id) return
  505. try {
  506. this.formLoading = true
  507. const res = await getForecastDetail(id)
  508. if (res.data && res.data.success && res.data.data) {
  509. const detailData = res.data.data
  510. this.formData = this.cleanAndFormatFormData(detailData)
  511. // 加载客户选项数据,确保客户下拉框能正确显示
  512. if (this.formData.customerId) {
  513. await this.loadCustomerOption(this.formData.customerId, this.formData.customerName)
  514. }
  515. // 加载物料选项数据,确保物料下拉框能正确显示
  516. if (this.formData.itemId) {
  517. await this.loadItemOption(this.formData.itemId, this.formData.itemName, this.formData.itemCode, this.formData.specs)
  518. }
  519. // 映射明细到表格:pcBladeSalesForecastSummaryList -> stockTableData
  520. if (Array.isArray(detailData.pcBladeSalesForecastSummaryList)) {
  521. try {
  522. this.stockTableData = detailData.pcBladeSalesForecastSummaryList.map(item => ({
  523. id: item.id != null ? item.id : undefined,
  524. goodsId: item.itemId != null ? item.itemId : undefined,
  525. code: item.itemCode || '',
  526. cname: item.itemName || '',
  527. brandId: item.brandId != null ? item.brandId : undefined,
  528. brandCode: item.brandCode || '',
  529. brandName: item.brandName || '',
  530. typeNo: item.specs || '',
  531. productDescription: item.pattern || '',
  532. brandItem: item.pattern || '',
  533. // 回显数据可能无库存,先不默认写入 '0',留给后续合并方法填充
  534. storeInventory: (item.storeInventory !== undefined && item.storeInventory !== null && item.storeInventory !== '') ? String(item.storeInventory) : undefined,
  535. forecastQuantity: Number(item.forecastQuantity || 0)
  536. }))
  537. // 合并接口库存数据以支持回显
  538. this.mergeEchoStoreInventory && this.mergeEchoStoreInventory().catch(() => {})
  539. } catch (e) {
  540. console.warn('映射详情明细失败:', e)
  541. }
  542. }
  543. }
  544. } catch (error) {
  545. console.error('加载销售预测详情失败:', error)
  546. } finally {
  547. this.formLoading = false
  548. }
  549. },
  550. /**
  551. * 加载单个客户选项
  552. * @description 为编辑模式加载特定客户的选项数据
  553. * @param {string|number} customerId - 客户ID
  554. * @param {string} customerName - 客户名称
  555. * @param {string} [customerCode] - 客户编码(可选)
  556. * @returns {Promise<void>}
  557. * @this {ForecastFormMixinComponent & Vue}
  558. */
  559. async loadCustomerOption(/** @type {string|number} */ customerId, /** @type {string} */ customerName, /** @type {string} */ customerCode) {
  560. if (!customerId) return
  561. try {
  562. // customer-select组件会自动处理回显,我们只需要确保formData中有正确的值
  563. // 组件的watch会监听value变化并调用loadCustomerById方法
  564. } catch (error) {
  565. console.error('加载客户选项失败:', error)
  566. }
  567. },
  568. /**
  569. * 远程搜索客户
  570. * @description 根据关键字远程搜索客户数据
  571. * @param {string} query - 搜索关键字
  572. * @returns {Promise<void>}
  573. * @this {ForecastFormMixinComponent & Vue}
  574. */
  575. async remoteSearchCustomer(/** @type {string} */ query) {
  576. if (query === '') {
  577. this.customerOptions = []
  578. return
  579. }
  580. try {
  581. this.customerLoading = true
  582. const response = await getCustomerList(1, 20, {
  583. customerName: query
  584. })
  585. if (response.data && response.data.success && response.data.data) {
  586. const { records } = response.data.data
  587. this.customerOptions = records.map(item => ({
  588. value: item.Customer_ID,
  589. label: item.Customer_NAME,
  590. customerCode: item.Customer_CODE
  591. }))
  592. }
  593. } catch (error) {
  594. console.error('搜索客户失败:', error)
  595. } finally {
  596. this.customerLoading = false
  597. }
  598. },
  599. /**
  600. * 加载当前登录客户信息并填充表单
  601. * @this {ForecastFormMixinComponent & Vue}
  602. * @returns {Promise<void>}
  603. */
  604. async loadCurrentCustomerInfo() {
  605. try {
  606. const response = await getCustomerInfo()
  607. const ok = response && response.data && response.data.success
  608. const data = ok ? response.data.data : null
  609. if (ok && data) {
  610. // 根据接口common.d.ts中的CustomerInfoData结构进行赋值
  611. this.formData.customerId = data.Customer_ID ? Number(data.Customer_ID) : null
  612. this.formData.customerCode = data.Customer_CODE || ''
  613. this.formData.customerName = data.Customer_NAME || ''
  614. }
  615. } catch (e) {
  616. console.error('获取客户信息失败:', e)
  617. } finally {
  618. // 新增模式下,无论客户信息是否获取成功,都应确保物料明细加载一次。
  619. // 使用表格是否为空作为幂等保护,避免重复加载。
  620. if (!this.isEdit && Array.isArray(this.stockTableData) && this.stockTableData.length === 0) {
  621. try {
  622. await this.loadUserLinkGoods()
  623. } catch (err) {
  624. // loadUserLinkGoods 内部已做错误提示,这里静默即可
  625. }
  626. }
  627. }
  628. },
  629. /**
  630. * 加载单个物料选项(用于编辑时显示)
  631. * @param {string|number} itemId - 物料ID
  632. * @param {string} itemName - 物料名称
  633. * @param {string} itemCode - 物料编码
  634. * @param {string} specs - 物料规格
  635. * @returns {void}
  636. * @this {ForecastFormMixinComponent & Vue}
  637. */
  638. loadItemOption(/** @type {string|number} */ itemId, /** @type {string} */ itemName, /** @type {string} */ itemCode, /** @type {string} */ specs) {
  639. if (itemId && itemName && itemCode) {
  640. const option = {
  641. label: `${itemName} (${itemCode})`,
  642. value: itemId,
  643. itemName,
  644. itemCode,
  645. specs: specs || ''
  646. }
  647. // 检查是否已存在,避免重复添加
  648. const exists = this.itemOptions.some(opt => opt.value === itemId)
  649. if (!exists) {
  650. this.itemOptions.push(option)
  651. }
  652. }
  653. },
  654. /**
  655. * 远程搜索物料
  656. * @description 根据关键字远程搜索物料数据
  657. * @param {string} query - 搜索关键字
  658. * @returns {Promise<void>}
  659. * @this {ForecastFormMixinComponent & Vue}
  660. */
  661. async remoteSearchItem(/** @type {string} */ query) {
  662. if (query === '') {
  663. this.itemOptions = []
  664. return
  665. }
  666. try {
  667. this.itemLoading = true
  668. const res = await getItemList(1, 10, {
  669. itemName: query
  670. })
  671. if (res.data && res.data.success && res.data.data) {
  672. const { records } = res.data.data
  673. this.itemOptions = records.map(item => ({
  674. value: item.id,
  675. label: `${item.Item_Name} (${item.Item_Code})`,
  676. itemName: item.Item_Name,
  677. itemCode: item.Item_Code,
  678. specs: item.Item_PECS || '',
  679. id: item.id
  680. }))
  681. }
  682. } catch (error) {
  683. console.error('搜索物料失败:', error)
  684. } finally {
  685. this.itemLoading = false
  686. }
  687. },
  688. /**
  689. * 客户选择变化处理
  690. * @description 处理客户选择变化,更新表单中的客户相关字段
  691. * @param {string|number} customerId - 客户ID
  692. * @returns {void}
  693. * @this {ForecastFormMixinComponent & Vue}
  694. */
  695. handleCustomerChange(/** @type {string|number} */ customerId) {
  696. const customer = this.customerOptions.find(item => item.value === customerId)
  697. if (customer) {
  698. this.formData.customerId = typeof customer.value === 'string' ? parseInt(customer.value) || null : customer.value
  699. this.formData.customerCode = customer.customerCode
  700. this.formData.customerName = customer.label
  701. // 触发客户变更事件
  702. this.$emit(FORECAST_FORM_EVENTS.CUSTOMER_CHANGE, customer)
  703. }
  704. },
  705. /**
  706. * 物料选择变化处理
  707. * @description 处理物料选择变化,更新表单中的物料相关字段
  708. * @param {string|number} itemId - 物料ID
  709. * @returns {void}
  710. * @this {ForecastFormMixinComponent & Vue}
  711. */
  712. handleItemChange(/** @type {string|number} */ itemId) {
  713. const item = this.itemOptions.find(option => option.value === itemId)
  714. if (item) {
  715. this.formData.itemId = typeof item.value === 'string' ? parseInt(item.value) || null : item.value
  716. this.formData.itemCode = item.itemCode
  717. this.formData.itemName = item.itemName
  718. this.formData.specs = item.specs
  719. // 触发物料变更事件
  720. this.$emit(FORECAST_FORM_EVENTS.ITEM_CHANGE, item)
  721. }
  722. },
  723. /**
  724. * 初始化表单配置
  725. * @description 根据编辑模式初始化表单配置选项
  726. * @returns {void}
  727. * @this {ForecastFormMixinComponent & Vue}
  728. */
  729. initFormOption() {
  730. this.formOption = getFormOption(this.isEdit)
  731. },
  732. /**
  733. * 初始化表单数据
  734. * @description 根据编辑模式初始化表单数据,新增模式自动填入下个月
  735. * @returns {void}
  736. * @this {ForecastFormMixinComponent & Vue}
  737. */
  738. initFormData() {
  739. if (this.isEdit && this.editData) {
  740. // 编辑模式:使用传入的数据,确保year字段为字符串格式
  741. this.formData = {
  742. ...this.editData,
  743. year: this.editData.year ? this.editData.year.toString() : ''
  744. }
  745. // 若编辑入参未包含预测编码,则根据id加载详情以保证回显
  746. try {
  747. const id = (this.editData && (this.editData.id || this.editData.Id)) || (this.formData && (this.formData.id || this.formData.Id))
  748. if (!this.formData.forecastCode && id) {
  749. this.loadForecastDetail(id)
  750. }
  751. } catch (e) {
  752. // 非关键性异常,忽略
  753. }
  754. } else {
  755. // 新增模式:使用默认数据,自动填入下个月
  756. const now = new Date()
  757. const currentYear = now.getFullYear()
  758. const currentMonth = now.getMonth() + 1
  759. let nextYear, nextMonth
  760. if (currentMonth === 12) {
  761. // 当前是12月,下个月是明年1月
  762. nextYear = currentYear + 1
  763. nextMonth = 1
  764. } else {
  765. // 其他月份,直接 +1
  766. nextYear = currentYear
  767. nextMonth = currentMonth + 1
  768. }
  769. this.formData = {
  770. id: null,
  771. forecastCode: '',
  772. year: nextYear.toString(),
  773. month: nextMonth,
  774. customerId: null,
  775. customerCode: '',
  776. customerName: '',
  777. brandId: null,
  778. brandCode: '',
  779. brandName: '',
  780. itemId: null,
  781. itemCode: '',
  782. itemName: '',
  783. specs: '',
  784. itemSpecs: '',
  785. forecastQuantity: null,
  786. currentInventory: null,
  787. approvalStatus: APPROVAL_STATUS.PENDING,
  788. approvedName: '',
  789. approvedTime: null,
  790. approvalRemark: '',
  791. createTime: null,
  792. updateTime: null
  793. }
  794. // 生成预测编码
  795. // this.generateForecastCode()
  796. }
  797. },
  798. /**
  799. * 收集当前可见表单项的必填与数值规则错误信息(用于控制台打印)
  800. * @returns {string[]} 错误消息列表
  801. */
  802. collectValidationErrors() {
  803. try {
  804. const errors = []
  805. const option = this.formOption || {}
  806. const groups = Array.isArray(option.group) ? option.group : []
  807. const isEmpty = (v) => v === undefined || v === null || v === ''
  808. groups.forEach(group => {
  809. const columns = Array.isArray(group.column) ? group.column : []
  810. columns.forEach(field => {
  811. if (!field || !field.prop) return
  812. // 仅校验可见字段
  813. if (field.display === false) return
  814. const rules = Array.isArray(field.rules) ? field.rules : []
  815. const val = this.formData ? this.formData[field.prop] : undefined
  816. // 必填校验
  817. const requiredRule = rules.find(r => r && r.required)
  818. if (requiredRule && isEmpty(val)) {
  819. const label = field.label || field.prop
  820. const msg = requiredRule.message || `${label}为必填项`
  821. errors.push(`${label}: ${msg}`)
  822. }
  823. // 数值最小值校验
  824. const numberRule = rules.find(r => r && r.type === 'number' && (r.min !== undefined))
  825. if (numberRule && !isEmpty(val)) {
  826. const num = Number(val)
  827. if (!Number.isFinite(num) || num < numberRule.min) {
  828. const label = field.label || field.prop
  829. const msg = numberRule.message || `${label}必须不小于${numberRule.min}`
  830. errors.push(`${label}: ${msg}`)
  831. }
  832. }
  833. })
  834. })
  835. return errors
  836. } catch (e) {
  837. return []
  838. }
  839. },
  840. /**
  841. * 表单提交事件处理(Avue表单 @submit 入口)
  842. * @description 响应 avue-form 的提交事件,统一走 submitForm 逻辑
  843. * @returns {void}
  844. * @this {ForecastFormMixinComponent & Vue}
  845. */
  846. handleSubmit(form, done, loading) {
  847. try {
  848. // 先结束 Avue 内置的按钮loading,避免未调用 done 导致一直loading
  849. if (typeof done === 'function') done()
  850. console.log(this.formData)
  851. // 采用旧实现风格:通过 this.$refs.forecastForm.validate 回调进行校验
  852. if (this.$refs && this.$refs.forecastForm && typeof this.$refs.forecastForm.validate === 'function') {
  853. this.$refs.forecastForm.validate((valid) => {
  854. if (!valid) {
  855. // 编辑态下,收集并打印具体未通过原因
  856. if (this.isEdit && typeof console !== 'undefined') {
  857. const errors = this.collectValidationErrors ? this.collectValidationErrors() : []
  858. if (errors && errors.length) {
  859. console.group && console.group('表单校验未通过')
  860. errors.forEach(msg => console.error(msg))
  861. console.groupEnd && console.groupEnd()
  862. }
  863. }
  864. // 校验失败时,如存在 loading 回调(部分版本提供),尝试恢复按钮状态
  865. if (typeof loading === 'function') loading()
  866. // 通知父组件校验失败,便于父侧重置保存按钮loading
  867. this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_ERROR, { message: '表单校验未通过' })
  868. return
  869. }
  870. // 校验通过后执行提交
  871. this.submitForm()
  872. .catch((e) => {
  873. console.error('提交异常:', e)
  874. // 将错误交由父组件统一处理,避免重复toast
  875. this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_ERROR, e)
  876. })
  877. })
  878. } else {
  879. // 无法获取到 validate 时,直接尝试提交
  880. this.submitForm()
  881. .catch((e) => {
  882. console.error('提交异常:', e)
  883. // 将错误交由父组件统一处理,避免重复toast
  884. this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_ERROR, e)
  885. })
  886. }
  887. } catch (e) {
  888. console.error('提交异常:', e)
  889. // 将错误交由父组件统一处理,避免重复toast
  890. this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_ERROR, e)
  891. }
  892. },
  893. /**
  894. * 提交表单数据(main-add:提交销售预测主表及子项明细)
  895. * @returns {Promise<void>}
  896. * @this {ForecastFormMixinComponent & Vue}
  897. */
  898. async submitForm() {
  899. try {
  900. // 基础校验(客户必选)
  901. if (!this.formData.customerId) {
  902. this.$message && this.$message.warning('请选择客户')
  903. // 通知父组件失败,重置保存按钮loading
  904. this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_ERROR, { message: '未选择客户' })
  905. return
  906. }
  907. // 转换年份与月份
  908. const year = typeof this.formData.year === 'string' ? parseInt(this.formData.year, 10) : this.formData.year
  909. const month = Number(this.formData.month)
  910. // 安全的ID转换:优先使用 BigInt 校验范围,再决定以 number 还是 string 传输
  911. /** @param {unknown} val @returns {string|number|''} */
  912. const toIdOutput = (val) => {
  913. if (val == null || val === '') return ''
  914. try {
  915. const bi = BigInt(String(val))
  916. const absBi = bi >= 0n ? bi : -bi
  917. const maxSafe = BigInt(Number.MAX_SAFE_INTEGER)
  918. if (absBi <= maxSafe) {
  919. return Number(bi)
  920. }
  921. return String(bi)
  922. } catch (e) {
  923. return String(val)
  924. }
  925. }
  926. // 安全的数值转换(用于数量等非ID字段):若不可安全表示整数,仍以字符串传输
  927. /** @param {unknown} val @returns {number|string} */
  928. const toSafeNumberOrString = (val) => {
  929. if (val == null || val === '') return 0
  930. if (typeof val === 'number') {
  931. return Number.isFinite(val) ? val : String(val)
  932. }
  933. const parsed = Number(val)
  934. return Number.isFinite(parsed) ? parsed : String(val)
  935. }
  936. // 组装子项明细,仅保留预测数量>0的行
  937. const items = this.stockTableData
  938. .filter(row => Number(row.forecastQuantity) > 0)
  939. .map(row => {
  940. const matchedBrand = this.brandDescList.find(b => b.cname === row.brandName)
  941. const rawBrandId = row.brandId != null && row.brandId !== '' ? row.brandId : (matchedBrand ? matchedBrand.id : '')
  942. const rawItemId = row.goodsId
  943. const brandId = toIdOutput(rawBrandId)
  944. const itemId = toIdOutput(rawItemId)
  945. const base = {
  946. brandId,
  947. brandCode: row.brandCode || '',
  948. brandName: row.brandName || (matchedBrand ? matchedBrand.cname : ''),
  949. itemId,
  950. itemCode: row.code || '',
  951. itemName: row.cname || '',
  952. specs: row.typeNo || '',
  953. pattern: row.productDescription || row.brandItem || '',
  954. forecastQuantity: toSafeNumberOrString(row.forecastQuantity),
  955. approvalStatus: Number(this.formData.approvalStatus ?? 0)
  956. }
  957. // 编辑模式下,如果明细有 id,带上给后端做区分
  958. if (this.isEdit && (row.id != null && row.id !== '')) {
  959. return { id: toIdOutput(row.id), ...base }
  960. }
  961. return base
  962. })
  963. // 新增模式下需要至少一条有效明细;编辑模式下仅提交主表四个字段,不校验明细条数
  964. if (!this.isEdit && !items.length) {
  965. this.$message && this.$message.warning('请至少填写一条有效的预测数量')
  966. // 通知父组件失败,便于父侧重置保存按钮loading
  967. this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_ERROR, { message: '未填写有效的预测明细' })
  968. return
  969. }
  970. // 组装载荷
  971. const payloadBase = {
  972. year: year || new Date().getFullYear(),
  973. month: month || (new Date().getMonth() + 1),
  974. approvalStatus: Number(this.formData.approvalStatus ?? 0),
  975. pcBladeSalesForecastSummaryList: items
  976. }
  977. let res
  978. if (this.isEdit && this.formData.id) {
  979. // 更新:需要主表 id
  980. res = await updateSalesForecastMain({ id: toIdOutput(this.formData.id), ...payloadBase })
  981. } else {
  982. // 新增
  983. res = await addSalesForecastMain(payloadBase)
  984. }
  985. if (res && res.data && res.data.success) {
  986. this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT, res.data)
  987. this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_SUCCESS, res.data)
  988. } else {
  989. const msg = (res && res.data && (res.data.msg || res.data.message)) || '提交失败'
  990. if (typeof this.setYearMonthDisabled === 'function') {
  991. this.setYearMonthDisabled(false)
  992. } else if (this.$refs) {
  993. this.$nextTick(() => {
  994. try {
  995. if (this.$refs.yearPicker) this.$refs.yearPicker.disabled = false
  996. if (this.$refs.monthSelect) this.$refs.monthSelect.disabled = false
  997. } catch (e) { /* 忽略 */ }
  998. })
  999. }
  1000. this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_ERROR, { message: msg, response: res })
  1001. }
  1002. } catch (error) {
  1003. console.error('提交表单失败:', error)
  1004. this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_ERROR, error)
  1005. }
  1006. },
  1007. /**
  1008. * 客户选择事件处理
  1009. * @description 处理CustomerSelect组件的客户选择事件
  1010. * @param {CustomerSelectData} customerData - 客户选择数据
  1011. * @returns {void}
  1012. * @this {ForecastFormMixinComponent & Vue}
  1013. */
  1014. handleCustomerSelected(/** @type {import('./types').CustomerSelectData} */ customerData) {
  1015. if (customerData && customerData.customerId) {
  1016. this.formData.customerId = Number(customerData.customerId)
  1017. this.formData.customerCode = customerData.customerCode
  1018. this.formData.customerName = customerData.customerName
  1019. // 选中客户后加载该用户关联的品牌与库存物料(仅新增模式自动加载,编辑模式不覆盖回显数据)
  1020. if (!this.isEdit) {
  1021. this.loadUserLinkGoods()
  1022. }
  1023. } else {
  1024. this.formData.customerId = null
  1025. this.formData.customerCode = ''
  1026. this.formData.customerName = ''
  1027. // 清空表格
  1028. this.stockTableData = []
  1029. }
  1030. },
  1031. /**
  1032. * 加载用户关联商品(品牌与库存)
  1033. * @returns {Promise<void>}
  1034. * @this {ForecastFormMixinComponent & Vue}
  1035. */
  1036. async loadUserLinkGoods() {
  1037. try {
  1038. this.tableLoading = true
  1039. // 初始化容器
  1040. this.stockTableData = []
  1041. this.brandDescList = []
  1042. this.stockDescList = []
  1043. this.stockSelectOptions = []
  1044. this.selectedStockId = null
  1045. const res = await getUserLinkGoods()
  1046. const payload = res && res.data && res.data.data ? res.data.data : null
  1047. const brandList = (payload && payload.pjpfBrandDescList) || []
  1048. const stockList = (payload && payload.pjpfStockDescList) || []
  1049. this.brandDescList = brandList
  1050. // 存储库存列表供选择用,不直接展示到表格
  1051. this.stockDescList = stockList
  1052. // 构造下拉选项,label 使用 cname,value 使用 id
  1053. this.stockSelectOptions = stockList.map(item => ({
  1054. label: item.cname,
  1055. value: item.id
  1056. }))
  1057. // 默认显示全部物料至下方表格,预测数量默认 0,用户可手动删除不需要的物料
  1058. this.stockTableData = stockList.map(item => ({ ...item, forecastQuantity: 1 }))
  1059. } catch (e) {
  1060. console.error('加载用户关联商品失败:', e)
  1061. this.$message.error(e.message || '加载用户关联商品失败')
  1062. } finally {
  1063. this.tableLoading = false
  1064. }
  1065. },
  1066. /**
  1067. * 导入所选物料到下方表格
  1068. * @description 仅在点击"导入物料"按钮后,将选择的物料行添加到表格,默认预测数量为 1
  1069. * @returns {void}
  1070. * @this {ForecastFormMixinComponent & Vue}
  1071. */
  1072. handleImportSelectedStock() {
  1073. // 未选择则提示
  1074. if (!this.selectedStockId) {
  1075. this.$message.warning('请先在上方选择要导入的物料')
  1076. return
  1077. }
  1078. // 查找明细
  1079. const stock = this.stockDescList.find(s => s.id === this.selectedStockId)
  1080. if (!stock) {
  1081. this.$message.error('未找到所选物料数据,请重新选择')
  1082. return
  1083. }
  1084. // 防止重复导入 - 使用多个字段进行更全面的重复检查
  1085. const exists = this.stockTableData.some(row => {
  1086. // 优先使用 id 进行匹配
  1087. if (row.id && stock.id && row.id === stock.id) {
  1088. return true
  1089. }
  1090. // 使用 goodsId 进行匹配
  1091. if (row.goodsId && stock.goodsId && row.goodsId === stock.goodsId) {
  1092. return true
  1093. }
  1094. // 使用 code 进行匹配
  1095. if (row.code && stock.code && row.code === stock.code) {
  1096. return true
  1097. }
  1098. return false
  1099. })
  1100. if (exists) {
  1101. this.$message.warning('该物料已在列表中')
  1102. this.selectedStockId = null
  1103. return
  1104. }
  1105. // 添加到表格,默认预测数量为 1
  1106. this.stockTableData.push({ ...stock, forecastQuantity: 1 })
  1107. // 清空已选
  1108. this.selectedStockId = null
  1109. },
  1110. /**
  1111. * 删除物料行
  1112. * @description 在下方物料表格中删除指定行,包含二次确认流程;删除后保持数据与UI同步。
  1113. * @param {import('./types').ForecastFormMixinData['stockTableData'][number]} row - 待删除的表格行数据
  1114. * @param {number} index - 行索引
  1115. * @returns {Promise<void>}
  1116. * @this {import('./types').ForecastFormMixinComponent & Vue}
  1117. */
  1118. async handleDelete(row, index) {
  1119. try {
  1120. // 索引校验,必要时根据唯一标识兜底定位
  1121. let removeIndex = typeof index === 'number' ? index : -1
  1122. if (removeIndex < 0 || removeIndex >= this.stockTableData.length) {
  1123. const keyId = row && (row.id != null ? row.id : row.goodsId)
  1124. removeIndex = this.stockTableData.findIndex(r => (r.id != null ? r.id : r.goodsId) === keyId)
  1125. }
  1126. if (removeIndex < 0) {
  1127. this.$message && this.$message.warning('未定位到要删除的记录')
  1128. return
  1129. }
  1130. // 二次确认
  1131. await this.$confirm('确认删除该物料吗?删除后可重新通过上方选择器导入。', '提示', {
  1132. type: 'warning',
  1133. confirmButtonText: '删除',
  1134. cancelButtonText: '取消'
  1135. })
  1136. // 使用 Vue.set/delete 保持响应式
  1137. this.$delete(this.stockTableData, removeIndex)
  1138. // 如有需要,清理与该行相关的临时状态(当前实现无行级临时状态)
  1139. // 例如:this.currentInventory = null
  1140. this.$message && this.$message.success('已删除')
  1141. } catch (e) {
  1142. // 用户取消不提示为错误,其他情况做日志记录
  1143. if (e && e !== 'cancel') {
  1144. console.error('删除失败:', e)
  1145. this.$message && this.$message.error('删除失败,请稍后重试')
  1146. }
  1147. }
  1148. },
  1149. /**
  1150. * 品牌变更处理
  1151. * @param {number} brandId - 品牌ID
  1152. * @returns {void}
  1153. * @this {ForecastFormMixinComponent & Vue}
  1154. */
  1155. handleBrandChange(/** @type {number} */ brandId) {
  1156. const selectedBrand = this.brandOptions.find(brand => /** @type {any} */ (brand).id === brandId)
  1157. if (selectedBrand) {
  1158. this.formData.brandId = brandId
  1159. this.formData.brandCode = /** @type {any} */ (selectedBrand).code
  1160. this.formData.brandName = /** @type {any} */ (selectedBrand).name
  1161. } else {
  1162. this.formData.brandId = null
  1163. this.formData.brandCode = ''
  1164. this.formData.brandName = ''
  1165. }
  1166. },
  1167. /**
  1168. * 物料选择处理
  1169. * @description 处理MaterialSelect组件的物料选择事件
  1170. * @param {MaterialSelectData} materialData - 物料选择数据
  1171. * @returns {void}
  1172. * @this {ForecastFormMixinComponent & Vue}
  1173. */
  1174. handleMaterialSelected(/** @type {import('./types').MaterialSelectData} */ materialData) {
  1175. if (materialData && materialData.itemId) {
  1176. this.formData.itemId = Number(materialData.itemId)
  1177. this.formData.itemCode = materialData.itemCode
  1178. this.formData.itemName = materialData.itemName
  1179. this.formData.itemSpecs = materialData.specification || ''
  1180. // 获取当前库存
  1181. this.getCurrentInventory(materialData.itemId)
  1182. } else {
  1183. this.formData.itemId = null
  1184. this.formData.itemCode = ''
  1185. this.formData.itemName = ''
  1186. this.formData.itemSpecs = ''
  1187. this.currentInventory = null
  1188. }
  1189. },
  1190. /**
  1191. * 合并回显行的库存数量
  1192. * @description 使用 getUserLinkGoods 接口返回的库存数据,为编辑态回显的物料行补齐 storeInventory 字段
  1193. * @returns {Promise<void>}
  1194. */
  1195. async mergeEchoStoreInventory() {
  1196. try {
  1197. if (!Array.isArray(this.stockTableData) || this.stockTableData.length === 0) return
  1198. const res = await getUserLinkGoods()
  1199. const payload = res && res.data && res.data.data ? res.data.data : null
  1200. const stockList = (payload && payload.pjpfStockDescList) || []
  1201. if (!Array.isArray(stockList) || stockList.length === 0) return
  1202. // 在编辑模式下,确保"导入物料"的选择器有数据可选
  1203. // 不修改现有表格数据,仅补齐选择来源
  1204. this.stockDescList = stockList
  1205. this.stockSelectOptions = stockList.map(item => ({
  1206. label: item.cname,
  1207. value: item.id
  1208. }))
  1209. // 构建基于 goodsId 与 code 的索引映射
  1210. /** @type {Map<string, string|undefined>} */
  1211. const invByGoodsId = new Map()
  1212. /** @type {Map<string, string|undefined>} */
  1213. const invByCode = new Map()
  1214. stockList.forEach(s => {
  1215. const inv = (s && s.storeInventory !== undefined && s.storeInventory !== null && s.storeInventory !== '') ? String(s.storeInventory) : undefined
  1216. if (s && s.goodsId !== undefined && s.goodsId !== null) invByGoodsId.set(String(s.goodsId), inv)
  1217. if (s && s.code) invByCode.set(String(s.code), inv)
  1218. })
  1219. // 合并库存到现有表格数据(仅填充缺失的库存字段)
  1220. this.stockTableData = this.stockTableData.map(row => {
  1221. const hasInv = !(row.storeInventory === undefined || row.storeInventory === null || row.storeInventory === '')
  1222. if (hasInv) return row
  1223. const keyGoodsId = row && row.goodsId !== undefined && row.goodsId !== null ? String(row.goodsId) : ''
  1224. const keyCode = row && row.code ? String(row.code) : ''
  1225. const fromGoods = keyGoodsId ? invByGoodsId.get(keyGoodsId) : undefined
  1226. const fromCode = (!fromGoods && keyCode) ? invByCode.get(keyCode) : undefined
  1227. const value = (fromGoods !== undefined && fromGoods !== null && fromGoods !== '') ? fromGoods : ((fromCode !== undefined && fromCode !== null && fromCode !== '') ? fromCode : '0')
  1228. return { ...row, storeInventory: String(value) }
  1229. })
  1230. } catch (e) {
  1231. console.warn('回显库存合并失败:', e)
  1232. }
  1233. }
  1234. }
  1235. }