Ver código fonte

feat(主页): 主页头像部分新增用户信息、优化图标显示

1.更新DynamicIcon组件,支持更多类型文件
2.主页右上角部分新增用户信息,可下拉查看
3.区分超级管理员与普通用户的导航栏展示
4.更新了游戏选择栏的加载逻辑
5.新增UserHeadIcon组件,用户展示用户信息
fxs 1 dia atrás
pai
commit
25765b807e

+ 5 - 0
components.d.ts

@@ -12,6 +12,7 @@ declare module 'vue' {
     CustomDialog: typeof import('./src/components/common/CustomDialog.vue')['default']
     CustomFilter: typeof import('./src/components/form/CustomFilter.vue')['default']
     CustomForm: typeof import('./src/components/form/CustomForm.vue')['default']
+    CustomIcon: typeof import('./src/components/common/CustomIcon.vue')['default']
     CustomTable: typeof import('./src/components/table/CustomTable.vue')['default']
     DownloadTipModal: typeof import('./src/components/common/DownloadTipModal.vue')['default']
     DropDownSelection: typeof import('./src/components/dataAnalysis/DropDownSelection.vue')['default']
@@ -20,6 +21,7 @@ declare module 'vue' {
     ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
     ElButton: typeof import('element-plus/es')['ElButton']
     ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
+    ElCol: typeof import('element-plus/es')['ElCol']
     ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
     ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
     ElDialog: typeof import('element-plus/es')['ElDialog']
@@ -33,6 +35,7 @@ declare module 'vue' {
     ElIcon: typeof import('element-plus/es')['ElIcon']
     ElImage: typeof import('element-plus/es')['ElImage']
     ElInput: typeof import('element-plus/es')['ElInput']
+    ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
     ElLink: typeof import('element-plus/es')['ElLink']
     ElMenu: typeof import('element-plus/es')['ElMenu']
     ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
@@ -43,6 +46,7 @@ declare module 'vue' {
     ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
     ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
     ElResult: typeof import('element-plus/es')['ElResult']
+    ElRow: typeof import('element-plus/es')['ElRow']
     ElSelect: typeof import('element-plus/es')['ElSelect']
     ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
     ElSwitch: typeof import('element-plus/es')['ElSwitch']
@@ -80,6 +84,7 @@ declare module 'vue' {
     TableTools: typeof import('./src/components/table/TableTools.vue')['default']
     TemporalTrend: typeof import('./src/components/dataAnalysis/TemporalTrend.vue')['default']
     TimeLineChart: typeof import('./src/components/echarts/TimeLineChart.vue')['default']
+    UserHeadIcon: typeof import('./src/components/common/UserHeadIcon.vue')['default']
     WithIconSelect: typeof import('./src/components/common/WithIconSelect.vue')['default']
   }
   export interface ComponentCustomProperties {

+ 15 - 1
src/components/common/DynamicIcon.vue

@@ -29,7 +29,21 @@ const props = defineProps<Props>()
 
 // 判断是否为 Element Plus 图标(首字母大写,且不包含斜杠或 .svg)
 const isElementPlusIcon = computed(() => {
-  return /^[A-Z]/.test(props.icon) && !props.icon.includes('/') && !props.icon.endsWith('.svg')
+  const iconStr = props.icon
+
+  // 判断是否为图片路径的常见特征:
+  // 1. 包含常见图片扩展名 (.png, .jpg, .svg 等)
+  // 2. 是 base64 图片数据
+  // 3. 是网络图片 URL
+  // 4. 包含路径分隔符(/ 或 \)
+  const isImage =
+    /\.(png|jpe?g|gif|svg|webp|bmp|ico)$/i.test(iconStr) ||
+    iconStr.startsWith('data:image/') ||
+    /^https?:\/\//i.test(iconStr) ||
+    /[\\/]/.test(iconStr)
+
+  // 如果满足图片特征,就不是 Element Plus 图标
+  return !isImage
 })
 
 // 动态映射 Element Plus 图标

+ 292 - 0
src/components/common/UserHeadIcon.vue

@@ -0,0 +1,292 @@
+<template>
+  <div class="headPortraitBox">
+    <!-- 消息图标 -->
+    <el-icon class="messageBtn" :size="20">
+      <Bell />
+    </el-icon>
+
+    <!-- 垂直分割线 -->
+    <el-divider style="height: 25px; margin: 0 15px" direction="vertical" />
+
+    <!-- 用户信息下拉 -->
+    <el-popover
+      popper-class="headPopper"
+      placement="bottom-end"
+      trigger="hover"
+      :width="300"
+      :offset="10"
+    >
+      <template #reference>
+        <div class="headPopperRef">
+          <!-- 用户头像 -->
+          <DynamicIcon class="popperHeadIcon" :icon="headIcon" :size="24" />
+
+          <!-- 用户名 -->
+          <span class="popperUserName">{{ userInfo.userName }}</span>
+
+          <!-- 下拉箭头 -->
+          <el-icon class="popperArrowIcon">
+            <ArrowDown />
+          </el-icon>
+        </div>
+      </template>
+
+      <!-- 弹出内容 -->
+      <div class="userInfoContainer">
+        <!-- 用户基础信息 -->
+        <div class="userInfoContent">
+          <div class="userInfo">
+            <!-- 大头像 -->
+            <div class="headPortrait">
+              <DynamicIcon :icon="headIcon" :size="48" />
+            </div>
+
+            <!-- 文字信息 -->
+            <div class="info">
+              <span class="userName infoItem">{{ userInfo.userName }}</span>
+              <span class="userPost infoItem">{{
+                userInfo.isSuper ? '超级管理员' : '普通用户'
+              }}</span>
+            </div>
+          </div>
+        </div>
+
+        <!-- 工具菜单 -->
+        <div class="toolsContainer">
+          <!-- 工具项 -->
+          <div v-for="item in linkList" :key="item.label" class="toolItem">
+            <RouterLink class="userInfoToolLink userInfoText" :to="item.url">
+              {{ item.label }}
+            </RouterLink>
+          </div>
+
+          <!-- 功能按钮组 -->
+          <div class="userInfoText toolBtnGroup">
+            <!-- 管理中心 -->
+            <div class="leftTool toolBtnItem" v-if="userInfo.isSuper">
+              <el-text class="userInfoText" @click="goToManageCenter">
+                <el-icon><Setting /></el-icon>
+                管理中心
+              </el-text>
+            </div>
+
+            <!-- 分割线 -->
+            <el-divider direction="vertical" v-if="userInfo.isSuper" />
+
+            <!-- 退出登录 -->
+            <div class="rightTool toolBtnItem">
+              <el-text class="userInfoText" @click="logOut">
+                <el-icon><SwitchButton /></el-icon>
+                退出登录
+              </el-text>
+            </div>
+          </div>
+        </div>
+      </div>
+    </el-popover>
+  </div>
+</template>
+
+<script setup lang="ts">
+import router from '@/router'
+import { useUser } from '@/stores/useUser.ts'
+import { clearClientInfo } from '@/utils/auth/auth.ts'
+import { defineProps } from 'vue'
+import { RouterLink } from 'vue-router'
+import { Bell, ArrowDown, Setting, SwitchButton } from '@element-plus/icons-vue'
+
+interface UserInfo {
+  headIcon: string
+}
+
+interface LinkInfo {
+  label: string
+  url: string
+}
+
+const { getUserInfo } = useUser()
+const userInfo = getUserInfo()
+
+withDefaults(defineProps<UserInfo>(), {
+  headIcon: ''
+})
+
+const linkList: LinkInfo[] = [
+  // {
+  //   label: '查看角色权限',
+  //   url: '/manageCenter/managerList'
+  // },
+  // {
+  //   label: '修改密码',
+  //   url: '/'
+  // }
+]
+
+/**
+ * 退出登录
+ *
+ * 清除登录信息,跳转到登录页
+ */
+const logOut = () => {
+  ElMessage({
+    type: 'success',
+    message: '退出成功',
+    duration: 1000
+  })
+  clearClientInfo()
+  router.push('/login')
+}
+
+// 进入管理中心
+const goToManageCenter = () => {
+  router.push('/memberManage/memberTable')
+}
+</script>
+
+<style scoped>
+.headPortraitBox {
+  position: absolute;
+  right: 3%;
+  top: 50%;
+  transform: translateY(-50%);
+  display: inline-flex;
+  align-items: center;
+}
+
+.headPopperRef {
+  display: flex;
+  align-items: center;
+  padding: 10px 5px;
+}
+
+.popperHeadIcon {
+  margin-right: 5px;
+}
+
+.popperArrowIcon {
+  /* 200ms的延迟是为了跟弹出框的动画延迟同步 */
+  transition: transform 0.2s linear 200ms;
+  transform: rotate(0deg);
+}
+
+.popperUserName {
+  font-size: 12px;
+  font-weight: 400;
+  color: #333;
+  margin-right: 15px;
+  user-select: none;
+}
+
+.headPopperRef:hover > .popperArrowIcon {
+  transition: transform 0.2s linear 0s;
+  transform: rotate(180deg);
+}
+
+.userInfoContainer {
+  width: 300px;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+  border: 1px solid #ebeef5;
+}
+
+.userInfoContent {
+  width: 100%;
+  position: relative;
+}
+
+.userInfo {
+  position: relative;
+
+  display: flex;
+  padding: 10px 20px;
+  /* justify-content: space-around; */
+}
+
+.headPortrait {
+  cursor: pointer;
+  margin-right: 10px;
+}
+
+.infoItem {
+  font-size: 12px;
+  font-weight: 500;
+  color: #999;
+  margin-bottom: 3px;
+}
+
+.toolsContainer {
+  display: flex;
+  flex-direction: column;
+}
+
+.toolItem {
+  height: 36px;
+  padding: 5px 24px;
+  line-height: normal;
+  color: #333;
+  font-size: 14px;
+  display: flex;
+  align-items: center;
+  border-top: 1px solid #ebeef5;
+  cursor: pointer;
+}
+
+.toolItem:hover {
+  background-color: #ecf5ff;
+}
+
+.toolItem:hover > .userInfoText {
+  color: #197afb !important;
+}
+.userName {
+  font-weight: 700;
+  font-size: 14px;
+  color: black;
+}
+
+.info {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  justify-content: center;
+}
+
+.userTools {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-around;
+  align-items: center;
+}
+
+.userInfoText {
+  color: #333;
+  font-size: 14px;
+}
+
+.userInfoToolLink {
+  text-decoration: none;
+  display: inline-block;
+}
+
+.toolBtnGroup {
+  height: 40px;
+  padding: 0px 24px;
+  line-height: normal;
+  color: #333;
+  font-size: 14px;
+
+  display: flex;
+  align-items: center;
+  justify-content: space-around;
+  border-top: 1px solid #ebeef5;
+}
+
+.toolBtnItem {
+  cursor: pointer;
+}
+
+.toolBtnItem:hover > .userInfoText {
+  color: #197afb;
+}
+</style>

+ 9 - 0
src/utils/resource/index.ts

@@ -70,3 +70,12 @@ export const initLoadResource = async (
     return resultObj // 返回所有成功的资源
   })
 }
+
+/**
+ * @description: 生成一个打包后可以使用的url
+ * @param {string} url  传入一个assets文件夹下的文件名
+ * @return {*}
+ */
+export const getAssetsImageUrl = (url: string) => {
+  return new URL(`../../assets/${url}`, import.meta.url).href
+}

+ 82 - 49
src/views/IndexView.vue

@@ -8,8 +8,10 @@
 -->
 <script setup lang="ts">
 import { useRequest } from '@/hooks/useRequest.ts'
+import { useUser } from '@/stores/useUser.ts'
 import type { DropDownInfo } from '@/types/dataAnalysis'
 import type { ResponseInfo } from '@/types/res.ts'
+import { clearClientInfo } from '@/utils/auth/auth.ts'
 import AxiosInstance from '@/utils/axios/axiosInstance.ts'
 
 import { zhCn } from 'element-plus/es/locales.mjs'
@@ -20,8 +22,7 @@ import { getAllGameInfo } from '@/utils/table/table'
 
 import { useCommonStore } from '@/stores/useCommon'
 import { initLoadResource } from '@/utils/resource'
-import { setLoginState } from '@/utils/localStorage/localStorage'
-import { removeAllToken } from '@/utils/token/token'
+import { getLoginState, getUserInfo } from '@/utils/localStorage/localStorage'
 
 import router from '@/router'
 
@@ -35,6 +36,8 @@ interface GameSelectItemInfo {
 const route = useRoute()
 const { selectInfo, allGameInfo, saveSelectInfo } = useCommonStore()
 const { AllApi } = useRequest()
+const { updateUserInfo } = useUser()
+const userInfo = getUserInfo()
 
 const isCollapse = ref(false)
 const navBarSelect = ref<string>('Home')
@@ -49,25 +52,37 @@ const defaultActive = computed<string>(() => {
 })
 
 // 顶部导航栏信息
-const navBarMenuList = [
-  {
-    name: 'Home',
-    title: '应用分析'
-  },
-
-  {
-    name: 'AppManage',
-    title: '应用管理'
-  },
-  {
-    name: 'FileManage',
-    title: '文件管理'
-  },
-  {
-    name: 'MemberManage',
-    title: '成员管理'
-  }
-]
+const navBarMenuList = computed(() => {
+  const allNavBar = [
+    {
+      name: 'Home',
+      title: '应用分析',
+      needSuper: false
+    },
+
+    {
+      name: 'AppManage',
+      title: '应用管理',
+      needSuper: true
+    },
+    {
+      name: 'FileManage',
+      title: '文件管理',
+      needSuper: true
+    },
+    {
+      name: 'MemberManage',
+      title: '成员管理',
+      needSuper: true
+    }
+  ]
+  return allNavBar.filter((item) => {
+    if (item.needSuper) {
+      return userInfo?.isSuper
+    }
+    return true
+  })
+})
 
 /**
  * 侧边栏折叠改变
@@ -87,8 +102,7 @@ const logOut = () => {
     message: '退出成功',
     duration: 1000
   })
-  setLoginState(false)
-  removeAllToken()
+  clearClientInfo()
   router.push('/login')
 }
 
@@ -135,12 +149,12 @@ const basePath = ref<string | undefined>()
 // 是否过滤不活跃的游戏
 const isFilterNotActiveGame = ref<boolean>(false)
 
-// 游戏选择框的选中值
-const gameSelectVal = ref<string>('')
-
 // 游戏选择框的加载状态
 const gameSelectLoading = ref<boolean>(false)
 
+// 选中的游戏
+const selectedGame = ref<string>('')
+
 /**
  * 创建侧边栏菜单
  */
@@ -233,30 +247,41 @@ const updateGameInfo = () => {
  */
 const setNavbarGameSelect = (optionsList: Array<GameSelectItemInfo>) => {
   gameSelectInfo.optionsList.splice(0, gameSelectInfo.optionsList.length)
-  if (!optionsList || optionsList.length === 0) return
+  if (!optionsList || optionsList.length === 0) {
+    selectedGame.value = ''
+    gameSelectInfo.optionsList = []
+    return
+  }
   optionsList.forEach((item) => {
     gameSelectInfo.optionsList.push({
       value: item.gid,
       label: item.gameName
     })
   })
-
-  gameSelectInfo.defaultSelect = optionsList[0].gid
-  changeGame(optionsList[0].gid)
-  // 去找本地的gid,如果有,就赋值,否则用请求回来的第一个gid
+  // 这个主要是用在初始化
+  // if (selectInfo.gid) {
+  //   selectedGame.value = selectInfo.gid
+  // }
 }
 
 /**
+ * TODO 这里好像没有必要
+ *
  * 获取导航栏游戏选择框的数据
  */
-const updateNavbarGameSelect = async (query: string) => {
+const updateNavbarGameSelect = async (query: string, force = false) => {
+  if (!query && !force) {
+    return
+  }
+
   try {
     gameSelectLoading.value = true
-    console.log(gameSelectInfo.defaultSelect)
+
     const res = (await AxiosInstance.post(AllApi.getGidList, {
       active: isFilterNotActiveGame.value,
       search: query
     })) as ResponseInfo
+
     if (res.code !== 0) {
       setNavbarGameSelect([])
       return false
@@ -312,12 +337,17 @@ const watchLoadingState = watch(
 )
 
 updateGameInfo()
-updateNavbarGameSelect('')
+updateNavbarGameSelect('', true)
 onMounted(() => {
   // 去加载所有需要的资源
   initLoadResource(resourceInfo).then((data) => {
     Object.assign(blobUrlInfo, data)
   })
+  selectedGame.value = selectInfo.gid
+
+  updateUserInfo({
+    ...userInfo
+  })
 })
 </script>
 
@@ -340,16 +370,18 @@ onMounted(() => {
             <el-select
               style="width: 100%"
               @change="changeGame"
-              v-model="gameSelectInfo.defaultSelect"
+              v-model="selectedGame"
               :placeholder="gameSelectInfo.title"
               filterable
-              remote
               :loading="gameSelectLoading"
-              :remote-method="updateNavbarGameSelect"
               v-bind="$attrs"
             >
+              <!--              :remote-method="updateNavbarGameSelect"-->
+              <!--              remote-->
               <template #header>
-                <el-checkbox v-model="isFilterNotActiveGame" @change="updateNavbarGameSelect('')"
+                <el-checkbox
+                  v-model="isFilterNotActiveGame"
+                  @change="updateNavbarGameSelect('', true)"
                   >是否过滤不活跃游戏
                 </el-checkbox>
               </template>
@@ -394,17 +426,18 @@ onMounted(() => {
           </el-menu>
         </div>
         <div class="headPortraitBox">
-          <el-popover popper-class="headPopper" placement="bottom-end" trigger="click">
-            <template #reference>
-              <el-image class="headPortrait" :src="blobUrlInfo.defaultHead"></el-image>
-            </template>
-            <div class="userTools">
-              <span class="userToolsItem" @click="logOut">
-                <icon-material-symbols-light-logout></icon-material-symbols-light-logout>
-                <span> 退出登录</span>
-              </span>
-            </div>
-          </el-popover>
+          <UserHeadIcon :head-icon="blobUrlInfo.defaultHead"></UserHeadIcon>
+          <!--          <el-popover popper-class="headPopper" placement="bottom-end" trigger="click">-->
+          <!--            <template #reference>-->
+          <!--              <el-image class="headPortrait" :src="blobUrlInfo.defaultHead"></el-image>-->
+          <!--            </template>-->
+          <!--            <div class="userTools">-->
+          <!--              <span class="userToolsItem" @click="logOut">-->
+          <!--                <icon-material-symbols-light-logout></icon-material-symbols-light-logout>-->
+          <!--                <span> 退出登录</span>-->
+          <!--              </span>-->
+          <!--            </div>-->
+          <!--          </el-popover>-->
         </div>
       </div>
       <!-- 侧边栏 -->