Browse Source

feat: 头像

yanghao 1 week ago
parent
commit
b3817104b0

+ 3 - 0
components.d.ts

@@ -20,6 +20,9 @@ declare module 'vue' {
     ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
     ElDialog: typeof import('element-plus/es')['ElDialog']
     ElDrawer: typeof import('element-plus/es')['ElDrawer']
+    ElDropdown: typeof import('element-plus/es')['ElDropdown']
+    ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
+    ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
     ElForm: typeof import('element-plus/es')['ElForm']
     ElFormItem: typeof import('element-plus/es')['ElFormItem']
     ElInput: typeof import('element-plus/es')['ElInput']

+ 1 - 1
src/App.vue

@@ -7,7 +7,7 @@ import { motion } from "motion-v";
     class="overflow-x-hidden"
     :key="$route.fullPath"
     :initial="{ opacity: 0 }"
-    :animate="{ opacity: 1, transition: { duration: 0.8, ease: 'easeOut' } }"
+    :animate="{ opacity: 1, transition: { duration: 0.5, ease: 'easeOut' } }"
   >
     <router-view></router-view>
   </motion.div>

+ 10 - 0
src/api/user.ts

@@ -11,3 +11,13 @@ export async function socialLogin(type: string, code: string, state: string) {
     },
   });
 }
+
+// 登出
+export const loginOut = () => {
+  return request.post({ url: "/admin-api/system/auth/logout" });
+};
+
+// 获取用户权限信息
+export const getUserInfo = () => {
+  return request.get({ url: "/admin-api/system/auth/get-permission-info" });
+};

BIN
src/assets/images/avatar.png


+ 187 - 5
src/components/home/header.vue

@@ -26,9 +26,132 @@
       <div class="hidden lg:flex items-center gap-3">
         <div class="flex items-center gap-3">
           <a class="text-[#606266] cursor-pointer">控制台</a>
