forecast-form-mixin.js 46 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349
  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. * @this {ForecastFormMixinComponent & Vue}
  801. * @returns {string[]} 错误消息列表
  802. */
  803. collectValidationErrors() {
  804. try {
  805. const errors = []
  806. const option = this.formOption || {}
  807. const groups = Array.isArray(option.group) ? option.group : []
  808. const isEmpty = (v) => v === undefined || v === null || v === ''
  809. groups.forEach(group => {
  810. const columns = Array.isArray(group.column) ? group.column : []
  811. columns.forEach(field => {
  812. if (!field || !field.prop) return
  813. // 仅校验可见字段
  814. if (field.display === false) return
  815. const rules = Array.isArray(field.rules) ? field.rules : []
  816. const val = this.formData ? this.formData[field.prop] : undefined
  817. // 必填校验
  818. const requiredRule = rules.find(r => r && r.required)
  819. if (requiredRule && isEmpty(val)) {
  820. const label = field.label || field.prop
  821. const msg = requiredRule.message || `${label}为必填项`
  822. errors.push(`${label}: ${msg}`)
  823. }
  824. // 数值最小值校验
  825. const numberRule = rules.find(r => r && r.type === 'number' && (r.min !== undefined))
  826. if (numberRule && !isEmpty(val)) {
  827. const num = Number(val)
  828. if (!Number.isFinite(num) || num < numberRule.min) {
  829. const label = field.label || field.prop
  830. const msg = numberRule.message || `${label}必须不小于${numberRule.min}`
  831. errors.push(`${label}: ${msg}`)
  832. }
  833. }
  834. })
  835. })
  836. return errors
  837. } catch (e) {
  838. return []
  839. }
  840. },
  841. /**
  842. * 表单提交事件处理(Avue表单 @submit 入口)
  843. * @description 响应 avue-form 的提交事件,统一走 submitForm 逻辑
  844. * @returns {void}
  845. * @this {ForecastFormMixinComponent & Vue}
  846. */
  847. handleSubmit(form, done, loading) {
  848. try {
  849. // 先结束 Avue 内置的按钮loading,避免未调用 done 导致一直loading
  850. if (typeof done === 'function') done()
  851. console.log(this.formData)
  852. // 采用旧实现风格:通过 this.$refs.forecastForm.validate 回调进行校验
  853. if (this.$refs && this.$refs.forecastForm && typeof this.$refs.forecastForm.validate === 'function') {
  854. this.$refs.forecastForm.validate((valid) => {
  855. if (!valid) {
  856. // 编辑态下,收集并打印具体未通过原因
  857. if (this.isEdit && typeof console !== 'undefined') {
  858. const errors = this.collectValidationErrors ? this.collectValidationErrors() : []
  859. if (errors && errors.length) {
  860. console.group && console.group('表单校验未通过')
  861. errors.forEach(msg => console.error(msg))
  862. console.groupEnd && console.groupEnd()
  863. }
  864. }
  865. // 校验失败时,如存在 loading 回调(部分版本提供),尝试恢复按钮状态
  866. if (typeof loading === 'function') loading()
  867. // 通知父组件校验失败,便于父侧重置保存按钮loading
  868. this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_ERROR, { message: '表单校验未通过' })
  869. return
  870. }
  871. // 校验通过后执行提交
  872. this.submitForm()
  873. .catch((e) => {
  874. console.error('提交异常:', e)
  875. // 将错误交由父组件统一处理,避免重复toast
  876. this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_ERROR, e)
  877. })
  878. })
  879. } else {
  880. // 无法获取到 validate 时,直接尝试提交
  881. this.submitForm()
  882. .catch((e) => {
  883. console.error('提交异常:', e)
  884. // 将错误交由父组件统一处理,避免重复toast
  885. this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_ERROR, e)
  886. })
  887. }
  888. } catch (e) {
  889. console.error('提交异常:', e)
  890. // 将错误交由父组件统一处理,避免重复toast
  891. this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_ERROR, e)
  892. }
  893. },
  894. /**
  895. * 提交表单数据(main-add:提交销售预测主表及子项明细)
  896. * @returns {Promise<void>}
  897. * @this {ForecastFormMixinComponent & Vue}
  898. */
  899. async submitForm() {
  900. try {
  901. // 基础校验(客户必选)
  902. if (!this.formData.customerId) {
  903. this.$message && this.$message.warning('请选择客户')
  904. // 通知父组件失败,重置保存按钮loading
  905. this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_ERROR, { message: '未选择客户' })
  906. return
  907. }
  908. // 转换年份与月份
  909. const year = typeof this.formData.year === 'string' ? parseInt(this.formData.year, 10) : this.formData.year
  910. const month = Number(this.formData.month)
  911. // 安全的ID转换:优先使用 BigInt 校验范围,再决定以 number 还是 string 传输
  912. /** @param {unknown} val @returns {string|number|''} */
  913. const toIdOutput = (val) => {
  914. if (val == null || val === '') return ''
  915. try {
  916. const bi = BigInt(String(val))
  917. const absBi = bi >= 0n ? bi : -bi
  918. const maxSafe = BigInt(Number.MAX_SAFE_INTEGER)
  919. if (absBi <= maxSafe) {
  920. return Number(bi)
  921. }
  922. return String(bi)
  923. } catch (e) {
  924. return String(val)
  925. }
  926. }
  927. // 安全的数值转换(用于数量等非ID字段):若不可安全表示整数,仍以字符串传输
  928. /** @param {unknown} val @returns {number|string} */
  929. const toSafeNumberOrString = (val) => {
  930. if (val == null || val === '') return 0
  931. if (typeof val === 'number') {
  932. return Number.isFinite(val) ? val : String(val)
  933. }
  934. const parsed = Number(val)
  935. return Number.isFinite(parsed) ? parsed : String(val)
  936. }
  937. // 组装子项明细,仅保留预测数量>0的行
  938. const items = this.stockTableData
  939. .filter(row => Number(row.forecastQuantity) > 0)
  940. .map(row => {
  941. const matchedBrand = this.brandDescList.find(b => b.cname === row.brandName)
  942. const rawBrandId = row.brandId != null && row.brandId !== '' ? row.brandId : (matchedBrand ? matchedBrand.id : '')
  943. const rawItemId = row.goodsId
  944. const brandId = toIdOutput(rawBrandId)
  945. const itemId = toIdOutput(rawItemId)
  946. const base = {
  947. brandId,
  948. brandCode: row.brandCode || '',
  949. brandName: row.brandName || (matchedBrand ? matchedBrand.cname : ''),
  950. itemId,
  951. itemCode: row.code || '',
  952. itemName: row.cname || '',
  953. specs: row.typeNo || '',
  954. pattern: row.productDescription || row.brandItem || '',
  955. forecastQuantity: toSafeNumberOrString(row.forecastQuantity),
  956. approvalStatus: Number(this.formData.approvalStatus ?? 0)
  957. }
  958. // 编辑模式下,如果明细有 id,带上给后端做区分
  959. if (this.isEdit && (row.id != null && row.id !== '')) {
  960. return { id: toIdOutput(row.id), ...base }
  961. }
  962. return base
  963. })
  964. // 新增模式下需要至少一条有效明细;编辑模式下仅提交主表四个字段,不校验明细条数
  965. if (!this.isEdit && !items.length) {
  966. this.$message && this.$message.warning('请至少填写一条有效的预测数量')
  967. // 通知父组件失败,便于父侧重置保存按钮loading
  968. this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_ERROR, { message: '未填写有效的预测明细' })
  969. return
  970. }
  971. // 组装载荷
  972. const payloadBase = {
  973. year: year || new Date().getFullYear(),
  974. month: month || (new Date().getMonth() + 1),
  975. approvalStatus: Number(this.formData.approvalStatus ?? 0),
  976. pcBladeSalesForecastSummaryList: items
  977. }
  978. let res
  979. if (this.isEdit && this.formData.id) {
  980. // 更新:需要主表 id
  981. res = await updateSalesForecastMain({ id: toIdOutput(this.formData.id), ...payloadBase })
  982. } else {
  983. // 新增
  984. res = await addSalesForecastMain(payloadBase)
  985. }
  986. if (res && res.data && res.data.success) {
  987. this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT, res.data)
  988. this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_SUCCESS, res.data)
  989. } else {
  990. const msg = (res && res.data && (res.data.msg || res.data.message)) || '提交失败'
  991. if (typeof this.setYearMonthDisabled === 'function') {
  992. this.setYearMonthDisabled(false)
  993. } else if (this.$refs) {
  994. this.$nextTick(() => {
  995. try {
  996. if (this.$refs.yearPicker) this.$refs.yearPicker.disabled = false
  997. if (this.$refs.monthSelect) this.$refs.monthSelect.disabled = false
  998. } catch (e) { /* 忽略 */ }
  999. })
  1000. }
  1001. this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_ERROR, { message: msg, response: res })
  1002. }
  1003. } catch (error) {
  1004. console.error('提交表单失败:', error)
  1005. this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_ERROR, error)
  1006. }
  1007. },
  1008. /**
  1009. * 客户选择事件处理
  1010. * @description 处理CustomerSelect组件的客户选择事件
  1011. * @param {CustomerSelectData} customerData - 客户选择数据
  1012. * @returns {void}
  1013. * @this {ForecastFormMixinComponent & Vue}
  1014. */
  1015. handleCustomerSelected(/** @type {import('./types').CustomerSelectData} */ customerData) {
  1016. if (customerData && customerData.customerId) {
  1017. this.formData.customerId = Number(customerData.customerId)
  1018. this.formData.customerCode = customerData.customerCode
  1019. this.formData.customerName = customerData.customerName
  1020. // 选中客户后加载该用户关联的品牌与库存物料(仅新增模式自动加载,编辑模式不覆盖回显数据)
  1021. if (!this.isEdit) {
  1022. this.loadUserLinkGoods()
  1023. }
  1024. } else {
  1025. this.formData.customerId = null
  1026. this.formData.customerCode = ''
  1027. this.formData.customerName = ''
  1028. // 清空表格
  1029. this.stockTableData = []
  1030. }
  1031. },
  1032. /**
  1033. * 加载用户关联商品(品牌与库存)
  1034. * @returns {Promise<void>}
  1035. * @this {ForecastFormMixinComponent & Vue}
  1036. */
  1037. async loadUserLinkGoods() {
  1038. try {
  1039. this.tableLoading = true
  1040. // 初始化容器
  1041. this.stockTableData = []
  1042. this.brandDescList = []
  1043. this.stockDescList = []
  1044. this.stockSelectOptions = []
  1045. this.selectedStockId = null
  1046. const res = await getUserLinkGoods()
  1047. const payload = res && res.data && res.data.data ? res.data.data : null
  1048. const brandList = (payload && payload.pjpfBrandDescList) || []
  1049. const stockList = (payload && payload.pjpfStockDescList) || []
  1050. this.brandDescList = brandList
  1051. // 存储库存列表供选择用,不直接展示到表格
  1052. this.stockDescList = stockList
  1053. // 构造下拉选项,label 使用 cname,value 使用 id
  1054. this.stockSelectOptions = stockList.map(item => ({
  1055. label: item.cname,
  1056. value: item.id
  1057. }))
  1058. // 默认显示全部物料至下方表格,预测数量默认 0,用户可手动删除不需要的物料
  1059. this.stockTableData = stockList.map(item => ({ ...item, forecastQuantity: 1 }))
  1060. } catch (e) {
  1061. console.error('加载用户关联商品失败:', e)
  1062. this.$message.error(e.message || '加载用户关联商品失败')
  1063. } finally {
  1064. this.tableLoading = false
  1065. }
  1066. },
  1067. /**
  1068. * 导入所选物料到下方表格
  1069. * @description 仅在点击"导入物料"按钮后,将选择的物料行添加到表格,默认预测数量为 1
  1070. * @returns {void}
  1071. * @this {ForecastFormMixinComponent & Vue}
  1072. */
  1073. handleImportSelectedStock() {
  1074. // 未选择则提示
  1075. if (!this.selectedStockId) {
  1076. this.$message.warning('请先在上方选择要导入的物料')
  1077. return
  1078. }
  1079. // 查找明细
  1080. const stock = this.stockDescList.find(s => s.id === this.selectedStockId)
  1081. if (!stock) {
  1082. this.$message.error('未找到所选物料数据,请重新选择')
  1083. return
  1084. }
  1085. // 防止重复导入 - 使用多个字段进行更全面的重复检查
  1086. const exists = this.stockTableData.some(row => {
  1087. // 优先使用 id 进行匹配
  1088. if (row.id && stock.id && row.id === stock.id) {
  1089. return true
  1090. }
  1091. // 使用 goodsId 进行匹配
  1092. if (row.goodsId && stock.goodsId && row.goodsId === stock.goodsId) {
  1093. return true
  1094. }
  1095. // 使用 code 进行匹配
  1096. if (row.code && stock.code && row.code === stock.code) {
  1097. return true
  1098. }
  1099. return false
  1100. })
  1101. if (exists) {
  1102. this.$message.warning('该物料已在列表中')
  1103. this.selectedStockId = null
  1104. return
  1105. }
  1106. // 添加到表格,默认预测数量为 1
  1107. this.stockTableData.push({ ...stock, forecastQuantity: 1 })
  1108. // 清空已选
  1109. this.selectedStockId = null
  1110. },
  1111. /**
  1112. * 删除物料行
  1113. * @description 在下方物料表格中删除指定行,包含二次确认流程;删除后保持数据与UI同步。
  1114. * @param {import('./types').ForecastFormMixinData['stockTableData'][number]} row - 待删除的表格行数据
  1115. * @param {number} index - 行索引
  1116. * @returns {Promise<void>}
  1117. * @this {import('./types').ForecastFormMixinComponent & Vue}
  1118. */
  1119. async handleDelete(row, index) {
  1120. try {
  1121. // 索引校验,必要时根据唯一标识兜底定位
  1122. let removeIndex = typeof index === 'number' ? index : -1
  1123. if (removeIndex < 0 || removeIndex >= this.stockTableData.length) {
  1124. const keyId = row && (row.id != null ? row.id : row.goodsId)
  1125. removeIndex = this.stockTableData.findIndex(r => (r.id != null ? r.id : r.goodsId) === keyId)
  1126. }
  1127. if (removeIndex < 0) {
  1128. this.$message && this.$message.warning('未定位到要删除的记录')
  1129. return
  1130. }
  1131. // 二次确认
  1132. await this.$confirm('确认删除该物料吗?删除后可重新通过上方选择器导入。', '提示', {
  1133. type: 'warning',
  1134. confirmButtonText: '删除',
  1135. cancelButtonText: '取消'
  1136. })
  1137. // 使用 Vue.set/delete 保持响应式
  1138. this.$delete(this.stockTableData, removeIndex)
  1139. // 如有需要,清理与该行相关的临时状态(当前实现无行级临时状态)
  1140. // 例如:this.currentInventory = null
  1141. this.$message && this.$message.success('已删除')
  1142. } catch (e) {
  1143. // 用户取消不提示为错误,其他情况做日志记录
  1144. if (e && e !== 'cancel') {
  1145. console.error('删除失败:', e)
  1146. this.$message && this.$message.error('删除失败,请稍后重试')
  1147. }
  1148. }
  1149. },
  1150. /**
  1151. * 品牌变更处理
  1152. * @param {number} brandId - 品牌ID
  1153. * @returns {void}
  1154. * @this {ForecastFormMixinComponent & Vue}
  1155. */
  1156. handleBrandChange(/** @type {number} */ brandId) {
  1157. const selectedBrand = this.brandOptions.find(brand => /** @type {any} */ (brand).id === brandId)
  1158. if (selectedBrand) {
  1159. this.formData.brandId = brandId
  1160. this.formData.brandCode = /** @type {any} */ (selectedBrand).code
  1161. this.formData.brandName = /** @type {any} */ (selectedBrand).name
  1162. } else {
  1163. this.formData.brandId = null
  1164. this.formData.brandCode = ''
  1165. this.formData.brandName = ''
  1166. }
  1167. },
  1168. /**
  1169. * 物料选择处理
  1170. * @description 处理MaterialSelect组件的物料选择事件
  1171. * @param {MaterialSelectData} materialData - 物料选择数据
  1172. * @returns {void}
  1173. * @this {ForecastFormMixinComponent & Vue}
  1174. */
  1175. handleMaterialSelected(/** @type {import('./types').MaterialSelectData} */ materialData) {
  1176. if (materialData && materialData.itemId) {
  1177. this.formData.itemId = Number(materialData.itemId)
  1178. this.formData.itemCode = materialData.itemCode
  1179. this.formData.itemName = materialData.itemName
  1180. this.formData.itemSpecs = materialData.specification || ''
  1181. // 获取当前库存
  1182. this.getCurrentInventory(materialData.itemId)
  1183. } else {
  1184. this.formData.itemId = null
  1185. this.formData.itemCode = ''
  1186. this.formData.itemName = ''
  1187. this.formData.itemSpecs = ''
  1188. this.currentInventory = null
  1189. }
  1190. },
  1191. /**
  1192. * 合并回显行的库存数量
  1193. * @description 使用 getUserLinkGoods 接口返回的库存数据,为编辑态回显的物料行补齐 storeInventory 字段
  1194. * @returns {Promise<void>}
  1195. */
  1196. async mergeEchoStoreInventory() {
  1197. try {
  1198. if (!Array.isArray(this.stockTableData) || this.stockTableData.length === 0) return
  1199. const res = await getUserLinkGoods()
  1200. const payload = res && res.data && res.data.data ? res.data.data : null
  1201. const stockList = (payload && payload.pjpfStockDescList) || []
  1202. if (!Array.isArray(stockList) || stockList.length === 0) return
  1203. // 在编辑模式下,确保"导入物料"的选择器有数据可选
  1204. // 不修改现有表格数据,仅补齐选择来源
  1205. this.stockDescList = stockList
  1206. this.stockSelectOptions = stockList.map(item => ({
  1207. label: item.cname,
  1208. value: item.id
  1209. }))
  1210. // 构建基于 goodsId 与 code 的索引映射
  1211. /** @type {Map<string, string|undefined>} */
  1212. const invByGoodsId = new Map()
  1213. /** @type {Map<string, string|undefined>} */
  1214. const invByCode = new Map()
  1215. stockList.forEach(s => {
  1216. const inv = (s && s.storeInventory !== undefined && s.storeInventory !== null && s.storeInventory !== '') ? String(s.storeInventory) : undefined
  1217. if (s && s.goodsId !== undefined && s.goodsId !== null) invByGoodsId.set(String(s.goodsId), inv)
  1218. if (s && s.code) invByCode.set(String(s.code), inv)
  1219. })
  1220. // 合并库存到现有表格数据(仅填充缺失的库存字段)
  1221. this.stockTableData = this.stockTableData.map(row => {
  1222. const hasInv = !(row.storeInventory === undefined || row.storeInventory === null || row.storeInventory === '')
  1223. if (hasInv) return row
  1224. const keyGoodsId = row && row.goodsId !== undefined && row.goodsId !== null ? String(row.goodsId) : ''
  1225. const keyCode = row && row.code ? String(row.code) : ''
  1226. const fromGoods = keyGoodsId ? invByGoodsId.get(keyGoodsId) : undefined
  1227. const fromCode = (!fromGoods && keyCode) ? invByCode.get(keyCode) : undefined
  1228. const value = (fromGoods !== undefined && fromGoods !== null && fromGoods !== '') ? fromGoods : ((fromCode !== undefined && fromCode !== null && fromCode !== '') ? fromCode : '0')
  1229. return { ...row, storeInventory: String(value) }
  1230. })
  1231. } catch (e) {
  1232. console.warn('回显库存合并失败:', e)
  1233. }
  1234. }
  1235. }
  1236. }