IndexView.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549
  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 type { DropDownInfo } from '@/types/dataAnalysis'
  11. import { zhCn } from 'element-plus/es/locales.mjs'
  12. import { type RouteRecordRaw, RouterView, useRoute } from 'vue-router'
  13. import { computed, onMounted, reactive, ref, watch } from 'vue'
  14. import { ElMessage } from 'element-plus'
  15. import { getAllGameInfo } from '@/utils/table/table'
  16. import { useCommonStore } from '@/stores/useCommon'
  17. import { initLoadResource } from '@/utils/resource'
  18. import { setLoginState } from '@/utils/localStorage/localStorage'
  19. import { removeAllToken } from '@/utils/token/token'
  20. import router from '@/router'
  21. import DropDownSelection from '@/components/dataAnalysis/DropDownSelection.vue'
  22. const route = useRoute()
  23. const { selectInfo, allGameInfo, saveSelectInfo } = useCommonStore()
  24. const isCollapse = ref(false)
  25. const navBarSelect = ref<string>('Home')
  26. const loadingState = ref(false) // 用来标记必要信息的加载状态
  27. // 路由信息,同时也是侧边栏生成的依据信息
  28. const menuList = reactive<Array<any>>([])
  29. // 默认选中
  30. const defaultActive = computed<string>(() => {
  31. return route.meta.activeMenu as string
  32. })
  33. // 顶部导航栏信息
  34. const navBarMenuList = [
  35. {
  36. name: 'Home',
  37. title: '应用分析'
  38. },
  39. {
  40. name: 'AppManage',
  41. title: '应用管理'
  42. },
  43. {
  44. name: 'FileManage',
  45. title: '文件管理'
  46. },
  47. {
  48. name: 'MemberManage',
  49. title: '成员管理'
  50. }
  51. ]
  52. /**
  53. * 侧边栏折叠改变
  54. */
  55. const changeCollapse = () => {
  56. isCollapse.value = !isCollapse.value
  57. }
  58. /**
  59. * 退出登录
  60. *
  61. * 清除登录信息,跳转到登录页
  62. */
  63. const logOut = () => {
  64. ElMessage({
  65. type: 'success',
  66. message: '退出成功',
  67. duration: 1000
  68. })
  69. setLoginState(false)
  70. removeAllToken()
  71. router.push('/login')
  72. }
  73. // 游戏下拉选择框需要的数据
  74. const gameSelectInfo = reactive<DropDownInfo>({
  75. defaultSelect: '1001',
  76. title: '请选择游戏',
  77. optionsList: []
  78. })
  79. /**
  80. * 更新整个页面的游戏选择
  81. * @param {*} gid 游戏id
  82. */
  83. const changeGame = (gid: string) => {
  84. selectInfo.gid = gid
  85. saveSelectInfo()
  86. }
  87. /**
  88. * 更新头部导航栏,跳转到对应页面
  89. * @param val 对应的name
  90. */
  91. const changeNavBar = (val: string) => {
  92. navBarSelect.value = val
  93. router.push(`/${val}`)
  94. // createdMenuList()
  95. }
  96. // 资源的加载路径
  97. const resourceInfo: Record<string, string> = {
  98. logo: `/img/logo.svg`,
  99. // logo: `/img/logoTest.svg`,
  100. defaultHead: `/img/default/defaultHead.png`
  101. }
  102. // 使用blob的资源路径信息
  103. const blobUrlInfo = reactive<Record<string, string>>({})
  104. // 侧边栏跳转路由的基本路由
  105. const basePath = ref<string | undefined>()
  106. /**
  107. * 创建侧边栏菜单
  108. */
  109. const createdMenuList = (topRoute: RouteRecordRaw | null) => {
  110. if (!topRoute) return
  111. basePath.value = topRoute?.path // 找到需要激活的菜单的路由,后续用来拼接需要跳转的路由
  112. menuList.splice(0, menuList.length, ...(topRoute?.children as Array<any>)) // 清空原来的路由信息,并且加入新选中的
  113. }
  114. /**
  115. * 从URL中解析出路径片段
  116. * @param fullUrl 完整的 URL
  117. * @returns 路径片段组成的数组
  118. */
  119. const parsePathFromUrl = (fullUrl: string): string[] => {
  120. const hashIndex = fullUrl.indexOf('#')
  121. const hashPart = hashIndex !== -1 ? fullUrl.slice(hashIndex + 1) : fullUrl
  122. const pathPart = hashPart.split('?')[0] // 去除查询参数
  123. return pathPart.split('/').filter(Boolean) // ['home', 'infoManage', 'gameManageView']
  124. }
  125. /**
  126. * 查找子路由所属的顶级路由
  127. * @param routes 路由数组
  128. * @param fullUrl 目标子路由的 name
  129. * @returns 顶级路由的 name 或 null
  130. */
  131. const findTopRouteName = (routes: RouteRecordRaw[], fullUrl: string): RouteRecordRaw | null => {
  132. const pathSegments = parsePathFromUrl(fullUrl)
  133. if (pathSegments.length === 0) return null
  134. for (const route of routes) {
  135. const routeSegments = route.path.split('/').filter(Boolean)
  136. if (routeSegments[0] === pathSegments[0]) {
  137. return route || null
  138. }
  139. }
  140. return null
  141. }
  142. /**
  143. * 当路由地址改变的时候,去获取最新的导航栏位置,并且重新生成侧边栏,不然刷新后,侧边栏会无法选中
  144. */
  145. watch(
  146. () => [router.currentRoute.value.fullPath],
  147. ([newFullPath]) => {
  148. let routes = router.options.routes // 获取路由信息
  149. let indexRoutesChild = routes.find((item) => item.name === 'Index')?.children
  150. if (!indexRoutesChild) {
  151. ElMessage.error('路由错误')
  152. return
  153. }
  154. const topRoute = findTopRouteName(indexRoutesChild, newFullPath)
  155. navBarSelect.value = topRoute?.name?.toString() || ''
  156. createdMenuList(topRoute)
  157. },
  158. {
  159. immediate: true
  160. }
  161. )
  162. /**
  163. * 获取所有游戏列表
  164. */
  165. const getGameInfo = () => {
  166. getAllGameInfo()
  167. .then((data) => {
  168. if (data) {
  169. allGameInfo.splice(0, allGameInfo.length)
  170. gameSelectInfo.optionsList.splice(0, gameSelectInfo.optionsList.length)
  171. data.map((item) => {
  172. allGameInfo.push({
  173. gid: item.gid,
  174. gameName: item.gameName
  175. })
  176. gameSelectInfo.optionsList.push({
  177. value: item.gid,
  178. label: item.gameName
  179. })
  180. })
  181. gameSelectInfo.defaultSelect = data[0].gid
  182. // 去找本地的gid,如果有,就赋值,否则用请求回来的第一个gid
  183. changeGame(selectInfo.gid)
  184. gameSelectInfo.defaultSelect = selectInfo.gid
  185. loadingState.value = true
  186. } else {
  187. throw new Error('游戏信息获取失败')
  188. }
  189. })
  190. .catch((err) => {
  191. console.log(err)
  192. })
  193. }
  194. /**
  195. * 监听游戏列表的变化
  196. *
  197. * 此处只是声明,在后续加载完成后,会被赋值唯一一个监听器
  198. *
  199. */
  200. let watchGameListChange: () => void = () => {}
  201. /**
  202. * @description: 监听加载状态的变化,加载完成的时候,给游戏列表的监听器赋值,然后把自己这个监听器摧毁
  203. * @return {*}
  204. */
  205. const watchLoadingState = watch(
  206. () => loadingState,
  207. (newVal) => {
  208. if (newVal) {
  209. // 用来监听游戏列表的变化
  210. watchGameListChange = watch(
  211. () => allGameInfo,
  212. (newGameInfo: Array<any>) => {
  213. gameSelectInfo.optionsList.splice(0, gameSelectInfo.optionsList.length)
  214. newGameInfo.forEach((item) => {
  215. gameSelectInfo.optionsList.push({
  216. value: item.gid,
  217. label: item.gameName
  218. })
  219. })
  220. },
  221. { deep: true }
  222. )
  223. watchLoadingState()
  224. } else {
  225. watchGameListChange()
  226. }
  227. },
  228. {
  229. deep: true
  230. }
  231. )
  232. getGameInfo()
  233. onMounted(() => {
  234. // 去加载所有需要的资源
  235. initLoadResource(resourceInfo).then((data) => {
  236. Object.assign(blobUrlInfo, data)
  237. })
  238. })
  239. </script>
  240. <template>
  241. <el-config-provider :locale="zhCn">
  242. <div class="body" v-if="loadingState">
  243. <div class="navBarBox">
  244. <div class="logoBox">
  245. <el-image :fit="'fill'" class="logoImg" :src="blobUrlInfo.logo"></el-image>
  246. <!-- <span class="logoTitle">测试库</span>-->
  247. <span class="logoTitle">淳皓科技</span>
  248. </div>
  249. <div class="gameSelect">
  250. <el-icon class="gameIcon" :size="20">
  251. <icon-icon-park-game-three></icon-icon-park-game-three>
  252. <!-- <icon-icon-park-solid-ad></icon-icon-park-solid-ad>-->
  253. </el-icon>
  254. <DropDownSelection
  255. :default-select="gameSelectInfo.defaultSelect"
  256. :title="gameSelectInfo.title"
  257. :options-list="gameSelectInfo.optionsList"
  258. :size="'default'"
  259. @change-select="changeGame"
  260. ></DropDownSelection>
  261. </div>
  262. <!-- 顶部导航栏 -->
  263. <div class="navBarMenu">
  264. <el-menu
  265. :default-active="navBarSelect"
  266. class="el-menu-demo"
  267. mode="horizontal"
  268. @select="changeNavBar"
  269. >
  270. <el-menu-item
  271. v-for="item in navBarMenuList"
  272. :key="item.name"
  273. class="navBarMenuItem"
  274. :index="item.name"
  275. >{{ item.title }}</el-menu-item
  276. >
  277. </el-menu>
  278. </div>
  279. <div class="headPortraitBox">
  280. <el-popover popper-class="headPopper" placement="bottom-end" trigger="click">
  281. <template #reference>
  282. <el-image class="headPortrait" :src="blobUrlInfo.defaultHead"></el-image>
  283. </template>
  284. <div class="userTools">
  285. <span class="userToolsItem" @click="logOut">
  286. <icon-material-symbols-light-logout></icon-material-symbols-light-logout>
  287. <span> 退出登录</span>
  288. </span>
  289. </div>
  290. </el-popover>
  291. </div>
  292. </div>
  293. <!-- 侧边栏 -->
  294. <div class="sideBarBox">
  295. <el-menu :default-active="defaultActive" class="sideBar" :collapse="isCollapse">
  296. <!-- ref="sideBar"-->
  297. <template v-for="(item, index) in menuList">
  298. <el-sub-menu
  299. :index="`${index}`"
  300. v-if="item.children && item.showChild"
  301. :key="item.name"
  302. >
  303. <template #title>
  304. <el-icon><component :is="item.icon"></component></el-icon>
  305. <span>{{ item.cnName }}</span>
  306. </template>
  307. <router-link
  308. style="text-decoration: none"
  309. v-for="val in item.children"
  310. :to="{ path: basePath + '/' + item.path + '/' + val.path }"
  311. :key="val"
  312. >
  313. <el-menu-item :index="val.path">{{ val.cnName }}</el-menu-item>
  314. </router-link>
  315. </el-sub-menu>
  316. <router-link
  317. style="text-decoration: none"
  318. v-else
  319. :to="{ path: basePath + '/' + item.path }"
  320. :key="index"
  321. >
  322. <el-menu-item :index="item.path">
  323. <el-icon><component :is="item.icon" /></el-icon>
  324. <template #title>
  325. <span class="menuTitle">{{ item.cnName }}</span>
  326. </template>
  327. </el-menu-item>
  328. </router-link>
  329. </template>
  330. <div class="sideBarFold" @click="changeCollapse">
  331. <el-icon :size="25"><Fold /></el-icon>
  332. </div>
  333. </el-menu>
  334. </div>
  335. <div class="content">
  336. <router-view v-slot="{ Component, route }">
  337. <keep-alive>
  338. <component
  339. :is="Component"
  340. :key="route.meta.activeMenu"
  341. v-if="route.meta.needKeepAlive == true"
  342. ></component>
  343. </keep-alive>
  344. <component
  345. :is="Component"
  346. :key="route.meta.activeMenu"
  347. v-if="route.meta.needKeepAlive == false"
  348. ></component>
  349. </router-view>
  350. </div>
  351. </div>
  352. </el-config-provider>
  353. </template>
  354. <style scoped>
  355. .body {
  356. width: 100%;
  357. display: flex;
  358. height: 100vh;
  359. }
  360. /* 设置宽度后,content无法适应宽度,只能去间接的调整内部元素的宽度 */
  361. .sideBarBox {
  362. position: relative;
  363. /* width: 12%; */
  364. z-index: 1;
  365. height: 93vh;
  366. margin-top: 7vh;
  367. top: 0;
  368. }
  369. .sideBar {
  370. /* width: 12vw; */
  371. height: 93vh;
  372. position: relative;
  373. overflow: scroll;
  374. }
  375. .logoImg {
  376. display: flex;
  377. align-items: center;
  378. width: 33px;
  379. margin-right: 10px;
  380. }
  381. .logoTitle {
  382. font-family: 'Helvetica Neue', 'Hiragino Sans GB', 'Segoe UI', 'Microsoft Yahei', '微软雅黑',
  383. Tahoma, Arial, STHeiti, sans-serif;
  384. font-size: 18px;
  385. font-weight: 600;
  386. }
  387. /* 主要用来调整整个menu的宽度 */
  388. .menuTitle {
  389. margin-right: 40px;
  390. }
  391. .sideBarFold {
  392. width: 5%;
  393. height: 3%;
  394. position: absolute;
  395. right: 40px;
  396. bottom: 20px;
  397. }
  398. .navBarBox {
  399. position: fixed;
  400. display: flex;
  401. align-items: center;
  402. width: 100vw;
  403. z-index: 2;
  404. height: 7vh;
  405. top: 0;
  406. background-color: white;
  407. right: 0;
  408. border-bottom: 1px solid gainsboro;
  409. }
  410. /* 调整LOGO */
  411. .logoBox {
  412. box-sizing: border-box;
  413. left: 30px;
  414. position: relative;
  415. display: flex;
  416. justify-content: space-between;
  417. align-items: center;
  418. }
  419. .gameSelect {
  420. position: relative;
  421. height: 80%;
  422. display: flex;
  423. align-items: center;
  424. left: 5%;
  425. }
  426. .gameIcon {
  427. /* box-sizing: border-box; */
  428. /* padding-right: 12px; */
  429. margin-right: 12px;
  430. }
  431. .navBarMenu {
  432. width: 60%;
  433. position: relative;
  434. left: 6%;
  435. }
  436. .headPortraitBox {
  437. position: absolute;
  438. right: 3%;
  439. top: 50%;
  440. transform: translateY(-50%);
  441. }
  442. .userTools {
  443. width: 100%;
  444. height: 100%;
  445. display: flex;
  446. flex-direction: column;
  447. justify-content: space-around;
  448. align-items: center;
  449. }
  450. .userToolsItem {
  451. cursor: pointer;
  452. width: 100%;
  453. height: 4vh;
  454. display: flex;
  455. align-items: center;
  456. justify-content: center;
  457. /* padding: 10px; */
  458. margin: 2%;
  459. }
  460. .userToolsItem > span {
  461. margin-left: 10%;
  462. }
  463. .userToolsItem:hover {
  464. background-color: #f2f3f5;
  465. }
  466. .headPortrait {
  467. cursor: pointer;
  468. width: 30px;
  469. }
  470. .content {
  471. /* flex-grow: 1; */
  472. /* position: absolute; */
  473. width: 100%;
  474. /* height: 93%; */
  475. margin-top: 7vh;
  476. overflow: scroll;
  477. background-color: #f2f3f5;
  478. right: 0;
  479. top: 0;
  480. }
  481. </style>
  482. <!-- 为了让popper-class生效,需要的单独写一份 -->
  483. <style>
  484. .headPopper {
  485. padding: 0 !important;
  486. border: 1px solid #e5e6eb;
  487. background-color: white;
  488. }
  489. .el-menu--horizontal.el-menu {
  490. border-bottom: none;
  491. }
  492. </style>