-          <el-button type="primary" class="bg-[#0050b3]!" @click="login"
-            >登录 / 注册</el-button
-          >
+          <template v-if="isLoggedIn">
+            <el-dropdown @command="onUserCommand" trigger="click">
+              <span class="flex items-center gap-2 cursor-pointer">
+                <div class="avatar-wrapper">
+                  <img
+                    :src="userAvatar"
+                    alt="avatar"
+                    class="w-8 h-8 rounded-full avatar-image"
+                  />
+                </div>
+                <span class="text-sm text-[#303133]">{{ userName }}</span>
+              </span>
+              <template #dropdown>
+                <el-dropdown-menu>
+                  <el-dropdown-item command="profile"
+                    ><svg
+                      xmlns="http://www.w3.org/2000/svg"
+                      width="18"
+                      height="18"
+                      viewBox="0 0 16 16"
+                    >
+                      <g fill="none">
+                        <path
+                          fill="url(#SVG3BqCJdyi)"
+                          d="M11.5 8A1.5 1.5 0 0 1 13 9.5v.5c0 1.971-1.86 4-5 4s-5-2.029-5-4v-.5A1.5 1.5 0 0 1 4.5 8z"
+                        />
+                        <path
+                          fill="url(#SVGfKhxtenh)"
+                          d="M11.5 8A1.5 1.5 0 0 1 13 9.5v.5c0 1.971-1.86 4-5 4s-5-2.029-5-4v-.5A1.5 1.5 0 0 1 4.5 8z"
+                        />
+                        <path
+                          fill="url(#SVGJYCMTblH)"
+                          d="M8 1.5A2.75 2.75 0 1 1 8 7a2.75 2.75 0 0 1 0-5.5"
+                        />
+                        <defs>
+                          <linearGradient
+                            id="SVG3BqCJdyi"
+                            x1="5.378"
+                            x2="7.616"
+                            y1="8.798"
+                            y2="14.754"
+                            gradientUnits="userSpaceOnUse"
+                          >
+                            <stop offset=".125" stop-color="#9c6cfe" />
+                            <stop offset="1" stop-color="#7a41dc" />
+                          </linearGradient>
+                          <linearGradient
+                            id="SVGfKhxtenh"
+                            x1="8"
+                            x2="11.164"
+                            y1="7.286"
+                            y2="17.139"
+                            gradientUnits="userSpaceOnUse"
+                          >
+                            <stop stop-color="#885edb" stop-opacity="0" />
+                            <stop offset="1" stop-color="#e362f8" />
+                          </linearGradient>
+                          <linearGradient
+                            id="SVGJYCMTblH"
+                            x1="6.558"
+                            x2="9.361"
+                            y1="2.231"
+                            y2="6.707"
+                            gradientUnits="userSpaceOnUse"
+                          >
+                            <stop offset=".125" stop-color="#9c6cfe" />
+                            <stop offset="1" stop-color="#7a41dc" />
+                          </linearGradient>
+                        </defs>
+                      </g>
+                    </svg>
+                    <span class="pl-2">个人中心</span>
+                  </el-dropdown-item>
+                  <el-dropdown-item command="logout"
+                    ><svg
+                      xmlns="http://www.w3.org/2000/svg"
+                      width="18"
+                      height="18"
+                      viewBox="0 0 24 24"
+                    >
+                      <g fill="none">
+                        <path
+                          fill="url(#SVG0pAmxd9w)"
+                          d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2"
+                        />
+                        <path
+                          fill="url(#SVGFnXqmeDt)"
+                          d="m15.53 8.47l-.084-.073a.75.75 0 0 0-.882-.007l-.094.08L12 10.939l-2.47-2.47l-.084-.072a.75.75 0 0 0-.882-.007l-.094.08l-.073.084a.75.75 0 0 0-.007.882l.08.094L10.939 12l-2.47 2.47l-.072.084a.75.75 0 0 0-.007.882l.08.094l.084.073a.75.75 0 0 0 .882.007l.094-.08L12 13.061l2.47 2.47l.084.072a.75.75 0 0 0 .882.007l.094-.08l.073-.084a.75.75 0 0 0 .007-.882l-.08-.094L13.061 12l2.47-2.47l.072-.084a.75.75 0 0 0 .007-.882z"
+                        />
+                        <defs>
+                          <linearGradient
+                            id="SVG0pAmxd9w"
+                            x1="5.125"
+                            x2="18.25"
+                            y1="3.25"
+                            y2="22.625"
+                            gradientUnits="userSpaceOnUse"
+                          >
+                            <stop stop-color="#f83f54" />
+                            <stop offset="1" stop-color="#ca2134" />
+                          </linearGradient>
+                          <linearGradient
+                            id="SVGFnXqmeDt"
+                            x1="8.685"
+                            x2="12.591"
+                            y1="12.332"
+                            y2="16.392"
+                            gradientUnits="userSpaceOnUse"
+                          >
+                            <stop stop-color="#fdfdfd" />
+                            <stop offset="1" stop-color="#fecbe6" />
+                          </linearGradient>
+                        </defs>
+                      </g>
+                    </svg>
+                    <span class="pl-2">退出登录</span></el-dropdown-item
+                  >
+                </el-dropdown-menu>
+              </template>
+            </el-dropdown>
+          </template>
+          <template v-else>
+            <el-button type="primary" class="bg-[#0050b3]!" @click="login"
+              >登录 / 注册</el-button
+            >
+          </template>
         </div>
       </div>
 
@@ -67,9 +190,17 @@
 
 <script setup lang="ts">
 import { Icon } from "@iconify/vue";
-import { ref } from "vue";
+import { ref, computed } from "vue";
 import { useRouter } from "vue-router";
 import logo from "@/assets/images/logo.png";
+import { useUserStoreWithOut } from "@/stores/useUserStore";
+const userStore = useUserStoreWithOut();
+
+const isLoggedIn = computed(
+  () => !!userStore.isSetUser || !!userStore.user?.id
+);
+const userAvatar = computed(() => userStore.user?.avatar || "");
+const userName = computed(() => userStore.user?.nickname || "");
 
 const router = useRouter();
 const drawer = ref(false);
@@ -83,6 +214,57 @@ const login = () => {
     path: "/login",
   });
 };
+
+const onUserCommand = async (command: string) => {
+  if (command === "logout") {
+    await userStore.loginOut();
+
+    router.replace("/login");
+  }
+};
 </script>
 
-<style scoped></style>
+<style scoped>
+.avatar-wrapper {
+  position: relative;
+  overflow: hidden;
+  border-radius: 50%;
+}
+
+.avatar-wrapper::before {
+  content: "";
+  position: absolute;
+  top: 0;
+  left: -100%;
+  width: 50%;
+  height: 100%;
+  background: linear-gradient(
+    90deg,
+    rgba(255, 255, 255, 0) 0%,
+    rgba(255, 255, 255, 0.8) 50%,
+    rgba(255, 255, 255, 0) 100%
+  );
+  transform: skewX(-25deg);
+  transition: none;
+  z-index: 1;
+  opacity: 0;
+}
+
+.avatar-wrapper:hover::before {
+  animation: shine 0.5s ease-out;
+}
+
+@keyframes shine {
+  0% {
+    left: -100%;
+    opacity: 0;
+  }
+  10% {
+    opacity: 1;
+  }
+  100% {
+    left: 100%;
+    opacity: 0;
+  }
+}
+</style>

