IndexView.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698
  1. <!--
  2. * @Author: fxs bjnsfxs@163.com
  3. * @Date: 2024-08-20 14:06:49
  4. * @LastEditors: fxs bjnsfxs@163.com
  5. * @LastEditTime: 2025-06-03
  6. * @Description:
  7. *
  8. -->
  9. <script setup lang="ts">
  10. import { useRequest } from '@/hooks/useRequest.ts'
  11. import { useUser } from '@/stores/useUser.ts'
  12. import type { DropDownGroupInfo, PlatformInfo } from '@/types/dataAnalysis'
  13. import type { ResponseInfo } from '@/types/res.ts'
  14. import AxiosInstance from '@/utils/axios/axiosInstance.ts'
  15. import { zhCn } from 'element-plus/es/locales.mjs'
  16. import { type RouteRecordRaw, RouterView, useRoute } from 'vue-router'
  17. import { computed, onMounted, reactive, ref, watch } from 'vue'
  18. import { ElMessage } from 'element-plus'
  19. // import { getAllGameInfo } from '@/utils/table/table'
  20. import { initSelect, useCommonStore } from '@/stores/useCommon'
  21. import { initLoadResource } from '@/utils/resource'
  22. import { getUserInfo } from '@/utils/localStorage/localStorage'
  23. import router from '@/router'
  24. // interface GameSelectItemInfo {
  25. // id: number
  26. // pid: string
  27. // gid: string
  28. // gameName: string
  29. // }
  30. // interface GameItem {
  31. // id: number
  32. // pid: string
  33. // gid: string
  34. // gameName: string
  35. // pidName: string
  36. // }
  37. //
  38. // interface PlatformInfo {
  39. // pidName: string
  40. // pid: string
  41. // gidList: GameItem[]
  42. // }
  43. const route = useRoute()
  44. const { selectInfo, gameInfoList, saveSelectInfo, updateSelectInfo, updateGameInfoList } =
  45. useCommonStore()
  46. const { AllApi } = useRequest()
  47. const { updateUserInfo } = useUser()
  48. const userInfo = getUserInfo()
  49. const isCollapse = ref(false)
  50. const navBarSelect = ref<string>('Home')
  51. const loadingState = ref(false) // 用来标记必要信息的加载状态
  52. // 路由信息,同时也是侧边栏生成的依据信息
  53. const menuList = reactive<Array<any>>([])
  54. // 默认选中
  55. const defaultActive = computed<string>(() => {
  56. return route.meta.activeMenu as string
  57. })
  58. // 顶部导航栏信息
  59. const navBarMenuList = computed(() => {
  60. const allNavBar = [
  61. {
  62. name: 'Home',
  63. title: '应用分析',
  64. needSuper: false
  65. },
  66. {
  67. name: 'AppManage',
  68. title: '应用管理',
  69. needSuper: true
  70. },
  71. {
  72. name: 'FileManage',
  73. title: '文件管理',
  74. needSuper: true
  75. },
  76. {
  77. name: 'MemberManage',
  78. title: '成员管理',
  79. needSuper: true
  80. }
  81. ]
  82. return allNavBar.filter((item) => {
  83. if (item.needSuper) {
  84. return userInfo?.isSuper
  85. }
  86. return true
  87. })
  88. })
  89. /**
  90. * 侧边栏折叠改变
  91. */
  92. const changeCollapse = () => {
  93. isCollapse.value = !isCollapse.value
  94. }
  95. const backupOptionsList: Array<PlatformInfo> = []
  96. const allOptionsList: Array<PlatformInfo> = []
  97. // 游戏下拉选择框需要的数据
  98. const gameSelectInfo = reactive<DropDownGroupInfo>({
  99. defaultSelect: '1001',
  100. placeholder: '请选择游戏',
  101. // optionsList: []
  102. optionsList: []
  103. })
  104. /**
  105. * 更新整个页面的游戏选择
  106. * @param {*} gid 游戏id
  107. */
  108. const changeGame = (gid: string) => {
  109. selectInfo.gid = gid
  110. saveSelectInfo()
  111. }
  112. /**
  113. * 更新头部导航栏,跳转到对应页面
  114. * @param val 对应的name
  115. */
  116. const changeNavBar = (val: string) => {
  117. navBarSelect.value = val
  118. router.push(`/${val}`)
  119. // createdMenuList()
  120. }
  121. // 资源的加载路径
  122. const resourceInfo: Record<string, string> = {
  123. logo: `/img/logo.svg`,
  124. // logo: `/img/logoTest.svg`,
  125. defaultHead: `/img/default/defaultHead.png`
  126. }
  127. // 使用blob的资源路径信息
  128. const blobUrlInfo = reactive<Record<string, string>>({})
  129. // 侧边栏跳转路由的基本路由
  130. const basePath = ref<string | undefined>()
  131. // 是否过滤不活跃的游戏
  132. const isFilterNotActiveGame = ref<boolean>(true)
  133. // 游戏选择框的加载状态
  134. const gameSelectLoading = ref<boolean>(false)
  135. // 选中的游戏
  136. const selectedGame = ref<string>('')
  137. /**
  138. * 创建侧边栏菜单
  139. */
  140. const createdMenuList = (topRoute: RouteRecordRaw | null) => {
  141. if (!topRoute) return
  142. basePath.value = topRoute?.path // 找到需要激活的菜单的路由,后续用来拼接需要跳转的路由
  143. menuList.splice(0, menuList.length, ...(topRoute?.children as Array<any>)) // 清空原来的路由信息,并且加入新选中的
  144. }
  145. /**
  146. * 从URL中解析出路径片段
  147. * @param fullUrl 完整的 URL
  148. * @returns 路径片段组成的数组
  149. */
  150. const parsePathFromUrl = (fullUrl: string): string[] => {
  151. const hashIndex = fullUrl.indexOf('#')
  152. const hashPart = hashIndex !== -1 ? fullUrl.slice(hashIndex + 1) : fullUrl
  153. const pathPart = hashPart.split('?')[0] // 去除查询参数
  154. return pathPart.split('/').filter(Boolean) // ['home', 'infoManage', 'gameManageView']
  155. }
  156. /**
  157. * 查找子路由所属的顶级路由
  158. * @param routes 路由数组
  159. * @param fullUrl 目标子路由的 name
  160. * @returns 顶级路由的 name 或 null
  161. */
  162. const findTopRouteName = (routes: RouteRecordRaw[], fullUrl: string): RouteRecordRaw | null => {
  163. const pathSegments = parsePathFromUrl(fullUrl)
  164. if (pathSegments.length === 0) return null
  165. for (const route of routes) {
  166. const routeSegments = route.path.split('/').filter(Boolean)
  167. if (routeSegments[0] === pathSegments[0]) {
  168. return route || null
  169. }
  170. }
  171. return null
  172. }
  173. /**
  174. * 当路由地址改变的时候,去获取最新的导航栏位置,并且重新生成侧边栏,不然刷新后,侧边栏会无法选中
  175. */
  176. watch(
  177. () => [router.currentRoute.value.fullPath],
  178. ([newFullPath]) => {
  179. let routes = router.options.routes // 获取路由信息
  180. let indexRoutesChild = routes.find((item) => item.name === 'Index')?.children
  181. if (!indexRoutesChild) {
  182. ElMessage.error('路由错误')
  183. return
  184. }
  185. const topRoute = findTopRouteName(indexRoutesChild, newFullPath)
  186. navBarSelect.value = topRoute?.name?.toString() || ''
  187. createdMenuList(topRoute)
  188. },
  189. {
  190. immediate: true
  191. }
  192. )
  193. /**
  194. * 获取所有游戏列表
  195. */
  196. // const updateGameInfo = () => {
  197. // return new Promise((resolve, reject) => {
  198. // getAllGameInfo()
  199. // .then((data) => {
  200. // if (data) {
  201. // resolve(data)
  202. // } else {
  203. // throw new Error('游戏信息获取失败')
  204. // }
  205. // })
  206. // .catch((err) => {
  207. // console.log(err)
  208. // reject(err)
  209. // })
  210. // })
  211. // }
  212. /**
  213. * 设置导航栏的游戏选择框数据
  214. */
  215. const setNavbarGameSelect = (optionsList: Array<PlatformInfo>) => {
  216. // gameSelectInfo.optionsList.clear()
  217. gameSelectInfo.optionsList.splice(0, gameSelectInfo.optionsList.length)
  218. if (!optionsList || optionsList.length === 0) {
  219. selectedGame.value = ''
  220. gameSelectInfo.optionsList.length = 0
  221. return
  222. }
  223. gameSelectInfo.optionsList.splice(0, gameSelectInfo.optionsList.length, ...optionsList)
  224. backupOptionsList.splice(0, backupOptionsList.length, ...optionsList)
  225. const nowOptions = gameSelectInfo.optionsList.map((item) => [...item.gidList]).flat()
  226. if (nowOptions.findIndex((item) => item.gid === selectInfo.gid) === -1) {
  227. const targetOption = allOptionsList.find(
  228. (item) => item.gidList.findIndex((gidItem) => gidItem.gid === selectInfo.gid) !== -1
  229. )
  230. // targetOption = targetOption.
  231. if (targetOption) {
  232. const cloned = {
  233. ...targetOption,
  234. gidList: targetOption.gidList.filter((i) => i.gid === selectInfo.gid)
  235. }
  236. gameSelectInfo.optionsList.push(cloned)
  237. }
  238. }
  239. }
  240. /**
  241. *
  242. *
  243. * 获取导航栏游戏选择框的数据
  244. */
  245. const updateNavbarGameSelect = async (query: string, force = false) => {
  246. if (!query && !force) {
  247. return
  248. }
  249. try {
  250. gameSelectLoading.value = true
  251. // const res = (await AxiosInstance.post(AllApi.getGidList, {
  252. // active: isFilterNotActiveGame.value,
  253. // search: query
  254. // })) as ResponseInfo
  255. const res = (await AxiosInstance.post(AllApi.pidToGidList, {
  256. active: isFilterNotActiveGame.value,
  257. search: query
  258. })) as ResponseInfo
  259. const allOptions = (await AxiosInstance.post(AllApi.pidToGidList, {
  260. active: false,
  261. search: ''
  262. })) as ResponseInfo
  263. allOptionsList.splice(0, allOptionsList.length, ...allOptions.data)
  264. if (res.code !== 0) {
  265. setNavbarGameSelect([])
  266. return false
  267. }
  268. setNavbarGameSelect(res.data)
  269. } catch (err) {
  270. setNavbarGameSelect([])
  271. console.error(err)
  272. ElMessage.error('游戏列表获取失败')
  273. } finally {
  274. gameSelectLoading.value = false
  275. }
  276. }
  277. const filterSelect = (query: string) => {
  278. const next = backupOptionsList.filter(
  279. (item) => item.pidName.includes(query) || item.gidList.some((g) => g.gameName.includes(query))
  280. )
  281. gameSelectInfo.optionsList.splice(0, gameSelectInfo.optionsList.length, ...next)
  282. }
  283. /**
  284. * 监听游戏列表的变化
  285. *
  286. * 此处只是声明,在后续加载完成后,会被赋值唯一一个监听器
  287. *
  288. */
  289. let watchGameListChange: () => void = () => {}
  290. /**
  291. * @description: 监听加载状态的变化,加载完成的时候,给游戏列表的监听器赋值,然后把自己这个监听器摧毁
  292. * @return {*}
  293. */
  294. const watchLoadingState = watch(
  295. () => loadingState,
  296. (newVal) => {
  297. if (newVal.value) {
  298. watchGameListChange = watch(
  299. () => gameInfoList,
  300. () => {
  301. updateNavbarGameSelect('', true)
  302. },
  303. { deep: true }
  304. )
  305. watchLoadingState()
  306. } else {
  307. watchGameListChange()
  308. }
  309. },
  310. {
  311. deep: true
  312. }
  313. )
  314. onMounted(async () => {
  315. // // 去加载所有需要的资源
  316. // // await updateNavbarGameSelect('', true)
  317. // // await updateGameInfo()
  318. const { gid, pf, multiPf, gameInfoList } = await initSelect()
  319. updateSelectInfo(
  320. {
  321. pf,
  322. gid
  323. },
  324. {
  325. gid,
  326. pf: multiPf
  327. }
  328. )
  329. updateGameInfoList(gameInfoList)
  330. await updateNavbarGameSelect('', true)
  331. initLoadResource(resourceInfo).then((data) => {
  332. Object.assign(blobUrlInfo, data)
  333. })
  334. selectedGame.value = selectInfo.gid
  335. if (userInfo) {
  336. updateUserInfo({
  337. ...userInfo
  338. })
  339. }
  340. console.log('gaowan l')
  341. loadingState.value = true
  342. })
  343. </script>
  344. <template>
  345. <el-config-provider :locale="zhCn">
  346. <div class="body" v-if="loadingState">
  347. <div class="navBarBox">
  348. <div class="logoBox">
  349. <el-image :fit="'fill'" class="logoImg" :src="blobUrlInfo.logo"></el-image>
  350. <!-- <span class="logoTitle">测试库</span>-->
  351. <span class="logoTitle">淳皓科技</span>
  352. </div>
  353. <div class="gameSelect">
  354. <el-icon class="gameIcon" :size="20">
  355. <icon-icon-park-game-three></icon-icon-park-game-three>
  356. <!-- <icon-icon-park-solid-ad></icon-icon-park-solid-ad>-->
  357. </el-icon>
  358. <div style="width: 150px">
  359. <el-select
  360. style="width: 100%"
  361. @change="changeGame"
  362. v-model="selectedGame"
  363. :placeholder="gameSelectInfo.placeholder"
  364. filterable
  365. :filter-method="filterSelect"
  366. :loading="gameSelectLoading"
  367. v-bind="$attrs"
  368. >
  369. <template #header>
  370. <el-checkbox
  371. v-model="isFilterNotActiveGame"
  372. @change="updateNavbarGameSelect('', true)"
  373. >过滤不活跃游戏
  374. </el-checkbox>
  375. </template>
  376. <el-option-group
  377. :key="item.pid"
  378. :label="item.pidName"
  379. v-for="item in gameSelectInfo.optionsList"
  380. >
  381. <el-option
  382. v-for="option in item.gidList"
  383. :key="option.gid"
  384. :label="option.gameName"
  385. :value="option.gid"
  386. />
  387. </el-option-group>
  388. </el-select>
  389. </div>
  390. </div>
  391. <!-- 顶部导航栏 -->
  392. <div class="navBarMenu">
  393. <el-menu
  394. :default-active="navBarSelect"
  395. class="el-menu-demo"
  396. mode="horizontal"
  397. @select="changeNavBar"
  398. >
  399. <el-menu-item
  400. v-for="item in navBarMenuList"
  401. :key="item.name"
  402. class="navBarMenuItem"
  403. :index="item.name"
  404. >{{ item.title }}</el-menu-item
  405. >
  406. </el-menu>
  407. </div>
  408. <div class="headPortraitBox">
  409. <UserHeadIcon :head-icon="blobUrlInfo.defaultHead"></UserHeadIcon>
  410. <!-- <el-popover popper-class="headPopper" placement="bottom-end" trigger="click">-->
  411. <!-- <template #reference>-->
  412. <!-- <el-image class="headPortrait" :src="blobUrlInfo.defaultHead"></el-image>-->
  413. <!-- </template>-->
  414. <!-- <div class="userTools">-->
  415. <!-- <span class="userToolsItem" @click="logOut">-->
  416. <!-- <icon-material-symbols-light-logout></icon-material-symbols-light-logout>-->
  417. <!-- <span> 退出登录</span>-->
  418. <!-- </span>-->
  419. <!-- </div>-->
  420. <!-- </el-popover>-->
  421. </div>
  422. </div>
  423. <!-- 侧边栏 -->
  424. <div class="sideBarBox">
  425. <el-menu :default-active="defaultActive" class="sideBar" :collapse="isCollapse">
  426. <!-- ref="sideBar"-->
  427. <template v-for="(item, index) in menuList">
  428. <el-sub-menu
  429. :index="`${index}`"
  430. v-if="item.children && item.showChild"
  431. :key="item.name"
  432. >
  433. <template #title>
  434. <!-- <el-icon><component :is="item.icon"></component></el-icon>-->
  435. <DynamicIcon :icon="item.icon" />
  436. <span>{{ item.cnName }}</span>
  437. </template>
  438. <router-link
  439. style="text-decoration: none"
  440. v-for="val in item.children"
  441. :to="{ path: basePath + '/' + item.path + '/' + val.path }"
  442. :key="val"
  443. >
  444. <el-menu-item :index="val.path">{{ val.cnName }}</el-menu-item>
  445. </router-link>
  446. </el-sub-menu>
  447. <router-link
  448. style="text-decoration: none"
  449. v-else
  450. :to="{ path: basePath + '/' + item.path }"
  451. :key="index"
  452. >
  453. <el-menu-item :index="item.path">
  454. <!-- <el-icon><component :is="item.icon" /></el-icon>-->
  455. <DynamicIcon :icon="item.icon" />
  456. <template #title>
  457. <span class="menuTitle">{{ item.cnName }}</span>
  458. </template>
  459. </el-menu-item>
  460. </router-link>
  461. </template>
  462. <div class="sideBarFold" @click="changeCollapse">
  463. <el-icon :size="25"><Fold /></el-icon>
  464. </div>
  465. </el-menu>
  466. </div>
  467. <div class="content">
  468. <router-view v-slot="{ Component, route }">
  469. <keep-alive>
  470. <component
  471. :is="Component"
  472. :key="route.meta.activeMenu"
  473. v-if="route.meta.needKeepAlive == true"
  474. ></component>
  475. </keep-alive>
  476. <component
  477. :is="Component"
  478. :key="route.meta.activeMenu"
  479. v-if="route.meta.needKeepAlive == false"
  480. ></component>
  481. </router-view>
  482. </div>
  483. </div>
  484. </el-config-provider>
  485. </template>
  486. <style scoped>
  487. .body {
  488. width: 100%;
  489. display: flex;
  490. height: 100vh;
  491. }
  492. /* 设置宽度后,content无法适应宽度,只能去间接的调整内部元素的宽度 */
  493. .sideBarBox {
  494. position: relative;
  495. /* width: 12%; */
  496. z-index: 1;
  497. height: 93vh;
  498. margin-top: 7vh;
  499. top: 0;
  500. }
  501. .sideBar {
  502. /* width: 12vw; */
  503. height: 93vh;
  504. position: relative;
  505. overflow: scroll;
  506. }
  507. .logoImg {
  508. display: flex;
  509. align-items: center;
  510. width: 33px;
  511. margin-right: 10px;
  512. }
  513. .logoTitle {
  514. font-family: 'Helvetica Neue', 'Hiragino Sans GB', 'Segoe UI', 'Microsoft Yahei', '微软雅黑',
  515. Tahoma, Arial, STHeiti, sans-serif;
  516. font-size: 18px;
  517. font-weight: 600;
  518. }
  519. /* 主要用来调整整个menu的宽度 */
  520. .menuTitle {
  521. margin-right: 40px;
  522. }
  523. .sideBarFold {
  524. width: 5%;
  525. height: 3%;
  526. position: absolute;
  527. right: 40px;
  528. bottom: 20px;
  529. }
  530. .navBarBox {
  531. position: fixed;
  532. display: flex;
  533. align-items: center;
  534. width: 100vw;
  535. z-index: 2;
  536. height: 7vh;
  537. top: 0;
  538. background-color: white;
  539. right: 0;
  540. border-bottom: 1px solid gainsboro;
  541. }
  542. /* 调整LOGO */
  543. .logoBox {
  544. box-sizing: border-box;
  545. left: 30px;
  546. position: relative;
  547. display: flex;
  548. justify-content: space-between;
  549. align-items: center;
  550. }
  551. .gameSelect {
  552. position: relative;
  553. height: 80%;
  554. display: flex;
  555. align-items: center;
  556. left: 5%;
  557. }
  558. .gameIcon {
  559. /* box-sizing: border-box; */
  560. /* padding-right: 12px; */
  561. margin-right: 12px;
  562. }
  563. .navBarMenu {
  564. width: 60%;
  565. position: relative;
  566. left: 6%;
  567. }
  568. .headPortraitBox {
  569. position: absolute;
  570. right: 3%;
  571. top: 50%;
  572. transform: translateY(-50%);
  573. }
  574. .userTools {
  575. width: 100%;
  576. height: 100%;
  577. display: flex;
  578. flex-direction: column;
  579. justify-content: space-around;
  580. align-items: center;
  581. }
  582. .userToolsItem {
  583. cursor: pointer;
  584. width: 100%;
  585. height: 4vh;
  586. display: flex;
  587. align-items: center;
  588. justify-content: center;
  589. /* padding: 10px; */
  590. margin: 2%;
  591. }
  592. .userToolsItem > span {
  593. margin-left: 10%;
  594. }
  595. .userToolsItem:hover {
  596. background-color: #f2f3f5;
  597. }
  598. .headPortrait {
  599. cursor: pointer;
  600. width: 30px;
  601. }
  602. .content {
  603. /* flex-grow: 1; */
  604. /* position: absolute; */
  605. width: 100%;
  606. /* height: 93%; */
  607. margin-top: 7vh;
  608. overflow: scroll;
  609. background-color: #f2f3f5;
  610. right: 0;
  611. top: 0;
  612. }
  613. </style>
  614. <!-- 为了让popper-class生效,需要的单独写一份 -->
  615. <style>
  616. .headPopper {
  617. padding: 0 !important;
  618. border: 1px solid #e5e6eb;
  619. background-color: white;
  620. }
  621. .el-menu--horizontal.el-menu {
  622. border-bottom: none;
  623. }
  624. </style>