imageStoreApplyIndex.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. import { getList, getDetail, update, getAttachmentList } from '@/api/order/image-store-apply'
  2. import { formatFileSize } from '@/util/util'
  3. import { mapGetters } from 'vuex'
  4. import {
  5. APPLICANT_TYPE,
  6. APPLICANT_TYPE_OPTIONS,
  7. AUDIT_STATUS,
  8. AUDIT_STATUS_OPTIONS,
  9. NEED_REIMBURSE,
  10. NEED_REIMBURSE_OPTIONS,
  11. getApplicantTypeLabel,
  12. getApplicantTypeType,
  13. getAuditStatusLabel,
  14. getAuditStatusType,
  15. getNeedReimburseLabel,
  16. getNeedReimburseType,
  17. canAudit,
  18. isAuditCompleted,
  19. isDealerApplicant,
  20. isStoreApplicant
  21. } from '@/constants/image-store-apply'
  22. /**
  23. * @typedef {import('@/api/order/image-store-apply').ImageStoreApplyRecord} ImageStoreApplyRecord
  24. * @typedef {import('@/api/order/image-store-apply').QualificationItem} QualificationItem
  25. * @typedef {import('@/api/order/image-store-apply').ApplyAttachmentRecord} ApplyAttachmentRecord
  26. * @typedef {import('@/api/order/image-store-apply').ImageStoreApplyQueryParams} ImageStoreApplyQueryParams
  27. */
  28. /**
  29. * 形象申请页面业务逻辑 Mixin
  30. * @mixin
  31. */
  32. export default {
  33. data() {
  34. return {
  35. /** @type {ImageStoreApplyRecord[]} */
  36. data: [],
  37. /** @type {ImageStoreApplyRecord} */
  38. form: {},
  39. /** @type {ImageStoreApplyQueryParams} */
  40. query: {},
  41. loading: true,
  42. // 分页信息
  43. page: {
  44. pageSize: 10,
  45. currentPage: 1,
  46. total: 0
  47. },
  48. // 详情对话框
  49. detailVisible: false,
  50. /** @type {ImageStoreApplyRecord|null} */
  51. detailData: null,
  52. /** @type {QualificationItem[]} */
  53. qualificationList: [],
  54. /** @type {ApplyAttachmentRecord[]} */
  55. attachmentList: [],
  56. attachmentLoading: false,
  57. // 审核相关
  58. auditVisible: false,
  59. rejectVisible: false,
  60. auditLoading: false,
  61. /** @type {Object} */
  62. rejectForm: {
  63. id: '',
  64. applyNo: '',
  65. customerName: '',
  66. auditRemark: ''
  67. },
  68. /** @type {Object} */
  69. rejectRules: {
  70. auditRemark: [
  71. { required: true, message: '请输入拒绝原因', trigger: 'blur' },
  72. { min: 5, max: 200, message: '拒绝原因长度在5到200个字符', trigger: 'blur' }
  73. ]
  74. },
  75. // 图片预览
  76. previewVisible: false,
  77. previewUrl: '',
  78. // 选择相关
  79. selectionList: [],
  80. // 表格配置
  81. option: {
  82. height: 'auto',
  83. calcHeight: 30,
  84. tip: false,
  85. searchShow: true,
  86. searchMenuSpan: 6,
  87. searchIndex: 3,
  88. border: true,
  89. index: true,
  90. viewBtn: false,
  91. editBtn: false,
  92. delBtn: false,
  93. addBtn: false,
  94. column: [
  95. {
  96. label: '申请编号',
  97. prop: 'applyNo',
  98. minWidth: 140,
  99. search: true,
  100. searchPlaceholder: '请输入申请编号'
  101. },
  102. {
  103. label: '申请方类型',
  104. prop: 'applicantType',
  105. minWidth: 100,
  106. slot: true,
  107. search: true,
  108. type: 'select',
  109. dicData: APPLICANT_TYPE_OPTIONS
  110. },
  111. {
  112. label: '客户编码',
  113. prop: 'customerCode',
  114. minWidth: 120,
  115. search: true,
  116. searchPlaceholder: '请输入客户编码'
  117. },
  118. {
  119. label: '客户名称',
  120. prop: 'customerName',
  121. minWidth: 180,
  122. search: true,
  123. searchPlaceholder: '请输入客户名称'
  124. },
  125. {
  126. label: '联系人',
  127. prop: 'contactName',
  128. minWidth: 100
  129. },
  130. {
  131. label: '联系电话',
  132. prop: 'contactPhone',
  133. minWidth: 120
  134. },
  135. {
  136. label: '品牌名称',
  137. prop: 'brandName',
  138. minWidth: 120,
  139. search: true,
  140. searchPlaceholder: '请输入品牌名称'
  141. },
  142. {
  143. label: '申请金额',
  144. prop: 'applyAmount',
  145. minWidth: 120,
  146. slot: true
  147. },
  148. {
  149. label: '审核状态',
  150. prop: 'auditStatus',
  151. minWidth: 100,
  152. slot: true,
  153. search: true,
  154. type: 'select',
  155. dicData: AUDIT_STATUS_OPTIONS
  156. },
  157. {
  158. label: '审核金额',
  159. prop: 'auditAmount',
  160. minWidth: 120,
  161. slot: true
  162. },
  163. {
  164. label: '审核人',
  165. prop: 'auditorName',
  166. minWidth: 100
  167. },
  168. {
  169. label: '审核时间',
  170. prop: 'auditTime',
  171. minWidth: 150
  172. },
  173. {
  174. label: '提交时间',
  175. prop: 'submitTime',
  176. minWidth: 150,
  177. search: true,
  178. type: 'daterange',
  179. searchPlaceholder: '请选择提交时间范围',
  180. formatter: (row, column, cellValue) => {
  181. if (!cellValue) return '-'
  182. // 如果是数组格式,取第一个元素
  183. if (Array.isArray(cellValue)) {
  184. return cellValue[0] || '-'
  185. }
  186. return cellValue
  187. }
  188. }
  189. ]
  190. }
  191. }
  192. },
  193. computed: {
  194. ...mapGetters(['permission']),
  195. /** @returns {Object} */
  196. permissionList() {
  197. return {
  198. viewBtn: false,
  199. editBtn: false,
  200. delBtn: false,
  201. addBtn: false,
  202. }
  203. },
  204. /** @returns {string[]} */
  205. ids() {
  206. const ids = []
  207. this.selectionList.forEach(ele => {
  208. ids.push(ele.id)
  209. })
  210. return ids
  211. }
  212. },
  213. methods: {
  214. isDealerApplicant,
  215. isStoreApplicant,
  216. /**
  217. * 格式化金额显示
  218. * @param {number|string} amount - 金额
  219. * @returns {string} 格式化后的金额
  220. */
  221. formatAmount(amount) {
  222. if (!amount || parseFloat(amount) === 0) return '0.00'
  223. return parseFloat(amount).toFixed(2)
  224. },
  225. getApplicantTypeLabel,
  226. getApplicantTypeType,
  227. getAuditStatusType,
  228. getAuditStatusLabel,
  229. /**
  230. * 获取是否需要报销标签
  231. * @param {number} value - 报销状态值
  232. * @returns {string} 对应的标签文本
  233. */
  234. getNeedReimburseText(value) {
  235. return getNeedReimburseLabel(value)
  236. },
  237. /**
  238. * 获取是否需要报销类型
  239. * @param {number} value - 报销状态值
  240. * @returns {string} 对应的Element UI标签类型
  241. */
  242. getNeedReimburseType(value) {
  243. return getNeedReimburseType(value)
  244. },
  245. /**
  246. * 判断是否为图片文件
  247. * @param {string} fileType - 文件类型
  248. * @returns {boolean} 是否为图片
  249. */
  250. isImageFile(fileType) {
  251. const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']
  252. return imageTypes.includes(fileType.toLowerCase())
  253. },
  254. /**
  255. * 判断是否为PDF文件
  256. * @param {string} fileType - 文件类型
  257. * @returns {boolean} 是否为PDF
  258. */
  259. isPdfFile(fileType) {
  260. return fileType.toLowerCase() === 'pdf'
  261. },
  262. /**
  263. * 格式化文件大小
  264. * @param {number} size - 文件大小(字节)
  265. * @returns {string} 格式化后的文件大小
  266. */
  267. formatFileSize(size) {
  268. return formatFileSize(size)
  269. },
  270. /**
  271. * 处理图片加载错误
  272. * @param {Event} event - 错误事件
  273. */
  274. handleImageError(event) {
  275. // 图片加载失败时显示默认图标
  276. event.target.style.display = 'none'
  277. const parent = event.target.parentNode
  278. if (parent && !parent.querySelector('.error-icon')) {
  279. const errorIcon = document.createElement('div')
  280. errorIcon.className = 'error-icon'
  281. errorIcon.innerHTML = '<i class="el-icon-picture-outline"></i>'
  282. parent.appendChild(errorIcon)
  283. }
  284. },
  285. /**
  286. * 预览PDF文件
  287. * @param {ApplyAttachmentRecord} row - 附件数据
  288. */
  289. handlePdfPreview(row) {
  290. if (!row.fileUrl) {
  291. this.$message.warning('文件链接不存在')
  292. return
  293. }
  294. // 在新窗口中打开PDF
  295. window.open(row.fileUrl, '_blank')
  296. },
  297. /**
  298. * 查看详情
  299. * @param {ImageStoreApplyRecord} row - 行数据
  300. * @returns {Promise<void>}
  301. */
  302. async handleView(row) {
  303. try {
  304. this.loading = true
  305. // 获取详情数据
  306. const detailRes = await getDetail(row.id)
  307. if (detailRes.data.success) {
  308. this.detailData = detailRes.data.data
  309. const qualificationList = JSON.parse(detailRes.data.data.qualificationInfo)
  310. this.qualificationList = qualificationList || []
  311. // 获取附件列表
  312. await this.loadAttachmentList(row.id)
  313. this.detailVisible = true
  314. } else {
  315. this.$message.error(detailRes.data.msg || '获取详情失败')
  316. }
  317. } catch (error) {
  318. console.error('获取详情失败:', error)
  319. this.$message.error('获取详情失败')
  320. } finally {
  321. this.loading = false
  322. }
  323. },
  324. /**
  325. * 加载附件列表
  326. * @param {string} applyId - 申请ID
  327. * @returns {Promise<void>}
  328. */
  329. async loadAttachmentList(applyId) {
  330. try {
  331. this.attachmentLoading = true
  332. const attachmentRes = await getAttachmentList(1, 100 ,{
  333. applyId
  334. })
  335. if (attachmentRes.data.success) {
  336. this.attachmentList = attachmentRes.data.data.records || []
  337. } else {
  338. this.$message.error(attachmentRes.data.msg || '获取附件列表失败')
  339. }
  340. } catch (error) {
  341. console.error('获取附件列表失败:', error)
  342. this.$message.error('获取附件列表失败')
  343. } finally {
  344. this.attachmentLoading = false
  345. }
  346. },
  347. /**
  348. * 处理审核通过操作
  349. * @param {ImageStoreApplyRecord} row - 行数据
  350. */
  351. handleApprove(row) {
  352. if (!canAudit(row.auditStatus)) {
  353. this.$message.warning('当前状态不允许审核操作')
  354. return
  355. }
  356. this.$confirm('确定要审核通过该申请吗?', '确认审核', {
  357. confirmButtonText: '确定',
  358. cancelButtonText: '取消',
  359. type: 'success'
  360. }).then(async () => {
  361. await this.submitAudit(row, AUDIT_STATUS.APPROVED, '审核通过')
  362. }).catch(() => {
  363. // 用户取消操作
  364. })
  365. },
  366. /**
  367. * 处理审核不通过操作
  368. * @param {ImageStoreApplyRecord} row - 行数据
  369. */
  370. handleReject(row) {
  371. if (!canAudit(row.auditStatus)) {
  372. this.$message.warning('当前状态不允许审核操作')
  373. return
  374. }
  375. this.rejectForm = {
  376. id: row.id,
  377. applyNo: row.applyNo,
  378. customerName: row.customerName,
  379. auditRemark: ''
  380. }
  381. // 重置表单验证
  382. this.$nextTick(() => {
  383. if (this.$refs.rejectFormRef) {
  384. this.$refs.rejectFormRef.clearValidate()
  385. }
  386. })
  387. this.rejectVisible = true
  388. },
  389. /**
  390. * 提交拒绝审核
  391. * @returns {Promise<void>}
  392. */
  393. async submitReject() {
  394. try {
  395. // 表单验证
  396. await this.$refs.rejectFormRef.validate()
  397. // 找到对应的行数据
  398. const row = this.data.find(item => item.id === this.rejectForm.id)
  399. if (!row) {
  400. this.$message.error('未找到对应的申请记录')
  401. return
  402. }
  403. await this.submitAudit(row, AUDIT_STATUS.REJECTED, this.rejectForm.auditRemark)
  404. this.rejectVisible = false
  405. } catch (error) {
  406. if (error !== false) { // 不是表单验证失败
  407. console.error('提交拒绝审核失败:', error)
  408. this.$message.error('提交失败,请重试')
  409. }
  410. }
  411. },
  412. /**
  413. * 提交审核(通用方法)
  414. * @param {ImageStoreApplyRecord} row - 行数据
  415. * @param {number} auditStatus - 审核状态 1-通过 2-不通过
  416. * @param {string} auditRemark - 审核备注
  417. * @returns {Promise<void>}
  418. */
  419. async submitAudit(row, auditStatus, auditRemark) {
  420. try {
  421. this.auditLoading = true
  422. const params = {
  423. id: row.id,
  424. auditStatus,
  425. auditRemark
  426. }
  427. const res = await update(params)
  428. if (res.data.success) {
  429. this.$message.success('审核操作成功')
  430. // 更新列表数据
  431. const index = this.data.findIndex(item => item.id === row.id)
  432. if (index !== -1) {
  433. this.data[index].auditStatus = auditStatus
  434. this.data[index].auditRemark = auditRemark
  435. this.data[index].auditTime = new Date().toLocaleString()
  436. }
  437. // 如果详情对话框打开,也更新详情数据
  438. if (this.detailVisible && this.detailData && this.detailData.id === row.id) {
  439. this.detailData.auditStatus = auditStatus
  440. this.detailData.auditRemark = auditRemark
  441. this.detailData.auditTime = new Date().toLocaleString()
  442. }
  443. } else {
  444. this.$message.error(res.data.msg || '审核操作失败')
  445. }
  446. } catch (error) {
  447. console.error('审核操作失败:', error)
  448. this.$message.error('审核操作失败')
  449. } finally {
  450. this.auditLoading = false
  451. }
  452. },
  453. /**
  454. * 预览图片
  455. * @param {ApplyAttachmentRecord} row - 附件数据
  456. */
  457. handlePreview(row) {
  458. if (!row.fileUrl) {
  459. this.$message.warning('文件链接不存在')
  460. return
  461. }
  462. this.previewUrl = row.fileUrl
  463. this.previewVisible = true
  464. },
  465. /**
  466. * 下载文件
  467. * @param {ApplyAttachmentRecord} row - 附件数据
  468. */
  469. handleDownload(row) {
  470. if (!row.fileUrl) {
  471. this.$message.warning('文件链接不存在')
  472. return
  473. }
  474. // 创建下载链接
  475. const link = document.createElement('a')
  476. link.href = row.fileUrl
  477. link.download = row.fileName || '下载文件'
  478. link.target = '_blank'
  479. document.body.appendChild(link)
  480. link.click()
  481. document.body.removeChild(link)
  482. },
  483. /**
  484. * 搜索变化事件
  485. * @param {Object} params - 搜索参数
  486. * @param {Function} done - 完成回调
  487. */
  488. searchChange(params, done) {
  489. this.query = params
  490. this.onLoad(this.page, params)
  491. done()
  492. },
  493. /**
  494. * 搜索重置事件
  495. */
  496. searchReset() {
  497. this.query = {}
  498. this.onLoad(this.page)
  499. },
  500. /**
  501. * 选择变化事件
  502. * @param {Array} selection - 选中的数据
  503. */
  504. selectionChange(selection) {
  505. this.selectionList = selection
  506. },
  507. /**
  508. * 清空选择事件
  509. */
  510. selectionClear() {
  511. this.selectionList = []
  512. },
  513. /**
  514. * 当前页变化事件
  515. * @param {number} currentPage - 当前页
  516. */
  517. currentChange(currentPage) {
  518. this.page.currentPage = currentPage
  519. },
  520. /**
  521. * 页大小变化事件
  522. * @param {number} pageSize - 页大小
  523. */
  524. sizeChange(pageSize) {
  525. this.page.pageSize = pageSize
  526. },
  527. /**
  528. * 刷新事件
  529. */
  530. refreshChange() {
  531. this.onLoad(this.page, this.query)
  532. },
  533. /**
  534. * 打开前事件
  535. * @param {Function} done - 完成回调
  536. * @param {string} type - 操作类型
  537. */
  538. beforeOpen(done, type) {
  539. done()
  540. },
  541. /**
  542. * 加载数据
  543. * @param {Object} page - 分页参数
  544. * @param {Object} params - 查询参数
  545. * @returns {Promise<void>}
  546. */
  547. async onLoad(page, params = {}) {
  548. try {
  549. this.loading = true
  550. const queryParams = {
  551. ...params,
  552. current: page.currentPage,
  553. size: page.pageSize
  554. }
  555. const res = await getList(
  556. queryParams.current,
  557. queryParams.size,
  558. queryParams
  559. )
  560. if (res.data.success) {
  561. this.data = res.data.data.records || []
  562. this.page.total = res.data.data.total || 0
  563. } else {
  564. this.$message.error(res.data.msg || '获取数据失败')
  565. }
  566. } catch (error) {
  567. console.error('获取数据失败:', error)
  568. this.$message.error('获取数据失败')
  569. } finally {
  570. this.loading = false
  571. }
  572. }
  573. }
  574. }