+ 3 - 5
src/main.ts

@@ -1,7 +1,7 @@
 import { createApp } from "vue";
-import { createPinia } from "pinia";
 
-import piniaPersist from "pinia-plugin-persistedstate";
+// 引入状态管理
+import { setupStore } from "@/stores";
 
 import "./assets/style/main.css";
 
@@ -10,10 +10,8 @@ import router from "./router";
 
 const app = createApp(App);
 
-const pinia = createPinia();
-pinia.use(piniaPersist);
+setupStore(app);
 
-app.use(pinia);
 app.use(router);
 
 app.mount("#app");

+ 72 - 2
src/router/index.ts

@@ -13,6 +13,9 @@ import Login from "@/views/login.vue";
 import { getAccessToken } from "@utils/auth";
 import { socialLogin } from "@/api/user";
 import * as authUtil from "@/utils/auth";
+import { isRelogin } from "@/config/axios/service";
+
+import { useUserStoreWithOut } from "@/stores/useUserStore";
 
 const routes: RouteRecordRaw[] = [
   {
@@ -24,21 +27,33 @@ const routes: RouteRecordRaw[] = [
     path: "/login",
     name: "Login",
     component: Login,
+    meta: {
+      title: "DeepOil 智慧经营平台 | 登录",
+    },
   },
   {
     path: "/management",
     name: "Management",
     component: Management,
+    meta: {
+      title: "DeepOil 智慧经营平台 | 经营管理平台",
+    },
   },
   {
     path: "/command",
     name: "Command",
     component: Command,
+    meta: {
+      title: "DeepOil 智慧经营平台 | 生产指挥平台",
+    },
   },
   {
     path: "/chatbi",
     name: "ChatBI",
     component: ChatBI,
+    meta: {
+      title: "DeepOil 智慧经营平台 | Chat BI平台",
+    },
   },
 ];
 
@@ -61,13 +76,68 @@ export const resetRouter = (): void => {
   });
 };
 
