index.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  1. /**
  2. * @fileoverview 销售预测表单混入组件
  3. * @description 提供销售预测表单的数据管理、验证规则和业务逻辑的混入组件,支持新增和编辑模式
  4. */
  5. // API接口导入
  6. import { addForecast, updateForecast, getForecastDetail } from '@/api/forecast'
  7. // 常量和枚举导入
  8. import {
  9. APPROVAL_STATUS,
  10. APPROVAL_STATUS_OPTIONS,
  11. FORECAST_FORM_RULES,
  12. DEFAULT_FORECAST_FORM,
  13. getApprovalStatusLabel,
  14. getApprovalStatusType,
  15. canEdit
  16. } from '@/constants/forecast'
  17. // 远程搜索API
  18. import { getCustomerList, getItemList } from '@/api/common'
  19. /**
  20. * 销售预测表单事件常量
  21. * @readonly
  22. * @type {Object}
  23. */
  24. export const FORECAST_FORM_EVENTS = {
  25. /** 表单提交成功事件 */
  26. SUBMIT_SUCCESS: 'submit-success',
  27. /** 表单取消事件 */
  28. CANCEL: 'cancel',
  29. /** 表单加载完成事件 */
  30. LOADED: 'loaded',
  31. /** 客户选择变更事件 */
  32. CUSTOMER_CHANGE: 'customer-change',
  33. /** 物料选择变更事件 */
  34. ITEM_CHANGE: 'item-change'
  35. }
  36. /**
  37. * 销售预测表单混入
  38. * @description 提供销售预测表单的数据管理、验证规则和业务逻辑
  39. */
  40. export default {
  41. /**
  42. * 组件名称
  43. */
  44. name: 'ForecastFormMixin',
  45. /**
  46. * 组件属性定义
  47. * @description 定义组件接收的外部属性及其类型约束
  48. */
  49. props: {
  50. /**
  51. * 表单可见性控制
  52. * @description 控制表单的显示和隐藏
  53. * @type {boolean}
  54. * @default false
  55. */
  56. visible: {
  57. type: Boolean,
  58. default: false
  59. },
  60. /**
  61. * 编辑模式标识
  62. * @description 标识当前表单是否处于编辑模式
  63. * @type {boolean}
  64. * @default false
  65. */
  66. isEdit: {
  67. type: Boolean,
  68. default: false
  69. },
  70. /**
  71. * 初始表单数据
  72. * @description 用于表单初始化的数据对象
  73. * @type {Object}
  74. * @default null
  75. */
  76. initialFormData: {
  77. type: Object,
  78. default: null
  79. },
  80. /**
  81. * 表单标题
  82. * @description 自定义表单标题,如果不提供则根据编辑模式自动生成
  83. * @type {string}
  84. * @default ''
  85. */
  86. title: {
  87. type: String,
  88. default: ''
  89. }
  90. },
  91. /**
  92. * 组件响应式数据
  93. * @description 定义组件的响应式数据状态
  94. */
  95. data() {
  96. return {
  97. /**
  98. * 销售预测表单数据模型
  99. * @description 存储销售预测表单的所有字段数据
  100. * @type {Object}
  101. */
  102. formData: this.createInitialFormData(),
  103. /**
  104. * 保存操作加载状态
  105. * @description 控制保存按钮的加载状态,防止重复提交
  106. * @type {boolean}
  107. */
  108. saveLoading: false,
  109. /**
  110. * 表单加载状态
  111. * @description 控制表单的加载状态,用于编辑模式下的数据加载
  112. * @type {boolean}
  113. */
  114. formLoading: false,
  115. /**
  116. * 客户选项列表
  117. * @description 客户下拉选择器的选项数据
  118. * @type {Array<{value: string|number, label: string, customerCode: string}>}
  119. */
  120. customerOptions: [],
  121. /**
  122. * 客户选项加载状态
  123. * @description 控制客户选项的加载状态
  124. * @type {boolean}
  125. */
  126. customerLoading: false,
  127. /**
  128. * 物料选项列表
  129. * @description 物料下拉选择器的选项数据
  130. * @type {Array<{value: string|number, label: string, itemCode: string, specs: string}>}
  131. */
  132. itemOptions: [],
  133. /**
  134. * 物料选项加载状态
  135. * @description 控制物料选项的加载状态
  136. * @type {boolean}
  137. */
  138. itemLoading: false,
  139. /**
  140. * 审批状态选项列表
  141. * @description 审批状态下拉选择器的选项数据
  142. * @type {typeof APPROVAL_STATUS_OPTIONS}
  143. */
  144. approvalStatusOptions: APPROVAL_STATUS_OPTIONS,
  145. /**
  146. * 表单验证规则
  147. * @description 表单字段的验证规则
  148. * @type {typeof FORECAST_FORM_RULES}
  149. */
  150. formRules: FORECAST_FORM_RULES
  151. }
  152. },
  153. /**
  154. * 计算属性
  155. * @description 组件的响应式计算属性
  156. */
  157. computed: {
  158. /**
  159. * 表单标题
  160. * @description 根据编辑模式动态显示表单标题
  161. * @returns {string} 表单标题文本
  162. */
  163. formTitle() {
  164. if (this.title) {
  165. return this.title
  166. }
  167. return this.isEdit ? '编辑销售预测' : '新增销售预测'
  168. }
  169. },
  170. /**
  171. * 侦听器
  172. * @description 监听属性变化并执行相应操作
  173. */
  174. watch: {
  175. /**
  176. * 监听表单可见性变化
  177. * @param {boolean} val - 新的可见性值
  178. */
  179. visible(val) {
  180. if (val) {
  181. this.$nextTick(() => {
  182. // 表单显示时,初始化表单数据
  183. if (this.initialFormData) {
  184. this.formData = this.cleanAndFormatFormData(this.initialFormData)
  185. } else {
  186. this.formData = this.createInitialFormData()
  187. }
  188. // 如果是编辑模式且有ID,则加载详情数据
  189. if (this.isEdit && this.formData.id) {
  190. this.loadForecastDetail(this.formData.id)
  191. }
  192. // 如果不是编辑模式,则生成预测编码
  193. if (!this.isEdit && !this.formData.forecastCode) {
  194. this.generateForecastCode()
  195. }
  196. })
  197. }
  198. },
  199. /**
  200. * 监听初始表单数据变化
  201. * @param {Object} val - 新的初始表单数据
  202. */
  203. initialFormData(val) {
  204. if (val && this.visible) {
  205. this.formData = this.cleanAndFormatFormData(val)
  206. }
  207. },
  208. /**
  209. * 监听预测ID变化
  210. * @param {string|number} val - 新的预测ID
  211. */
  212. forecastId: {
  213. handler(val) {
  214. if (val && this.isEdit && this.visible) {
  215. this.loadForecastDetail(val)
  216. }
  217. },
  218. immediate: true
  219. }
  220. },
  221. /**
  222. * 组件方法
  223. * @description 组件的业务逻辑方法集合
  224. */
  225. methods: {
  226. /**
  227. * 创建初始表单数据
  228. * @description 创建销售预测表单的初始数据结构
  229. * @returns {Object} 初始化的表单数据对象
  230. * @private
  231. */
  232. createInitialFormData() {
  233. return { ...DEFAULT_FORECAST_FORM }
  234. },
  235. /**
  236. * 清理和格式化表单数据
  237. * @description 对表单数据进行清理和格式化处理
  238. * @param {Object} data - 原始表单数据
  239. * @returns {Object} 清理和格式化后的表单数据
  240. * @private
  241. */
  242. cleanAndFormatFormData(data) {
  243. // 获取下个月的年份和月份作为默认值
  244. const now = new Date()
  245. const currentYear = now.getFullYear()
  246. const currentMonth = now.getMonth() + 1
  247. let defaultYear, defaultMonth
  248. if (currentMonth === 12) {
  249. // 当前是12月,下个月是明年1月
  250. defaultYear = currentYear + 1
  251. defaultMonth = 1
  252. } else {
  253. // 其他月份,直接 +1
  254. defaultYear = currentYear
  255. defaultMonth = currentMonth + 1
  256. }
  257. return {
  258. id: data.id || null,
  259. forecastCode: String(data.forecastCode || ''),
  260. year: data.year ? data.year.toString() : defaultYear.toString(),
  261. month: Number(data.month) || defaultMonth,
  262. customerId: data.customerId ? data.customerId.toString() : null,
  263. customerCode: String(data.customerCode || ''),
  264. customerName: String(data.customerName || ''),
  265. brandId: Number(data.brandId) || null,
  266. brandCode: String(data.brandCode || ''),
  267. brandName: String(data.brandName || ''),
  268. itemId: data.itemId ? data.itemId.toString() : null,
  269. itemCode: String(data.itemCode || ''),
  270. itemName: String(data.itemName || ''),
  271. specs: String(data.specs || ''),
  272. forecastQuantity: data.forecastQuantity !== undefined && data.forecastQuantity !== null && data.forecastQuantity !== '' ? Number(data.forecastQuantity) : null,
  273. currentInventory: Number(data.currentInventory) || null,
  274. approvalStatus: Number(data.approvalStatus) || APPROVAL_STATUS.PENDING,
  275. approvedName: String(data.approvedName || ''),
  276. approvedTime: data.approvedTime || null,
  277. createTime: data.createTime || null,
  278. updateTime: data.updateTime || null
  279. }
  280. },
  281. /**
  282. * 加载销售预测详情
  283. * @description 根据ID加载销售预测详情数据
  284. * @param {string|number} id - 销售预测ID
  285. * @returns {Promise<void>}
  286. * @private
  287. */
  288. async loadForecastDetail(id) {
  289. if (!id) return
  290. try {
  291. this.formLoading = true
  292. const res = await getForecastDetail(id)
  293. if (res.data && res.data.success && res.data.data) {
  294. const detailData = res.data.data
  295. this.formData = this.cleanAndFormatFormData(detailData)
  296. // 加载客户选项数据,确保客户下拉框能正确显示
  297. if (this.formData.customerId) {
  298. await this.loadCustomerOption(this.formData.customerId, this.formData.customerName)
  299. }
  300. // 加载物料选项数据,确保物料下拉框能正确显示
  301. if (this.formData.itemId) {
  302. await this.loadItemOption(this.formData.itemId, this.formData.itemName, this.formData.itemCode, this.formData.specs)
  303. }
  304. // 触发加载完成事件
  305. this.$emit(FORECAST_FORM_EVENTS.LOADED, this.formData)
  306. } else {
  307. this.$message.error(res.data?.msg || '获取详情失败')
  308. }
  309. } catch (error) {
  310. console.error('获取销售预测详情失败:', error)
  311. this.$message.error('获取详情失败,请稍后重试')
  312. } finally {
  313. this.formLoading = false
  314. }
  315. },
  316. /**
  317. * 加载单个客户选项
  318. * @description 为编辑模式加载特定客户的选项数据
  319. * @param {string|number} customerId - 客户ID
  320. * @param {string} customerName - 客户名称
  321. * @returns {Promise<void>}
  322. */
  323. async loadCustomerOption(customerId, customerName) {
  324. if (!customerId) return
  325. try {
  326. // customer-select组件会自动处理回显,我们只需要确保formData中有正确的值
  327. // 组件的watch会监听value变化并调用loadCustomerById方法
  328. } catch (error) {
  329. console.error('加载客户选项失败:', error)
  330. }
  331. },
  332. /**
  333. * 远程搜索客户
  334. * @description 根据关键字远程搜索客户数据
  335. * @param {string} query - 搜索关键字
  336. * @returns {Promise<void>}
  337. */
  338. async remoteSearchCustomer(query) {
  339. if (query === '') {
  340. this.customerOptions = []
  341. return
  342. }
  343. try {
  344. this.customerLoading = true
  345. const res = await getCustomerList({
  346. current: 1,
  347. size: 10,
  348. customerName: query
  349. })
  350. if (res.data && res.data.success && res.data.data) {
  351. const { records } = res.data.data
  352. this.customerOptions = records.map(item => ({
  353. value: item.id,
  354. label: item.customerName,
  355. customerCode: item.customerCode
  356. }))
  357. }
  358. } catch (error) {
  359. console.error('搜索客户失败:', error)
  360. } finally {
  361. this.customerLoading = false
  362. }
  363. },
  364. /**
  365. * 加载单个物料选项
  366. * @description 为编辑模式加载特定物料的选项数据
  367. * @param {string|number} itemId - 物料ID
  368. * @param {string} itemName - 物料名称
  369. * @param {string} itemCode - 物料编码
  370. * @param {string} specs - 规格
  371. * @returns {Promise<void>}
  372. */
  373. async loadItemOption(itemId, itemName, itemCode, specs) {
  374. if (!itemId) return
  375. try {
  376. // 如果选项中已经存在该物料,则不需要重新加载
  377. const existingOption = this.itemOptions.find(option => option.id === itemId)
  378. if (existingOption) return
  379. // 添加当前物料到选项中
  380. this.itemOptions = [{
  381. id: itemId,
  382. code: itemCode,
  383. name: itemName,
  384. specs: specs
  385. }]
  386. } catch (error) {
  387. console.error('加载物料选项失败:', error)
  388. }
  389. },
  390. /**
  391. * 远程搜索物料
  392. * @description 根据关键字远程搜索物料数据
  393. * @param {string} query - 搜索关键字
  394. * @returns {Promise<void>}
  395. */
  396. async remoteSearchItem(query) {
  397. if (query === '') {
  398. this.itemOptions = []
  399. return
  400. }
  401. try {
  402. this.itemLoading = true
  403. const res = await getItemList({
  404. current: 1,
  405. size: 10,
  406. itemName: query
  407. })
  408. if (res.data && res.data.success && res.data.data) {
  409. const { records } = res.data.data
  410. this.itemOptions = records.map(item => ({
  411. value: item.id,
  412. label: item.itemName,
  413. itemCode: item.itemCode,
  414. specs: item.specs
  415. }))
  416. }
  417. } catch (error) {
  418. console.error('搜索物料失败:', error)
  419. } finally {
  420. this.itemLoading = false
  421. }
  422. },
  423. /**
  424. * 客户选择变化处理
  425. * @description 处理客户选择变化,更新表单中的客户相关字段
  426. * @param {string|number} customerId - 客户ID
  427. * @returns {void}
  428. */
  429. handleCustomerChange(customerId) {
  430. const customer = this.customerOptions.find(item => item.value === customerId)
  431. if (customer) {
  432. this.formData.customerId = customer.value
  433. this.formData.customerCode = customer.customerCode
  434. this.formData.customerName = customer.label
  435. // 触发客户变更事件
  436. this.$emit(FORECAST_FORM_EVENTS.CUSTOMER_CHANGE, customer)
  437. }
  438. },
  439. /**
  440. * 物料选择变化处理
  441. * @description 处理物料选择变化,更新表单中的物料相关字段
  442. * @param {string|number} itemId - 物料ID
  443. * @returns {void}
  444. */
  445. handleItemChange(itemId) {
  446. const item = this.itemOptions.find(option => option.value === itemId)
  447. if (item) {
  448. this.formData.itemId = item.value
  449. this.formData.itemCode = item.itemCode
  450. this.formData.itemName = item.label
  451. this.formData.specs = item.specs
  452. // 触发物料变更事件
  453. this.$emit(FORECAST_FORM_EVENTS.ITEM_CHANGE, item)
  454. }
  455. },
  456. /**
  457. * 生成预测编码
  458. * @description 自动生成销售预测编码
  459. * @returns {void}
  460. */
  461. generateForecastCode() {
  462. const now = new Date()
  463. const year = now.getFullYear()
  464. const month = String(now.getMonth() + 1).padStart(2, '0')
  465. const timestamp = now.getTime().toString().slice(-6)
  466. this.formData.forecastCode = `FC-${year}-${month}-${timestamp}`
  467. },
  468. /**
  469. * 表单提交
  470. * @description 提交表单数据,根据编辑模式调用不同的API
  471. * @returns {Promise<void>}
  472. */
  473. async submitForm() {
  474. try {
  475. // 表单验证
  476. await this.$refs.form.validate()
  477. this.saveLoading = true
  478. // 准备提交数据
  479. const submitData = this.prepareSubmitData()
  480. let res
  481. if (this.isEdit) {
  482. res = await updateForecast(submitData)
  483. } else {
  484. res = await addForecast(submitData)
  485. }
  486. if (res.data && res.data.success) {
  487. this.$message.success(res.data.msg || (this.isEdit ? '修改成功' : '添加成功'))
  488. // 触发提交成功事件
  489. this.$emit(FORECAST_FORM_EVENTS.SUBMIT_SUCCESS, res.data.data)
  490. // 关闭表单
  491. this.closeForm()
  492. } else {
  493. this.$message.error(res.data?.msg || (this.isEdit ? '修改失败' : '添加失败'))
  494. }
  495. } catch (error) {
  496. if (error.fields) {
  497. // 表单验证失败
  498. return
  499. }
  500. console.error('保存失败:', error)
  501. this.$message.error('保存失败,请稍后重试')
  502. } finally {
  503. this.saveLoading = false
  504. }
  505. },
  506. /**
  507. * 准备提交数据
  508. * @description 复制表单数据并进行清理和格式化处理
  509. * @returns {Object} 准备好的提交数据
  510. * @private
  511. */
  512. prepareSubmitData() {
  513. const submitData = { ...this.formData }
  514. // 转换年份为数字
  515. if (submitData.year && typeof submitData.year === 'string') {
  516. submitData.year = parseInt(submitData.year, 10)
  517. }
  518. // 确保数值字段为数字类型
  519. if (submitData.forecastQuantity) {
  520. submitData.forecastQuantity = Number(submitData.forecastQuantity)
  521. }
  522. if (submitData.currentInventory) {
  523. submitData.currentInventory = Number(submitData.currentInventory)
  524. }
  525. return submitData
  526. },
  527. /**
  528. * 关闭表单
  529. * @description 关闭表单并重置数据
  530. * @returns {void}
  531. */
  532. closeForm() {
  533. // 触发取消事件
  534. this.$emit(FORECAST_FORM_EVENTS.CANCEL)
  535. // 更新可见性
  536. this.$emit('update:visible', false)
  537. // 重置表单数据
  538. this.formData = this.createInitialFormData()
  539. // 重置表单验证
  540. if (this.$refs.form) {
  541. this.$refs.form.resetFields()
  542. }
  543. },
  544. /**
  545. * 获取审批状态标签
  546. * @description 根据审批状态获取对应的标签文本
  547. * @param {number} status - 审批状态
  548. * @returns {string} 状态标签
  549. */
  550. getApprovalStatusLabel,
  551. /**
  552. * 获取审批状态类型
  553. * @description 根据审批状态获取对应的类型(用于标签样式)
  554. * @param {number} status - 审批状态
  555. * @returns {string} 状态类型
  556. */
  557. getApprovalStatusType,
  558. /**
  559. * 判断是否可以编辑
  560. * @description 根据审批状态判断记录是否可以编辑
  561. * @param {number} status - 审批状态
  562. * @returns {boolean} 是否可编辑
  563. */
  564. canEdit
  565. }
  566. }