forecastIndex.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
  1. import { getForecastList, addForecast, updateForecast } from '@/api/forecast'
  2. import { getCustomerList } from '@/api/common'
  3. import { getItemList } from '@/api/common'
  4. import {
  5. APPROVAL_STATUS,
  6. APPROVAL_STATUS_CONFIG,
  7. APPROVAL_STATUS_OPTIONS,
  8. getApprovalStatusLabel,
  9. getApprovalStatusType,
  10. canEdit,
  11. FORECAST_FORM_RULES,
  12. DEFAULT_FORECAST_FORM
  13. } from '@/constants/forecast'
  14. import { mapGetters } from 'vuex'
  15. /**
  16. * 经销商销售预测申报页面业务逻辑混入
  17. * @description 提供预测申报的增删改查、表单验证、远程搜索等功能
  18. */
  19. export default {
  20. data() {
  21. return {
  22. /** @type {Object} 表单数据 */
  23. form: { ...DEFAULT_FORECAST_FORM },
  24. /** @type {Object} 查询参数 */
  25. query: {},
  26. /** @type {boolean} 表格加载状态 */
  27. loading: true,
  28. /** @type {boolean} 表单提交状态 */
  29. submitting: false,
  30. /** @type {boolean} 添加/编辑弹窗显示状态 */
  31. dialogVisible: false,
  32. /** @type {boolean} 是否为编辑模式 */
  33. isEdit: false,
  34. /** @type {Object} 分页配置 */
  35. page: {
  36. pageSize: 10,
  37. currentPage: 1,
  38. total: 0
  39. },
  40. /** @type {Array<Object>} 表格数据 */
  41. data: [],
  42. /** @type {Array<Object>} 客户选项 */
  43. customerOptions: [],
  44. /** @type {boolean} 客户选项加载状态 */
  45. customerLoading: false,
  46. /** @type {Array<Object>} 物料选项 */
  47. itemOptions: [],
  48. /** @type {boolean} 物料选项加载状态 */
  49. itemLoading: false,
  50. /** @type {Object} avue表格配置 */
  51. option: {
  52. height: 'auto',
  53. calcHeight: 30,
  54. tip: false,
  55. searchShow: true,
  56. searchMenuSpan: 6,
  57. border: true,
  58. index: true,
  59. viewBtn: false,
  60. selection: false,
  61. addBtn: true,
  62. editBtn: false, // 禁用内置编辑按钮
  63. delBtn: false,
  64. excelBtn: false,
  65. columnBtn: false,
  66. refreshBtn: true,
  67. dialogClickModal: false,
  68. addBtnText: '新增预测申报',
  69. editBtnText: '编辑',
  70. // 添加这个配置来完全隐藏操作列
  71. menu: false,
  72. column: [
  73. {
  74. label: '预测编码',
  75. prop: 'forecastCode',
  76. search: true,
  77. searchSpan: 6,
  78. width: 150,
  79. overHidden: true
  80. },
  81. {
  82. label: '年份',
  83. prop: 'year',
  84. type: 'year',
  85. valueFormat: 'yyyy', // 添加这行,确保提交的数据是YYYY格式
  86. search: true,
  87. searchSpan: 6,
  88. width: 100
  89. },
  90. {
  91. label: '月份',
  92. prop: 'month',
  93. type: 'select',
  94. dicData: [
  95. { label: '1月', value: 1 },
  96. { label: '2月', value: 2 },
  97. { label: '3月', value: 3 },
  98. { label: '4月', value: 4 },
  99. { label: '5月', value: 5 },
  100. { label: '6月', value: 6 },
  101. { label: '7月', value: 7 },
  102. { label: '8月', value: 8 },
  103. { label: '9月', value: 9 },
  104. { label: '10月', value: 10 },
  105. { label: '11月', value: 11 },
  106. { label: '12月', value: 12 }
  107. ],
  108. search: true,
  109. searchSpan: 6,
  110. width: 100
  111. },
  112. {
  113. label: '客户名称',
  114. prop: 'customerName',
  115. search: true,
  116. searchSpan: 6,
  117. width: 180,
  118. overHidden: true
  119. },
  120. {
  121. label: '客户编码',
  122. prop: 'customerCode',
  123. search: false,
  124. width: 150,
  125. overHidden: true
  126. },
  127. {
  128. label: '品牌名称',
  129. prop: 'brandName',
  130. search: false,
  131. width: 120,
  132. overHidden: true
  133. },
  134. {
  135. label: '物料名称',
  136. prop: 'itemName',
  137. search: false,
  138. width: 200,
  139. overHidden: true
  140. },
  141. {
  142. label: '物料编码',
  143. prop: 'itemCode',
  144. search: false,
  145. width: 150,
  146. overHidden: true
  147. },
  148. {
  149. label: '规格',
  150. prop: 'specs',
  151. search: false,
  152. width: 150,
  153. overHidden: true
  154. },
  155. {
  156. label: '预测数量',
  157. prop: 'forecastQuantity',
  158. type: 'number',
  159. precision: 4,
  160. search: false,
  161. width: 120,
  162. align: 'right'
  163. },
  164. {
  165. label: '当前库存',
  166. prop: 'currentInventory',
  167. type: 'number',
  168. precision: 4,
  169. search: false,
  170. width: 120,
  171. align: 'right'
  172. },
  173. {
  174. label: '审批状态',
  175. prop: 'approvalStatus',
  176. type: 'select',
  177. dicData: APPROVAL_STATUS_OPTIONS,
  178. slot: true,
  179. search: true,
  180. searchSpan: 6,
  181. width: 120
  182. },
  183. {
  184. label: '审批人',
  185. prop: 'approvedName',
  186. search: false,
  187. width: 120,
  188. overHidden: true
  189. },
  190. {
  191. label: '审批时间',
  192. prop: 'approvedTime',
  193. type: 'datetime',
  194. format: 'yyyy-MM-dd HH:mm:ss',
  195. search: false,
  196. width: 160
  197. },
  198. {
  199. label: '创建时间',
  200. prop: 'createTime',
  201. type: 'datetime',
  202. format: 'yyyy-MM-dd HH:mm:ss',
  203. search: false,
  204. width: 160
  205. },
  206. // 添加自定义编辑按钮列
  207. {
  208. label: '操作',
  209. prop: 'editBtn',
  210. slot: true,
  211. width: 120,
  212. fixed: 'right'
  213. }
  214. ]
  215. },
  216. /** @type {Object} 表单验证规则 */
  217. formRules: FORECAST_FORM_RULES
  218. }
  219. },
  220. computed: {
  221. ...mapGetters(['permission']),
  222. /**
  223. * 权限配置
  224. * @returns {Object} 权限配置对象
  225. */
  226. permissionList() {
  227. return {
  228. // addBtn: this.vaildData(this.permission.forecast_add, false),
  229. // viewBtn: this.vaildData(this.permission.forecast_view, false),
  230. // delBtn: false, // 不提供删除功能
  231. // editBtn: this.vaildData(this.permission.forecast_edit, false)
  232. addBtn: false,
  233. viewBtn: false,
  234. delBtn: false, // 不提供删除功能
  235. editBtn: false
  236. }
  237. },
  238. /**
  239. * 弹窗标题
  240. * @returns {string} 弹窗标题文本
  241. */
  242. dialogTitle() {
  243. return this.isEdit ? '编辑预测申报' : '新增预测申报'
  244. }
  245. },
  246. created() {
  247. this.loadCustomerOptions()
  248. this.loadItemOptions()
  249. },
  250. methods: {
  251. /**
  252. * 加载客户选项(初始化时加载部分数据)
  253. * @returns {Promise<void>}
  254. */
  255. async loadCustomerOptions() {
  256. try {
  257. this.customerLoading = true
  258. const res = await getCustomerList(1, 20)
  259. if (res.data && res.data.success) {
  260. this.customerOptions = res.data.data.records.map(item => ({
  261. value: item.Customer_ID.toString(),
  262. label: item.Customer_NAME,
  263. customerCode: item.Customer_CODE,
  264. customerName: item.Customer_NAME
  265. }))
  266. }
  267. } catch (error) {
  268. console.error('加载客户选项失败:', error)
  269. this.$message.error('加载客户选项失败')
  270. } finally {
  271. this.customerLoading = false
  272. }
  273. },
  274. /**
  275. * 远程搜索客户
  276. * @param {string} query - 搜索关键词
  277. * @returns {Promise<void>}
  278. */
  279. async remoteSearchCustomer(query) {
  280. if (!query) {
  281. this.loadCustomerOptions()
  282. return
  283. }
  284. try {
  285. this.customerLoading = true
  286. const res = await getCustomerList(1, 50, {
  287. Customer_NAME: query
  288. })
  289. if (res.data && res.data.success) {
  290. this.customerOptions = res.data.data.records.map(item => ({
  291. value: item.Customer_ID.toString(),
  292. label: item.Customer_NAME,
  293. customerCode: item.Customer_CODE,
  294. customerName: item.Customer_NAME
  295. }))
  296. }
  297. } catch (error) {
  298. console.error('搜索客户失败:', error)
  299. } finally {
  300. this.customerLoading = false
  301. }
  302. },
  303. /**
  304. * 客户选择变化处理
  305. * @param {number} customerId - 客户ID
  306. * @returns {void}
  307. */
  308. handleCustomerChange(customerId) {
  309. const customer = this.customerOptions.find(item => item.value === customerId)
  310. if (customer) {
  311. this.form.customerId = customer.value
  312. this.form.customerCode = customer.customerCode
  313. this.form.customerName = customer.customerName
  314. }
  315. },
  316. /**
  317. * 加载物料选项(初始化时加载部分数据)
  318. * @returns {Promise<void>}
  319. */
  320. async loadItemOptions() {
  321. try {
  322. this.itemLoading = true
  323. const res = await getItemList(1, 20)
  324. if (res.data && res.data.success) {
  325. this.itemOptions = res.data.data.records.map(item => ({
  326. value: item.Item_ID,
  327. label: item.Item_Name,
  328. itemId: item.Item_ID,
  329. itemCode: item.Item_Code,
  330. itemName: item.Item_Name,
  331. specs: item.Item_PECS || ''
  332. }))
  333. }
  334. } catch (error) {
  335. console.error('加载物料选项失败:', error)
  336. this.$message.error('加载物料选项失败')
  337. } finally {
  338. this.itemLoading = false
  339. }
  340. },
  341. /**
  342. * 远程搜索物料
  343. * @param {string} query - 搜索关键词
  344. * @returns {Promise<void>}
  345. */
  346. async remoteSearchItem(query) {
  347. if (!query) {
  348. this.loadItemOptions()
  349. return
  350. }
  351. try {
  352. this.itemLoading = true
  353. const res = await getItemList(1, 50, {
  354. itemName: query
  355. })
  356. if (res.data && res.data.success) {
  357. this.itemOptions = res.data.data.records.map(item => ({
  358. value: item.Item_ID,
  359. label: item.Item_Name,
  360. itemId: item.Item_ID,
  361. itemCode: item.Item_Code,
  362. itemName: item.Item_Name,
  363. specs: item.Item_PECS || ''
  364. }))
  365. }
  366. } catch (error) {
  367. console.error('搜索物料失败:', error)
  368. } finally {
  369. this.itemLoading = false
  370. }
  371. },
  372. /**
  373. * 物料选择变化处理
  374. * @param {number} itemId - 物料ID
  375. * @returns {void}
  376. */
  377. handleItemChange(itemId) {
  378. const item = this.itemOptions.find(option => option.value === itemId)
  379. if (item) {
  380. this.form.itemId = item.value
  381. this.form.itemCode = item.itemCode
  382. this.form.itemName = item.itemName
  383. this.form.specs = item.specs
  384. }
  385. },
  386. /**
  387. * 获取审批状态标签
  388. * @param {number} status - 审批状态
  389. * @returns {string} 状态标签
  390. */
  391. getApprovalStatusLabel,
  392. /**
  393. * 获取审批状态类型
  394. * @param {number} status - 审批状态
  395. * @returns {string} 状态类型
  396. */
  397. getApprovalStatusType,
  398. /**
  399. * 检查是否可以编辑
  400. * @param {Object} row - 行数据
  401. * @returns {boolean} 是否可以编辑
  402. */
  403. canEditRow(row) {
  404. return canEdit(row.approvalStatus)
  405. },
  406. /**
  407. * 搜索重置
  408. * @returns {void}
  409. */
  410. searchReset() {
  411. this.query = {}
  412. this.onLoad(this.page)
  413. },
  414. /**
  415. * 搜索条件变化
  416. * @param {Object} params - 搜索参数
  417. * @param {Function} done - 完成回调
  418. * @returns {void}
  419. */
  420. searchChange(params, done) {
  421. this.query = params
  422. this.page.currentPage = 1
  423. this.onLoad(this.page, params)
  424. done()
  425. },
  426. /**
  427. * 页码变化
  428. * @param {number} currentPage - 当前页码
  429. * @returns {void}
  430. */
  431. currentChange(currentPage) {
  432. this.page.currentPage = currentPage
  433. },
  434. /**
  435. * 页大小变化
  436. * @param {number} pageSize - 页大小
  437. * @returns {void}
  438. */
  439. sizeChange(pageSize) {
  440. this.page.pageSize = pageSize
  441. },
  442. /**
  443. * 刷新数据
  444. * @returns {void}
  445. */
  446. refreshChange() {
  447. this.onLoad(this.page, this.query)
  448. },
  449. /**
  450. * 加载数据
  451. * @param {Object} page - 分页参数
  452. * @param {Object} params - 查询参数
  453. * @returns {Promise<void>}
  454. */
  455. async onLoad(page, params = {}) {
  456. this.loading = true
  457. try {
  458. const res = await getForecastList(page.currentPage, page.pageSize, {
  459. ...params,
  460. ...this.query
  461. })
  462. if (res.data && res.data.success) {
  463. const data = res.data.data
  464. this.page.total = data.total
  465. this.data = data.records
  466. } else {
  467. this.$message.error(res.data?.msg || '加载数据失败')
  468. this.data = []
  469. this.page.total = 0
  470. }
  471. } catch (error) {
  472. console.error('加载数据失败:', error)
  473. this.$message.error('加载数据失败,请稍后重试')
  474. this.data = []
  475. this.page.total = 0
  476. } finally {
  477. this.loading = false
  478. }
  479. },
  480. /**
  481. * 新增按钮点击
  482. * @returns {void}
  483. */
  484. rowAdd() {
  485. this.isEdit = false
  486. this.form = { ...DEFAULT_FORECAST_FORM }
  487. this.dialogVisible = true
  488. },
  489. /**
  490. * 编辑按钮点击
  491. * @param {Object} row - 行数据
  492. * @param {number} index - 行索引
  493. * @returns {void}
  494. */
  495. rowEdit(row, index) {
  496. if (!this.canEditRow(row)) {
  497. this.$message.warning('已审批或已拒绝的记录不能修改')
  498. return
  499. }
  500. this.isEdit = true
  501. this.form = {
  502. ...row,
  503. // 确保年份字段为字符串类型,以匹配el-date-picker的value-format="yyyy"
  504. year: row.year ? row.year.toString() : new Date().getFullYear().toString(),
  505. customerId: row.customerId.toString(),
  506. }
  507. this.dialogVisible = true
  508. },
  509. /**
  510. * 表单提交
  511. * @returns {Promise<void>}
  512. */
  513. async rowSave() {
  514. try {
  515. // 表单验证
  516. await this.$refs.form.validate()
  517. this.submitting = true
  518. let res
  519. if (this.isEdit) {
  520. res = await updateForecast(this.form)
  521. } else {
  522. res = await addForecast(this.form)
  523. }
  524. if (res.data && res.data.success) {
  525. this.$message.success(res.data.msg || (this.isEdit ? '修改成功' : '添加成功'))
  526. this.dialogVisible = false
  527. this.refreshChange()
  528. } else {
  529. this.$message.error(res.data?.msg || (this.isEdit ? '修改失败' : '添加失败'))
  530. }
  531. } catch (error) {
  532. if (error.fields) {
  533. // 表单验证失败
  534. return
  535. }
  536. console.error('保存失败:', error)
  537. this.$message.error('保存失败,请稍后重试')
  538. } finally {
  539. this.submitting = false
  540. }
  541. },
  542. /**
  543. * 取消表单
  544. * @returns {void}
  545. */
  546. rowCancel() {
  547. this.dialogVisible = false
  548. this.form = { ...DEFAULT_FORECAST_FORM }
  549. },
  550. /**
  551. * 生成预测编码
  552. * @returns {void}
  553. */
  554. generateForecastCode() {
  555. const now = new Date()
  556. const year = now.getFullYear()
  557. const month = String(now.getMonth() + 1).padStart(2, '0')
  558. const timestamp = now.getTime().toString().slice(-6)
  559. this.form.forecastCode = `FC-${year}-${month}-${timestamp}`
  560. }
  561. }
  562. }