+const parseURL = (
+  url: string | null | undefined
+): { basePath: string; paramsObject: { [key: string]: string } } => {
+  // 如果输入为 null 或 undefined,返回空字符串和空对象
+  if (url == null) {
+    return { basePath: "", paramsObject: {} };
+  }
+
+  // 找到问号 (?) 的位置,它之前是基础路径,之后是查询参数
+  const questionMarkIndex = url.indexOf("?");
+  let basePath = url;
+  const paramsObject: { [key: string]: string } = {};
+
+  // 如果找到了问号,说明有查询参数
+  if (questionMarkIndex !== -1) {
+    // 获取 basePath
+    basePath = url.substring(0, questionMarkIndex);
+
+    // 从 URL 中获取查询字符串部分
+    const queryString = url.substring(questionMarkIndex + 1);
+
+    // 使用 URLSearchParams 遍历参数
+    const searchParams = new URLSearchParams(queryString);
+    searchParams.forEach((value, key) => {
+      // 封装进 paramsObject 对象
+      paramsObject[key] = value;
+    });
+  }
+
+  // 返回 basePath 和 paramsObject
+  return { basePath, paramsObject };
+};
+
 const whiteList = ["/login", "/social-login", "/auth-redirect"];
 router.beforeEach(async (to, from, next) => {
+  // 设置页面标题
+  const title = to.meta.title as string;
+  if (title) {
+    document.title = `${title}`;
+  }
   if (getAccessToken()) {
     if (to.path === "/login") {
       next({ path: "/" });
     } else {
-      next();
+      const userStore = useUserStoreWithOut();
+
+      if (!userStore.getIsSetUser) {
+        isRelogin.show = true;
+        await userStore.setUserInfoAction();
+        isRelogin.show = false;
+        const redirectPath = from.query.redirect || to.path;
+        // 修复跳转时不带参数的问题
+        const redirect = decodeURIComponent(redirectPath as string);
+        const { paramsObject: query } = parseURL(redirect);
+        const nextData =
+          to.path === redirect
+            ? { ...to, replace: true }
+            : { path: redirect, query };
+        next(nextData);
+      } else {
+        next();
+      }
     }
   } else {
     if (whiteList.indexOf(to.path) !== -1) {
@@ -78,7 +148,7 @@ router.beforeEach(async (to, from, next) => {
           typeof code === "string" ? code : "",
           "22"
         );
-        console.log(">>>>>>>>>>>>>>>>>>>>>>>>>", res);
+
         authUtil.setToken(res);
         next({ path: "/" });
       } else {

+ 12 - 0
src/stores/index.ts

@@ -0,0 +1,12 @@
+import type { App } from "vue";
+import { createPinia } from "pinia";
+import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
+
+const store = createPinia();
+store.use(piniaPluginPersistedstate);
+
+export const setupStore = (app: App<Element>) => {
+  app.use(store);
+};
+
+export { store };

+ 0 - 18
src/stores/loginStore.ts

@@ -1,18 +0,0 @@
-import { defineStore } from "pinia";
-
-export const useAuthStore = defineStore("login", {
-  state: () => ({
-    // userId: "",
-    // accessToken: "",
-    // refreshToken: "",
-    // expiresTime: "",
-  }),
-
-  actions: {},
-
-  // persist: {
-  //   storage: localStorage,
-  //   key: "userInfo",
-  //   pick: ["accessToken", "refreshToken"],
-  // },
-});

+ 105 - 0
src/stores/useUserStore.ts

@@ -0,0 +1,105 @@
+import { store } from "@/stores";
+import { defineStore } from "pinia";
+import { getAccessToken, removeToken } from "@/utils/auth";
+import { CACHE_KEY, useCache, deleteUserCache } from "@hooks/useCache";
+import { getUserInfo, loginOut } from "@/api/user";
+// import avatarImage from "@/assets/images/avatar.png";
+
+const { wsCache } = useCache();
+
+interface UserVO {
+  id: number;
+  avatar: string;
+  nickname: string;
+  deptId: number;
+}
+
+interface UserInfoVO {
+  // USER 缓存
+  permissions: Set<string>;
+  roles: string[];
+  isSetUser: boolean;
+  user: UserVO;
+}
+
+export const useUserStore = defineStore("admin-user", {
+  state: (): UserInfoVO => ({
+    permissions: new Set<string>(),
+    roles: [],
+    isSetUser: false,
+    user: {
+      id: 0,
+      avatar: "",
+      nickname: "",
+      deptId: 0,
+    },
+  }),
+  getters: {
+    getPermissions(): Set<string> {
+      return this.permissions;
+    },
+    getRoles(): string[] {
+      return this.roles;
+    },
+    getIsSetUser(): boolean {
+      return this.isSetUser;
+    },
+    getUser(): UserVO {
+      return this.user;
+    },
+  },
+  actions: {
+    async setUserInfoAction() {
+      if (!getAccessToken()) {
+        this.resetState();
+        return null;
+      }
+      let userInfo = wsCache.get(CACHE_KEY.USER);
+      if (!userInfo) {
+        userInfo = await getUserInfo();
+      }
+      this.permissions = new Set(userInfo.permissions);
+      this.roles = userInfo.roles;
+      this.user = userInfo.user;
+      this.isSetUser = true;
+      wsCache.set(CACHE_KEY.USER, userInfo);
+      wsCache.set(CACHE_KEY.ROLE_ROUTERS, userInfo.menus);
+    },
+    async setUserAvatarAction(avatar: string) {
+      const userInfo = wsCache.get(CACHE_KEY.USER);
+      // NOTE: 是否需要像`setUserInfoAction`一样判断`userInfo != null`
+      this.user.avatar = avatar;
+      userInfo.user.avatar = avatar;
+      wsCache.set(CACHE_KEY.USER, userInfo);
+    },
+    async setUserNicknameAction(nickname: string) {
+      const userInfo = wsCache.get(CACHE_KEY.USER);
+      // NOTE: 是否需要像`setUserInfoAction`一样判断`userInfo != null`
+      this.user.nickname = nickname;
+      userInfo.user.nickname = nickname;
+      wsCache.set(CACHE_KEY.USER, userInfo);
+    },
+    async loginOut() {
+      await loginOut();
+      removeToken();
+      deleteUserCache(); // 删除用户缓存
+      this.resetState();
+    },
+    resetState() {
+      this.permissions = new Set<string>();
+      this.roles = [];
+      this.isSetUser = false;
+      this.user = {
+        id: 0,
+        avatar: "",
+        nickname: "",
+        deptId: 0,
+      };
+    },
+  },
+});
+
+// 工厂函数写法,用于在非组件环境或需要手动传递 store 实例的场景下使用
+export const useUserStoreWithOut = () => {
+  return useUserStore(store);
+};