EventManageView.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. <!--
  2. * @Author: fxs bjnsfxs@163.com
  3. * @Date: 2024-09-02 17:57:15
  4. * @LastEditors: fxs bjnsfxs@163.com
  5. * @LastEditTime: 2024-10-14 18:35:26
  6. * @FilePath: \Game-Backstage-Management-System\src\views\AppManage\EventManageView.vue
  7. * @Description:
  8. *
  9. -->
  10. <script setup lang="ts">
  11. import type { ResponseInfo } from '@/types/res'
  12. import HeaderCard from '@/components/dataAnalysis/HeaderCard.vue'
  13. import router from '@/router'
  14. import FileUpload from '@/components/form/FileUpload.vue'
  15. import axiosInstance from '@/utils/axios/axiosInstance'
  16. import { ref, reactive, computed } from 'vue'
  17. import { useRequest } from '@/hooks/useRequest'
  18. import { useCommonStore } from '@/stores/useCommon'
  19. import { downLoadData } from '@/utils/table/table'
  20. import { resetTimeToMidnight } from '@/utils/common'
  21. import { shouldListenToEvent } from '@/utils/table/table'
  22. import { throttleFunc } from '@/utils/common'
  23. interface uploadEvent {
  24. id?: number // 更新需要传入id
  25. gid: string
  26. actionId: string
  27. actionName: string
  28. status: number
  29. remark?: string
  30. }
  31. interface UploadOption {
  32. id?: number // 更新需要传入id
  33. optionId: string
  34. optionName: string
  35. optionType: string
  36. actionId: number
  37. status: number
  38. }
  39. interface DownLoadEvent {
  40. actionId: string
  41. actionName: string
  42. createdAt: string
  43. gid: string
  44. id: number
  45. remark: string
  46. status: number
  47. updatedAt: string
  48. }
  49. interface DownLoadOption {
  50. actionId: number
  51. createdAt: string
  52. id: number
  53. optionId: string
  54. optionName: string
  55. optionType: string
  56. status: number
  57. updatedAt: string
  58. }
  59. type GetTableReturn<T> = T extends 'all'
  60. ? {
  61. allEventTable: Array<DownLoadEvent>
  62. allOptionsInfo: { [key: string]: Array<DownLoadOption> }
  63. }
  64. : T extends 'event'
  65. ? { allEventTable: Array<DownLoadEvent> }
  66. : T extends 'option'
  67. ? { allOptionsInfo: { [key: string]: Array<DownLoadOption> } }
  68. : never
  69. const { selectInfo } = useCommonStore()
  70. const { AllApi } = useRequest()
  71. // 事件表格ref
  72. const eventTableRef = ref()
  73. // 头部ref
  74. const headerCard = ref<typeof HeaderCard>()
  75. // 上传ref
  76. const uploadRef = ref<InstanceType<typeof FileUpload> | null>(null)
  77. // 上传的信息
  78. const uploadInfo = reactive({
  79. dialogTitle: '上传事件信息'
  80. })
  81. // 现在的路由路径名称
  82. const nowRouteName = computed(() => router.currentRoute.value.name)
  83. /**
  84. * @description: 进入详情页,触发headercard的添加事件,增加一个面包屑导航
  85. * @param {*} info 传入的信息
  86. * @return {*}
  87. */
  88. const headerAddPath = (info: any) => {
  89. const { name, pathName } = info
  90. headerCard.value?.addPath(name, pathName)
  91. }
  92. /**
  93. * @description: 展示上传之后的反馈信息
  94. * @param {*} state 这个状态只表示是否全部成功,其余情况皆为false
  95. * @param {*} name 区分是事件还是选项
  96. * @return {*}
  97. */
  98. const showUploadMsg = (failedCount: number, totalCount: number, name: string): boolean => {
  99. if (failedCount < 0) throw new Error('Error!FailedCount < 0')
  100. let state = true
  101. let duration = 3000
  102. let title = '上传成功'
  103. let position: 'top-left' | 'top-right' = 'top-left'
  104. let message: string = `${name}全部上传成功`
  105. let type: 'success' | 'warning' | 'error' = 'success'
  106. // 有上传失败的情况下
  107. if (failedCount > 0) {
  108. state = false
  109. position = 'top-right'
  110. duration = 8000
  111. if (failedCount === totalCount) {
  112. message = `${name}全部上传失败`
  113. type = 'error'
  114. } else {
  115. message = `${name}部分上传失败`
  116. type = 'warning'
  117. }
  118. }
  119. ElNotification({
  120. type,
  121. title,
  122. message,
  123. duration,
  124. position
  125. })
  126. return state
  127. }
  128. /**
  129. * @description: 提交所有新上传的事件及选项请求
  130. * @param {*} reqList 请求列表
  131. * @param {*} msg 提示信息,用于展示上传之后返回的消息
  132. * @return {*}
  133. */
  134. const submitUpload = async (reqList: Array<Promise<boolean>>, msg: string): Promise<boolean> => {
  135. const result = await Promise.allSettled(reqList).then((res) => {
  136. const failedList = res.filter((item: any) => item.value === false)
  137. const failedCount = failedList.length
  138. const totalCount = res.length
  139. let state = showUploadMsg(failedCount, totalCount, msg)
  140. return state
  141. })
  142. return result
  143. }
  144. /**
  145. * @description: 请求表格数据,如果一次请求没有拿到所有的数据,则再请求一次
  146. * @param {*} url 请求地址
  147. * @param {*} otherInfo 请求参数
  148. * @param {*} offset 偏移量
  149. * @param {*} limit 请求数量
  150. * @return {*}
  151. */
  152. const getTableData = async (
  153. url: string,
  154. otherInfo: any,
  155. offset = 0,
  156. limit = 10000
  157. ): Promise<Array<any>> => {
  158. let total = limit + offset // 目前请求到的总数
  159. let finalResult = []
  160. finalResult = await axiosInstance
  161. .post(url, {
  162. offset,
  163. limit,
  164. ...otherInfo
  165. })
  166. .then(async (res) => {
  167. let resData = JSON.parse(JSON.stringify(res))
  168. let result = []
  169. if (!resData.data) return []
  170. if (resData.count > total) {
  171. result = await getTableData(url, otherInfo, total, limit)
  172. }
  173. return [...resData.data, ...result]
  174. })
  175. .catch((err) => {
  176. console.log(err)
  177. return []
  178. })
  179. return finalResult
  180. }
  181. /**
  182. * @description: 批量请求选项数据
  183. * @param {*} url 请求的地址
  184. * @param {*} reqParams 请求的参数列表,主要是actionid
  185. * @param {*} eventTable 事件列表
  186. * @return {Promise<Object>} 返回一个最终的结果,其中key是每个事件的actionid,值是选项列表
  187. */
  188. const batReqOptionsData = async (
  189. url: string,
  190. reqParams: Array<any>,
  191. eventTable: Array<DownLoadEvent>
  192. ): Promise<{ [key: string]: Array<DownLoadOption> }> => {
  193. let reqList: Array<Promise<any>> = []
  194. let finalResult: {
  195. [key: string]: Array<DownLoadOption>
  196. } = {}
  197. reqParams.map((item) => {
  198. reqList.push(
  199. getTableData(url, item)
  200. .then((res) => {
  201. let actionId = eventTable.find((i) => i.id === item.actionId)?.actionId
  202. if (actionId) finalResult[actionId] = res
  203. })
  204. .catch((err) => {
  205. console.log(err)
  206. })
  207. )
  208. })
  209. await Promise.all(reqList)
  210. return finalResult
  211. }
  212. /**
  213. * @description: 拿到事件数据和选项数据,首先需要拿到所有的事件列表,然后将他们的actionId抽出来形成一个列表,这个列表去作为optin的查询参数批量查询
  214. * @return {*} 返回事件数据和选项数据
  215. */
  216. const getAllTable = async <T extends 'all' | 'event' | 'option' = 'all'>(
  217. table: T = 'all' as any
  218. ): Promise<GetTableReturn<T>> => {
  219. let allEventTable: Array<DownLoadEvent> = [],
  220. allOptionsInfo: { [key: string]: Array<DownLoadOption> } = {}
  221. allEventTable = await getTableData(AllApi.gameActionList, { gid: selectInfo.gid })
  222. let optionReqList = allEventTable.map((item) => {
  223. return { actionId: item.id }
  224. })
  225. allOptionsInfo = await batReqOptionsData(
  226. AllApi.gameActionOptionList,
  227. optionReqList,
  228. allEventTable
  229. )
  230. // 只需要事件列表在这里就返回
  231. if (table === 'event') {
  232. return { allEventTable } as GetTableReturn<T>
  233. }
  234. // 只需要选项在这里就只返回选项
  235. if (table === 'option') {
  236. return { allOptionsInfo } as GetTableReturn<T>
  237. }
  238. return { allEventTable, allOptionsInfo } as GetTableReturn<T>
  239. }
  240. /**
  241. * @description: 开始上传
  242. * @return {*}
  243. */
  244. const startUpload = async () => {
  245. if (uploadRef.value) {
  246. uploadRef.value.startUpload()
  247. }
  248. }
  249. /**
  250. * @description: 获取事件列表和选项列表的数据,开始下载
  251. * @return {*}
  252. */
  253. const startDownload = async () => {
  254. let { allEventTable, allOptionsInfo } = await getAllTable()
  255. downLoadData(`allevents_${resetTimeToMidnight(new Date())}`, {
  256. allEventTable,
  257. allOptionsInfo
  258. })
  259. }
  260. // 把开始下载方法包装一下,节流
  261. const throttleStartDownload = throttleFunc(startDownload, 1000)
  262. /**
  263. * @description: 创建一个上传请求函数
  264. * @param {*} url 请求地址
  265. * @param {*} reqParams 请求参数
  266. * @param {*} name 上传的是事件还是选项
  267. * @return {*}
  268. */
  269. const createUploadReqFunc = async (url: string, reqParams: any, name: string): Promise<boolean> => {
  270. let result: boolean = false
  271. try {
  272. const res: ResponseInfo = await axiosInstance.post<string, ResponseInfo>(url, reqParams)
  273. if (res.code !== 0) {
  274. ElNotification({
  275. type: 'warning',
  276. title: '事件上传失败',
  277. dangerouslyUseHTMLString: true,
  278. message: `${name}上传失败.<br/>${res.msg}`,
  279. position: 'top-left',
  280. duration: 8000
  281. })
  282. }
  283. result = res.code === 0
  284. } catch (err: any) {
  285. ElNotification({
  286. type: 'error',
  287. title: '上传失败',
  288. message: `${name}上传失败,请检查参数`,
  289. position: 'top-right',
  290. duration: 5000
  291. })
  292. console.log(err)
  293. }
  294. return result
  295. }
  296. /**
  297. * @description: 上传所有事件
  298. * @param {*} uploadEventTable 上传的事件列表
  299. * @param {*} allEventTable 获取的事件列表
  300. * @return {Promise<boolean>} 返回是否上传成功
  301. */
  302. const uploadEvent = async (
  303. uploadEventTable: Array<uploadEvent>,
  304. allEventTable: Array<DownLoadEvent>
  305. ): Promise<boolean> => {
  306. let eventReqUrl = AllApi.setGameAction
  307. let eventReqList: Array<Promise<boolean>> = []
  308. uploadEventTable.map((item: uploadEvent) => {
  309. let { id, ...otherInfo } = item
  310. if (allEventTable.some((i: DownLoadEvent) => i.actionId === item.actionId)) {
  311. eventReqUrl = AllApi.updateGameAction
  312. }
  313. let eventReq = createUploadReqFunc(eventReqUrl, otherInfo, item.actionName)
  314. eventReqList.push(eventReq) // 统一放入请求列表中
  315. })
  316. return await submitUpload(eventReqList, '事件') // 等待所有的事件请求完成
  317. }
  318. /**
  319. * @description: 上传所有选项
  320. * @param {*} uploadOptionsInfo 上传的选项列表
  321. * @return {*}
  322. */
  323. const uploadOpiton = async (uploadOptionsInfo: { [key: string]: Array<UploadOption> }) => {
  324. const { allEventTable, allOptionsInfo } = await getAllTable() // 重新获取所有事件列表和选项列表
  325. let optionsReqList: Array<Promise<boolean>> = []
  326. if (Object.keys(uploadOptionsInfo).length) {
  327. // 开始上传选项
  328. allEventTable.map((item) => {
  329. // 在上传的事件列表中,找到有对应的事件的actionid的那一组数据
  330. let uploadOptionItem = uploadOptionsInfo[item.actionId]
  331. // 在现有的事件列表中,找到对应事件的actionid的那一组数据
  332. let nowOptionItem = allOptionsInfo[item.actionId] // 找到所有在已有事件列表中的选项列表
  333. // 如果有已存在的事件,并且上传的选项列表中也有对应的actionid,则开始上传
  334. if (uploadOptionItem) {
  335. // 对找到的那一组数据进行循环,区分出来哪些是已有的,哪些是新上传的
  336. // 新上传的需要给他加上actionid,然后上传,这个actionid其实是事件列表的id字段
  337. uploadOptionItem.map((i) => {
  338. const { id, actionId: originalActionId, ...otherInfo } = i // 上传参数拆分出来
  339. const isUpdate = nowOptionItem.some((k) => k.id === id) // 判断是否是更新选项
  340. // 设置请求 URL 和参数
  341. const optionReqUrl = isUpdate ? AllApi.updateGameActionOption : AllApi.addGameActionOption
  342. const reqParams = isUpdate
  343. ? { id, ...otherInfo } // 更新选项
  344. : { actionId: item.id, ...otherInfo } // 新增选项
  345. let eventReq = createUploadReqFunc(optionReqUrl, reqParams, item.actionName)
  346. optionsReqList.push(eventReq) // 统一放入请求列表中
  347. })
  348. } else {
  349. setTimeout(() => {
  350. ElNotification({
  351. type: 'warning',
  352. title: `没有对应事件`,
  353. message: `没有${item.actionName}事件,为事件添加的选项无效`,
  354. position: 'top-right',
  355. duration: 8000
  356. })
  357. }, 0)
  358. }
  359. })
  360. await submitUpload(optionsReqList, '选项') // 等待所有选项上传完成
  361. } else {
  362. ElNotification({
  363. type: 'warning',
  364. title: '没有选项被上传',
  365. message: `上传选项为空`,
  366. position: 'top-right',
  367. duration: 8000
  368. })
  369. }
  370. }
  371. // 上传选项的时候,选项的键要设置为actionid
  372. // 需要先上传事件,然后上传完了,用上传的选项中的第一个(如果有)的actionid去找到对应的事件的ID(不是actionid),然后作为选项上传的id
  373. /**
  374. * @description: 当文件添加后,开始进行上传前的处理
  375. * 首先需要上传事件,如果现有上传的列表中的actionid已经包含在了现有列表中,那么判定为更新,否则为新增
  376. * 上传完成后,需要首先获取一次新的数据,然后再开始上传选项。
  377. * 分别在上传和已有的选项列表中,找到对应的acionID的那一组选项,对这一组数据,如果已经存在对应的id,那么就是更新,否则为新增
  378. * 然后上传,上传完毕后,整体列表刷新,关闭弹框
  379. * @param {*} data 上传的文件数据,里面包含allEventTable:所有的事件列表,allOptionsInfo:所有选项列表
  380. * @return {*}
  381. */
  382. const uploadSuccess = async (data: {
  383. allEventTable: Array<uploadEvent>
  384. allOptionsInfo: { [key: string]: Array<UploadOption> }
  385. }) => {
  386. let uploadEventTable = data.allEventTable
  387. let uploadOptionsInfo = data.allOptionsInfo
  388. let allEventTable: Array<DownLoadEvent> = []
  389. // 上传的事件列表有值,则开始上传
  390. ;({ allEventTable } = await getAllTable('event')) // 获取所有事件列表和选项列表
  391. let eventUploadResult = false
  392. if (Array.isArray(uploadEventTable) && uploadEventTable.length > 0) {
  393. // 将新事件和旧事件区分,对于新事件走新增,对于旧事件走更新
  394. eventUploadResult = await uploadEvent(uploadEventTable, allEventTable)
  395. // 如果事件上传全部失败,那么就不要再上传选项了
  396. if (eventUploadResult) {
  397. uploadOpiton(uploadOptionsInfo)
  398. }
  399. } else {
  400. ElNotification({
  401. type: 'error',
  402. title: '上传参数有误',
  403. message: `上传参数为空,请检查参数`,
  404. position: 'top-left',
  405. duration: 5000
  406. })
  407. }
  408. uploadRef.value?.uploadCallback()
  409. eventTableRef.value?.updateData()
  410. }
  411. </script>
  412. <template>
  413. <div class="enventManage">
  414. <div class="breadcrumbBox">
  415. <HeaderCard
  416. ref="headerCard"
  417. :need-breadcrumb="true"
  418. :title="'事件管理'"
  419. :need-pf-select="false"
  420. ></HeaderCard>
  421. </div>
  422. <div class="handleEvent" v-if="nowRouteName === 'EventTable'">
  423. <div class="fileGroup">
  424. <el-button class="fileBtn" color="#626aef" @click="startUpload">上传</el-button>
  425. <el-button class="fileBtn" @click="throttleStartDownload">下载</el-button>
  426. </div>
  427. </div>
  428. <div class="eventTableBox">
  429. <!-- 监听表格的跳转事件 -->
  430. <router-view v-slot="{ Component, route }">
  431. <!-- 是eventtable组件就去监听enterdetail事件 -->
  432. <!-- 注释也不要写到keep-alive里面,会报错 -->
  433. <component
  434. :is="Component"
  435. ref="eventTableRef"
  436. v-if="shouldListenToEvent(route.name, 'EventTable')"
  437. @enterEventDetail="headerAddPath"
  438. />
  439. <!-- 如果不是正常渲染其他组件 -->
  440. <component v-if="route.name === 'EventDetail'" :is="Component" />
  441. <!-- <component v-else :is="Component" /> -->
  442. </router-view>
  443. </div>
  444. <div class="uploadFileBox">
  445. <FileUpload
  446. @upload-success="uploadSuccess"
  447. ref="uploadRef"
  448. :title="uploadInfo.dialogTitle"
  449. ></FileUpload>
  450. </div>
  451. </div>
  452. </template>
  453. <style scoped>
  454. .enventManage {
  455. width: 98%;
  456. margin: 1% auto;
  457. background-color: white;
  458. box-sizing: border-box;
  459. position: relative;
  460. }
  461. .breadcrumbBox {
  462. background-color: white;
  463. box-sizing: border-box;
  464. height: 64px;
  465. font-size: 16px;
  466. color: #17233d;
  467. font-weight: 600;
  468. /* padding: 0 24px; */
  469. line-height: 64px;
  470. }
  471. .eventTableBox {
  472. box-sizing: border-box;
  473. padding: 0px 24px;
  474. }
  475. .handleEvent {
  476. position: absolute;
  477. /* width: 12%; */
  478. background-color: white;
  479. box-sizing: border-box;
  480. /* height: 48px; */
  481. font-size: 16px;
  482. font-weight: 600;
  483. top: 20px;
  484. right: 24px;
  485. /* position: relative; */
  486. /* justify-content: flex-end; */
  487. }
  488. .fileGroup {
  489. width: 100%;
  490. }
  491. .fileBtn {
  492. margin-right: 10px;
  493. }
  494. </style>