IndexView.vue 19 KB

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