Pārlūkot izejas kodu

refactor: permission (#357)

* refactor: permission

* chore: update

* chore: update

* chore: update

* chore: update

* chore: update

* chore: update

* chore: update

* chore: update

* chore: update

* fix: 修复`mix`混合模式导航在生产环境左侧菜单一定机率不显示的问题

* chore: update

* chore: update

* chore: update

* chore: update

* chore: update

* chore: update

* chore: update

* chore: update
RealityBoy 2 gadi atpakaļ
vecāks
revīzija
6ef4cf9fb6
47 mainītis faili ar 604 papildinājumiem un 385 dzēšanām
  1. 1 1
      locales/en.yaml
  2. 1 1
      locales/zh-CN.yaml
  3. 37 38
      mock/asyncRoutes.ts
  4. 1 1
      mock/list.ts
  5. 36 0
      mock/login.ts
  6. 2 2
      mock/map.ts
  7. 27 0
      mock/refreshToken.ts
  8. 3 3
      mock/system.ts
  9. 1 1
      package.json
  10. 4 4
      pnpm-lock.yaml
  11. 1 2
      src/api/list.ts
  12. 2 2
      src/api/mock.ts
  13. 4 4
      src/api/routes.ts
  14. 2 3
      src/api/system.ts
  15. 28 15
      src/api/user.ts
  16. 5 0
      src/components/ReAuth/index.ts
  17. 20 0
      src/components/ReAuth/src/auth.tsx
  18. 2 1
      src/components/ReFlowChart/src/Control.vue
  19. 2 2
      src/components/ReMap/src/Amap.vue
  20. 13 0
      src/directives/auth/index.ts
  21. 1 1
      src/directives/index.ts
  22. 0 18
      src/directives/permission/index.ts
  23. 2 1
      src/layout/components/search/components/SearchModal.vue
  24. 3 1
      src/layout/components/setting/index.vue
  25. 1 1
      src/layout/components/sidebar/mixNav.vue
  26. 3 2
      src/layout/components/sidebar/vertical.vue
  27. 9 13
      src/layout/hooks/useNav.ts
  28. 1 1
      src/layout/types.ts
  29. 4 0
      src/main.ts
  30. 15 6
      src/router/index.ts
  31. 1 1
      src/router/modules/error.ts
  32. 1 0
      src/router/types.ts
  33. 64 27
      src/router/utils.ts
  34. 4 33
      src/store/modules/permission.ts
  35. 2 2
      src/store/modules/types.ts
  36. 45 39
      src/store/modules/user.ts
  37. 1 1
      src/style/element-plus.scss
  38. 58 28
      src/utils/auth.ts
  39. 29 23
      src/utils/http/index.ts
  40. 3 1
      src/views/able/line-tree.vue
  41. 3 1
      src/views/able/menu-tree.vue
  42. 26 32
      src/views/login/index.vue
  43. 70 26
      src/views/permission/button/index.vue
  44. 56 40
      src/views/permission/page/index.vue
  45. 5 5
      src/views/tabs/index.vue
  46. 1 0
      types/global.d.ts
  47. 4 2
      types/index.ts

+ 1 - 1
locales/en.yaml

@@ -32,7 +32,7 @@ menus:
   hsRole: Role Manage
   hsDept: Dept Manage
   hseditor: Editor
-  hserror: Error Page
+  hsabnormal: Abnormal Page
   hsfourZeroFour: "404"
   hsfourZeroOne: "403"
   hsFive: "500"

+ 1 - 1
locales/zh-CN.yaml

@@ -32,7 +32,7 @@ menus:
   hsRole: 角色管理
   hsDept: 部门管理
   hseditor: 编辑器
-  hserror: 错误页面
+  hsabnormal: 异常页面
   hsfourZeroFour: "404"
   hsfourZeroOne: "403"
   hsFive: "500"

+ 37 - 38
mock/asyncRoutes.ts

@@ -1,7 +1,12 @@
-// 根据角色动态生成路由
+// 模拟后端动态生成路由
 import { MockMethod } from "vite-plugin-mock";
 
-// http://mockjs.com/examples.html#Object
+/**
+ * roles:页面级别权限,这里模拟二种 "admin"、"common"
+ * admin:管理员角色
+ * common:普通角色
+ */
+
 const systemRouter = {
   path: "/system",
   meta: {
@@ -15,7 +20,8 @@ const systemRouter = {
       name: "User",
       meta: {
         icon: "flUser",
-        title: "menus.hsUser"
+        title: "menus.hsUser",
+        roles: ["admin"]
       }
     },
     {
@@ -23,7 +29,8 @@ const systemRouter = {
       name: "Role",
       meta: {
         icon: "role",
-        title: "menus.hsRole"
+        title: "menus.hsRole",
+        roles: ["admin"]
       }
     },
     {
@@ -31,7 +38,8 @@ const systemRouter = {
       name: "Dept",
       meta: {
         icon: "dept",
-        title: "menus.hsDept"
+        title: "menus.hsDept",
+        roles: ["admin"]
       }
     },
     {
@@ -41,7 +49,8 @@ const systemRouter = {
       meta: {
         icon: "dict",
         title: "menus.hsDict",
-        keepAlive: true
+        keepAlive: true,
+        roles: ["admin"]
       }
     }
   ]
@@ -52,13 +61,14 @@ const permissionRouter = {
   meta: {
     title: "menus.permission",
     icon: "lollipop",
-    rank: 7
+    rank: 10
   },
   children: [
     {
       path: "/permission/page/index",
       name: "PermissionPage",
       meta: {
+        roles: ["admin", "common"],
         title: "menus.permissionPage"
       }
     },
@@ -67,7 +77,8 @@ const permissionRouter = {
       name: "PermissionButton",
       meta: {
         title: "menus.permissionButton",
-        authority: []
+        roles: ["admin", "common"],
+        auths: ["btn_add", "btn_edit", "btn_delete"]
       }
     }
   ]
@@ -78,7 +89,7 @@ const frameRouter = {
   meta: {
     icon: "monitor",
     title: "menus.hsExternalPage",
-    rank: 10
+    rank: 7
   },
   children: [
     {
@@ -86,14 +97,16 @@ const frameRouter = {
       name: "FramePure",
       meta: {
         title: "menus.hsPureDocument",
-        frameSrc: "http://yiming_chang.gitee.io/pure-admin-doc"
+        frameSrc: "http://yiming_chang.gitee.io/pure-admin-doc",
+        roles: ["admin", "common"]
       }
     },
     {
       path: "/external",
       name: "http://yiming_chang.gitee.io/pure-admin-doc",
       meta: {
-        title: "menus.externalLink"
+        title: "menus.externalLink",
+        roles: ["admin", "common"]
       }
     },
     {
@@ -101,7 +114,8 @@ const frameRouter = {
       name: "FrameEp",
       meta: {
         title: "menus.hsEpDocument",
-        frameSrc: "https://element-plus.org/zh-CN/"
+        frameSrc: "https://element-plus.org/zh-CN/",
+        roles: ["admin", "common"]
       }
     }
   ]
@@ -119,7 +133,8 @@ const tabsRouter = {
       path: "/tabs/index",
       name: "Tabs",
       meta: {
-        title: "menus.hstabs"
+        title: "menus.hstabs",
+        roles: ["admin", "common"]
       }
     },
     {
@@ -127,7 +142,8 @@ const tabsRouter = {
       name: "TabQueryDetail",
       meta: {
         // 不在menu菜单中显示
-        showLink: false
+        showLink: false,
+        roles: ["admin", "common"]
       }
     },
     {
@@ -135,39 +151,22 @@ const tabsRouter = {
       component: "params-detail",
       name: "TabParamsDetail",
       meta: {
-        showLink: false
+        showLink: false,
+        roles: ["admin", "common"]
       }
     }
   ]
 };
 
-// 添加不同按钮权限到/permission/button页面中
-function setDifAuthority(authority, routes) {
-  routes.children[1].meta.authority = [authority];
-  return routes;
-}
-
 export default [
   {
     url: "/getAsyncRoutes",
     method: "get",
-    response: ({ query }) => {
-      if (query.name === "admin") {
-        return {
-          code: 0,
-          info: [
-            tabsRouter,
-            frameRouter,
-            systemRouter,
-            setDifAuthority("v-admin", permissionRouter)
-          ]
-        };
-      } else {
-        return {
-          code: 0,
-          info: [tabsRouter, setDifAuthority("v-test", permissionRouter)]
-        };
-      }
+    response: () => {
+      return {
+        success: true,
+        data: [systemRouter, permissionRouter, frameRouter, tabsRouter]
+      };
     }
   }
 ] as MockMethod[];

+ 1 - 1
mock/list.ts

@@ -6,7 +6,7 @@ export default [
     method: "post",
     response: () => {
       return {
-        code: 0,
+        success: true,
         data: {
           list: [
             {

+ 36 - 0
mock/login.ts

@@ -0,0 +1,36 @@
+// 根据角色动态生成路由
+import { MockMethod } from "vite-plugin-mock";
+
+export default [
+  {
+    url: "/login",
+    method: "post",
+    response: ({ body }) => {
+      if (body.username === "admin") {
+        return {
+          success: true,
+          data: {
+            username: "admin",
+            // 一个用户可能有多个角色
+            roles: ["admin"],
+            accessToken: "eyJhbGciOiJIUzUxMiJ9.admin",
+            refreshToken: "eyJhbGciOiJIUzUxMiJ9.adminRefresh",
+            expires: "2023/10/30 00:00:00"
+          }
+        };
+      } else {
+        return {
+          success: true,
+          data: {
+            username: "common",
+            // 一个用户可能有多个角色
+            roles: ["common"],
+            accessToken: "eyJhbGciOiJIUzUxMiJ9.common",
+            refreshToken: "eyJhbGciOiJIUzUxMiJ9.commonRefresh",
+            expires: "2023/10/30 00:00:00"
+          }
+        };
+      }
+    }
+  }
+] as MockMethod[];

+ 2 - 2
mock/map.ts

@@ -29,8 +29,8 @@ export default [
     method: "get",
     response: () => {
       return {
-        code: 0,
-        info: mapList()
+        success: true,
+        data: mapList()
       };
     }
   }

+ 27 - 0
mock/refreshToken.ts

@@ -0,0 +1,27 @@
+import { MockMethod } from "vite-plugin-mock";
+
+// 模拟刷新token接口
+export default [
+  {
+    url: "/refreshToken",
+    method: "post",
+    response: ({ body }) => {
+      if (body.refreshToken) {
+        return {
+          success: true,
+          data: {
+            accessToken: "eyJhbGciOiJIUzUxMiJ9.admin",
+            refreshToken: "eyJhbGciOiJIUzUxMiJ9.adminRefresh",
+            // `expires`选择这种日期格式是为了方便调试,后端直接设置时间戳或许更方便(每次都应该递增)。如果后端返回的是时间戳格式,前端开发请来到这个目录`src/utils/auth.ts`,把第`38`行的代码换成expires = data.expires即可。
+            expires: "2023/10/30 23:59:59"
+          }
+        };
+      } else {
+        return {
+          success: false,
+          data: {}
+        };
+      }
+    }
+  }
+] as MockMethod[];

+ 3 - 3
mock/system.ts

@@ -6,7 +6,7 @@ export default [
     method: "post",
     response: () => {
       return {
-        code: 0,
+        success: true,
         data: {
           list: [
             {
@@ -71,7 +71,7 @@ export default [
     method: "post",
     response: () => {
       return {
-        code: 0,
+        success: true,
         data: [
           {
             name: "杭州总公司",
@@ -212,7 +212,7 @@ export default [
     method: "post",
     response: () => {
       return {
-        code: 0,
+        success: true,
         data: {
           list: [
             {

+ 1 - 1
package.json

@@ -35,7 +35,7 @@
     "@pureadmin/components": "^1.1.0",
     "@pureadmin/descriptions": "^1.1.0",
     "@pureadmin/table": "^1.2.0",
-    "@pureadmin/utils": "^1.1.4",
+    "@pureadmin/utils": "^1.1.5",
     "@vueuse/core": "^9.3.0",
     "@vueuse/motion": "^2.0.0-beta.12",
     "@vueuse/shared": "^9.3.0",

+ 4 - 4
pnpm-lock.yaml

@@ -22,7 +22,7 @@ specifiers:
   "@pureadmin/descriptions": ^1.1.0
   "@pureadmin/table": ^1.2.0
   "@pureadmin/theme": ^2.4.0
-  "@pureadmin/utils": ^1.1.4
+  "@pureadmin/utils": ^1.1.5
   "@types/element-resize-detector": 1.1.3
   "@types/js-cookie": ^3.0.1
   "@types/lodash": ^4.14.180
@@ -132,7 +132,7 @@ dependencies:
   "@pureadmin/components": 1.1.0_vue@3.2.40
   "@pureadmin/descriptions": 1.1.0
   "@pureadmin/table": 1.2.0
-  "@pureadmin/utils": 1.1.4_888d42e6b1d4aaf209a7326195b5949d
+  "@pureadmin/utils": 1.1.5_888d42e6b1d4aaf209a7326195b5949d
   "@vueuse/core": 9.3.0_vue@3.2.40
   "@vueuse/motion": 2.0.0-beta.12_vue@3.2.40
   "@vueuse/shared": 9.3.0_vue@3.2.40
@@ -1428,10 +1428,10 @@ packages:
       string-hash: 1.1.3
     dev: true
 
-  /@pureadmin/utils/1.1.4_888d42e6b1d4aaf209a7326195b5949d:
+  /@pureadmin/utils/1.1.5_888d42e6b1d4aaf209a7326195b5949d:
     resolution:
       {
-        integrity: sha512-c3Zl9v6usKUqz6y8wYhk89g/hXz/I5QzHS7dTum8/YomqDMBph7c70u0J1dAgruDnEIIB2SNDuEWyGD8054WsQ==
+        integrity: sha512-5nQZyFAbs59gkMBj0WLox7BlY7llILR/ENo2QNEKW6avMt8sDL1+858EFjEbELl6enPsVvJpoCTxatmZzVjyAw==
       }
     peerDependencies:
       dayjs: "*"

+ 1 - 2
src/api/list.ts

@@ -1,12 +1,11 @@
 import { http } from "../utils/http";
 
 type Result = {
+  success: boolean;
   data?: {
     /** 列表数据 */
     list: Array<any>;
   };
-  code?: number;
-  msg?: string;
 };
 
 /** 卡片列表 */

+ 2 - 2
src/api/mock.ts

@@ -1,8 +1,8 @@
 import { http } from "../utils/http";
 
 type Result = {
-  code: number;
-  info: Array<any>;
+  success: boolean;
+  data: Array<any>;
 };
 
 /** 地图数据 */

+ 4 - 4
src/api/routes.ts

@@ -1,10 +1,10 @@
 import { http } from "../utils/http";
 
 type Result = {
-  code: number;
-  info: Array<any>;
+  success: boolean;
+  data: Array<any>;
 };
 
-export const getAsyncRoutes = (params?: object) => {
-  return http.request<Result>("get", "/getAsyncRoutes", { params });
+export const getAsyncRoutes = () => {
+  return http.request<Result>("get", "/getAsyncRoutes");
 };

+ 2 - 3
src/api/system.ts

@@ -1,14 +1,13 @@
 import { http } from "../utils/http";
 
 type Result = {
+  success: boolean;
   data?: {
     /** 列表数据 */
     list: Array<any>;
     /** 总数 */
-    total: number;
+    total?: number;
   };
-  code?: number;
-  msg?: string;
 };
 
 /** 获取用户管理列表 */

+ 28 - 15
src/api/user.ts

@@ -1,26 +1,39 @@
 import { http } from "../utils/http";
 
-type Result = {
-  svg?: string;
-  code?: number;
-  info?: object;
+export type UserResult = {
+  success: boolean;
+  data: {
+    /** 用户名 */
+    username: string;
+    /** 当前登陆用户的角色 */
+    roles: Array<string>;
+    /** `token` */
+    accessToken: string;
+    /** 用于调用刷新`accessToken`的接口时所需的`token` */
+    refreshToken: string;
+    /** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx') */
+    expires: Date;
+  };
 };
 
-/** 获取验证码 */
-export const getVerify = () => {
-  return http.request<Result>("get", "/captcha");
+export type RefreshTokenResult = {
+  success: boolean;
+  data: {
+    /** `token` */
+    accessToken: string;
+    /** 用于调用刷新`accessToken`的接口时所需的`token` */
+    refreshToken: string;
+    /** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx') */
+    expires: Date;
+  };
 };
 
 /** 登录 */
-export const getLogin = (data: object) => {
-  return http.request("post", "/login", { data });
+export const getLogin = (data?: object) => {
+  return http.request<UserResult>("post", "/login", { data });
 };
 
 /** 刷新token */
-export const refreshToken = (data: object) => {
-  return http.request("post", "/refreshToken", { data });
+export const refreshTokenApi = (data?: object) => {
+  return http.request<RefreshTokenResult>("post", "/refreshToken", { data });
 };
-
-// export const searchVague = (data: object) => {
-//   return http.request("post", "/searchVague", { data });
-// };

+ 5 - 0
src/components/ReAuth/index.ts

@@ -0,0 +1,5 @@
+import auth from "./src/auth";
+
+const Auth = auth;
+
+export { Auth };

+ 20 - 0
src/components/ReAuth/src/auth.tsx

@@ -0,0 +1,20 @@
+import { defineComponent, Fragment } from "vue";
+import { hasAuth } from "/@/router/utils";
+
+export default defineComponent({
+  name: "Auth",
+  props: {
+    value: {
+      type: undefined,
+      default: []
+    }
+  },
+  setup(props, { slots }) {
+    return () => {
+      if (!slots) return null;
+      return hasAuth(props.value) ? (
+        <Fragment>{slots.default?.()}</Fragment>
+      ) : null;
+    };
+  }
+});

+ 2 - 1
src/components/ReFlowChart/src/Control.vue

@@ -116,7 +116,8 @@ onMounted(() => {
             :disabled="item.disabled"
             :style="{
               cursor: item.disabled === false ? 'pointer' : 'not-allowed',
-              color: item.disabled === false ? '' : '#00000040'
+              color: item.disabled === false ? '' : '#00000040',
+              background: 'transparent'
             }"
             @click="onControl(item, key)"
           >

+ 2 - 2
src/components/ReMap/src/Amap.vue

@@ -92,8 +92,8 @@ onBeforeMount(() => {
 
       // 获取模拟车辆信息
       mapJson()
-        .then(({ info }) => {
-          let points: object = info.map(v => {
+        .then(({ data }) => {
+          let points: object = data.map(v => {
             return {
               lnglat: [v.lng, v.lat],
               ...v

+ 13 - 0
src/directives/auth/index.ts

@@ -0,0 +1,13 @@
+import { hasAuth } from "/@/router/utils";
+import { Directive, type DirectiveBinding } from "vue";
+
+export const auth: Directive = {
+  mounted(el: HTMLElement, binding: DirectiveBinding) {
+    const { value } = binding;
+    if (value) {
+      !hasAuth(value) && el.parentNode.removeChild(el);
+    } else {
+      throw new Error("need auths! Like v-auth=\"['btn.add','btn.edit']\"");
+    }
+  }
+};

+ 1 - 1
src/directives/index.ts

@@ -1,2 +1,2 @@
-export * from "./permission";
+export * from "./auth";
 export * from "./elResizeDetector";

+ 0 - 18
src/directives/permission/index.ts

@@ -1,18 +0,0 @@
-import { usePermissionStoreHook } from "/@/store/modules/permission";
-import { Directive } from "vue";
-import type { DirectiveBinding } from "vue";
-
-export const auth: Directive = {
-  mounted(el: HTMLElement, binding: DirectiveBinding) {
-    const { value } = binding;
-    if (value) {
-      const authRoles = value;
-      const hasAuth = usePermissionStoreHook().buttonAuth.includes(authRoles);
-      if (!hasAuth) {
-        el.parentNode.removeChild(el);
-      }
-    } else {
-      throw new Error("need roles! Like v-auth=\"['admin','test']\"");
-    }
-  }
-};

+ 2 - 1
src/layout/components/search/components/SearchModal.vue

@@ -1,5 +1,6 @@
 <script lang="ts" setup>
 import { useRouter } from "vue-router";
+import { cloneDeep } from "lodash-unified";
 import SearchResult from "./SearchResult.vue";
 import SearchFooter from "./SearchFooter.vue";
 import { useNav } from "/@/layout/hooks/useNav";
@@ -31,7 +32,7 @@ const handleSearch = useDebounceFn(search, 300);
 
 /** 菜单树形结构 */
 const menusData = computed(() => {
-  return deleteChildren(usePermissionStoreHook().menusTree);
+  return deleteChildren(cloneDeep(usePermissionStoreHook().wholeMenus));
 });
 
 const show = computed({

+ 3 - 1
src/layout/components/setting/index.vue

@@ -14,6 +14,7 @@ import panel from "../panel/index.vue";
 import { emitter } from "/@/utils/mitt";
 import { resetRouter } from "/@/router";
 import { templateRef } from "@vueuse/core";
+import { removeToken } from "/@/utils/auth";
 import { routerArrays } from "/@/layout/types";
 import { useNav } from "/@/layout/hooks/useNav";
 import { useAppStoreHook } from "/@/store/modules/app";
@@ -131,7 +132,7 @@ const multiTagsCacheChange = () => {
 
 /** 清空缓存并返回登录页 */
 function onReset() {
-  router.push("/login");
+  removeToken();
   storageLocal.clear();
   storageSession.clear();
   const { Grey, Weak, MultiTagsCache, EpThemeColor, Layout } = getConfig();
@@ -140,6 +141,7 @@ function onReset() {
   useMultiTagsStoreHook().multiTagsCacheChange(MultiTagsCache);
   toggleClass(Grey, "html-grey", document.querySelector("html"));
   toggleClass(Weak, "html-weakness", document.querySelector("html"));
+  router.push("/login");
   useMultiTagsStoreHook().handleTags("equal", [...routerArrays]);
   resetRouter();
 }

+ 1 - 1
src/layout/components/sidebar/mixNav.vue

@@ -48,7 +48,7 @@ nextTick(() => {
 });
 
 watch(
-  () => route.path,
+  () => [route.path, usePermissionStoreHook().wholeMenus],
   () => {
     getDefaultActive(route.path);
   }

+ 3 - 2
src/layout/components/sidebar/vertical.vue

@@ -27,7 +27,7 @@ const menuData = computed(() => {
     : usePermissionStoreHook().wholeMenus;
 });
 
-function getSubMenuData(path) {
+function getSubMenuData(path: string) {
   // path的上级路由组成的数组
   const parentPathArr = getParentPaths(
     path,
@@ -41,6 +41,7 @@ function getSubMenuData(path) {
   if (!parenetRoute?.children) return;
   subMenuData.value = parenetRoute?.children;
 }
+
 getSubMenuData(route.path);
 
 onBeforeMount(() => {
@@ -50,7 +51,7 @@ onBeforeMount(() => {
 });
 
 watch(
-  () => route.path,
+  () => [route.path, usePermissionStoreHook().wholeMenus],
   () => {
     getSubMenuData(route.path);
     menuSelect(route.path, routers);

+ 9 - 13
src/layout/hooks/useNav.ts

@@ -1,27 +1,26 @@
 import { computed } from "vue";
-import { router } from "/@/router";
 import { getConfig } from "/@/config";
 import { useRouter } from "vue-router";
 import { emitter } from "/@/utils/mitt";
 import { routeMetaType } from "../types";
-import type { StorageConfigs } from "/#/index";
-import { routerArrays } from "/@/layout/types";
+import { useGlobal } from "@pureadmin/utils";
 import { transformI18n } from "/@/plugins/i18n";
+import { router, remainingPaths } from "/@/router";
 import { useAppStoreHook } from "/@/store/modules/app";
-import { remainingPaths, resetRouter } from "/@/router";
 import { i18nChangeLanguage } from "@wangeditor/editor";
-import { storageSession, useGlobal } from "@pureadmin/utils";
+import { useUserStoreHook } from "/@/store/modules/user";
 import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
-import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
 
 const errorInfo = "当前路由配置不正确,请检查配置";
 
 export function useNav() {
   const pureApp = useAppStoreHook();
   const routers = useRouter().options.routes;
+
   /** 用户名 */
-  const username: string =
-    storageSession.getItem<StorageConfigs>("info")?.username;
+  const username = computed(() => {
+    return useUserStoreHook()?.username;
+  });
 
   /** 设置国际化选中后的样式 */
   const getDropdownItemStyle = computed(() => {
@@ -40,7 +39,7 @@ export function useNav() {
   });
 
   const avatarsStyle = computed(() => {
-    return username ? { marginRight: "10px" } : "";
+    return username.value ? { marginRight: "10px" } : "";
   });
 
   const isCollapse = computed(() => {
@@ -69,10 +68,7 @@ export function useNav() {
 
   /** 退出登录 */
   function logout() {
-    useMultiTagsStoreHook().handleTags("equal", [...routerArrays]);
-    storageSession.removeItem("info");
-    router.push("/login");
-    resetRouter();
+    useUserStoreHook().logOut();
   }
 
   function backHome() {

+ 1 - 1
src/layout/types.ts

@@ -14,7 +14,7 @@ export type routeMetaType = {
   icon?: string;
   showLink?: boolean;
   savedPosition?: boolean;
-  authority?: Array<string>;
+  auths?: Array<string>;
 };
 
 export type RouteConfigs = {

+ 4 - 0
src/main.ts

@@ -47,6 +47,10 @@ app.component("IconifyIconOffline", IconifyIconOffline);
 app.component("IconifyIconOnline", IconifyIconOnline);
 app.component("FontIcon", FontIcon);
 
+// 全局注册按钮级别权限组件
+import { Auth } from "/@/components/ReAuth";
+app.component("Auth", Auth);
+
 getServerConfig(app).then(async config => {
   app.use(router);
   await router.isReady();

+ 15 - 6
src/router/index.ts

@@ -2,8 +2,8 @@ import { getConfig } from "/@/config";
 import { toRouteType } from "./types";
 import NProgress from "/@/utils/progress";
 import { findIndex } from "lodash-unified";
-import type { StorageConfigs } from "/#/index";
 import { transformI18n } from "/@/plugins/i18n";
+import { sessionKey, type DataInfo } from "/@/utils/auth";
 import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
 import { usePermissionStoreHook } from "/@/store/modules/permission";
 import {
@@ -15,6 +15,7 @@ import {
 import {
   ascending,
   initRouter,
+  isOneOfArray,
   getHistoryMode,
   findRouteByPath,
   handleAliveRoute,
@@ -121,10 +122,10 @@ router.beforeEach((to: toRouteType, _from, next) => {
       handleAliveRoute(newMatched);
     }
   }
-  const name = storageSession.getItem<StorageConfigs>("info");
+  const userInfo = storageSession.getItem<DataInfo<number>>(sessionKey);
   NProgress.start();
   const externalLink = isUrl(to?.name as string);
-  if (!externalLink)
+  if (!externalLink) {
     to.matched.some(item => {
       if (!item.meta.title) return "";
       const Title = getConfig().Title;
@@ -132,7 +133,12 @@ router.beforeEach((to: toRouteType, _from, next) => {
         document.title = `${transformI18n(item.meta.title)} | ${Title}`;
       else document.title = transformI18n(item.meta.title);
     });
-  if (name) {
+  }
+  if (userInfo) {
+    // 无权限跳转403页面
+    if (to.meta?.roles && !isOneOfArray(to.meta?.roles, userInfo?.roles)) {
+      next({ path: "/error/403" });
+    }
     if (_from?.name) {
       // name为超链接
       if (externalLink) {
@@ -143,8 +149,11 @@ router.beforeEach((to: toRouteType, _from, next) => {
       }
     } else {
       // 刷新
-      if (usePermissionStoreHook().wholeMenus.length === 0)
-        initRouter(name.username).then((router: Router) => {
+      if (
+        usePermissionStoreHook().wholeMenus.length === 0 &&
+        to.path !== "/login"
+      )
+        initRouter().then((router: Router) => {
           if (!useMultiTagsStoreHook().getMultiTagsCache) {
             const { path } = to;
             const index = findIndex(remainingRouter, v => {

+ 1 - 1
src/router/modules/error.ts

@@ -6,7 +6,7 @@ const errorRouter: RouteConfigsTable = {
   redirect: "/error/403",
   meta: {
     icon: "information-line",
-    title: $t("menus.hserror"),
+    title: $t("menus.hsabnormal"),
     rank: 9
   },
   children: [

+ 1 - 0
src/router/types.ts

@@ -2,6 +2,7 @@ import { RouteLocationNormalized } from "vue-router";
 
 export interface toRouteType extends RouteLocationNormalized {
   meta: {
+    roles: Array<string>;
     keepAlive?: boolean;
     dynamicLevel?: string;
   };

+ 64 - 27
src/router/utils.ts

@@ -9,10 +9,16 @@ import {
 import { router } from "./index";
 import { isProxy, toRaw } from "vue";
 import { loadEnv } from "../../build";
-import { cloneDeep } from "lodash-unified";
 import { useTimeoutFn } from "@vueuse/core";
 import { RouteConfigs } from "/@/layout/types";
-import { buildHierarchyTree } from "@pureadmin/utils";
+import {
+  isString,
+  storageSession,
+  buildHierarchyTree,
+  isIncludeAllChildren
+} from "@pureadmin/utils";
+import { cloneDeep, intersection } from "lodash-unified";
+import { sessionKey, type DataInfo } from "/@/utils/auth";
 import { usePermissionStoreHook } from "/@/store/modules/permission";
 const IFrame = () => import("/@/layout/frameView.vue");
 // https://cn.vitejs.dev/guide/features.html#glob-import
@@ -38,7 +44,7 @@ function ascending(arr: any[]) {
   );
 }
 
-/** 过滤meta中showLink为false的路由 */
+/** 过滤meta中showLink为false的菜单 */
 function filterTree(data: RouteComponent[]) {
   const newTree = cloneDeep(data).filter(
     (v: { meta: { showLink: boolean } }) => v.meta?.showLink !== false
@@ -49,6 +55,37 @@ function filterTree(data: RouteComponent[]) {
   return newTree;
 }
 
+/** 过滤children长度为0的的目录,当目录下没有菜单时,会过滤此目录,目录没有赋予roles权限,当目录下只要有一个菜单有显示权限,那么此目录就会显示 */
+function filterChildrenTree(data: RouteComponent[]) {
+  const newTree = cloneDeep(data).filter((v: any) => v?.children?.length !== 0);
+  newTree.forEach(
+    (v: { children }) => v.children && (v.children = filterTree(v.children))
+  );
+  return newTree;
+}
+
+/** 判断两个数组彼此是否存在相同值 */
+function isOneOfArray(a: Array<string>, b: Array<string>) {
+  return Array.isArray(a) && Array.isArray(b)
+    ? intersection(a, b).length > 0
+      ? true
+      : false
+    : true;
+}
+
+/** 从sessionStorage里取出当前登陆用户的角色roles,过滤无权限的菜单 */
+function filterNoPermissionTree(data: RouteComponent[]) {
+  const currentRoles =
+    storageSession.getItem<DataInfo<number>>(sessionKey).roles ?? [];
+  const newTree = cloneDeep(data).filter((v: any) =>
+    isOneOfArray(v.meta?.roles, currentRoles)
+  );
+  newTree.forEach(
+    (v: any) => v.children && (v.children = filterNoPermissionTree(v.children))
+  );
+  return filterChildrenTree(newTree);
+}
+
 /** 批量删除缓存路由(keepalive) */
 function delAliveRoutes(delAliveRouteList: Array<RouteConfigs>) {
   delAliveRouteList.forEach(route => {
@@ -115,13 +152,13 @@ function addPathMatch() {
 }
 
 /** 初始化路由 */
-function initRouter(name: string) {
+function initRouter() {
   return new Promise(resolve => {
-    getAsyncRoutes({ name }).then(({ info }) => {
-      if (info.length === 0) {
-        usePermissionStoreHook().changeSetting(info);
+    getAsyncRoutes().then(({ data }) => {
+      if (data.length === 0) {
+        usePermissionStoreHook().handleWholeMenus(data);
       } else {
-        formatFlatteningRoutes(addAsyncRoutes(info)).map(
+        formatFlatteningRoutes(addAsyncRoutes(data)).map(
           (v: RouteRecordRaw) => {
             // 防止重复添加路由
             if (
@@ -144,7 +181,7 @@ function initRouter(name: string) {
             resolve(router);
           }
         );
-        usePermissionStoreHook().changeSetting(info);
+        usePermissionStoreHook().handleWholeMenus(data);
       }
       addPathMatch();
     });
@@ -275,30 +312,29 @@ function getHistoryMode(): RouterHistory {
   }
 }
 
-/** 是否有权限 */
-function hasPermissions(value: Array<string>): boolean {
-  if (value && value instanceof Array && value.length > 0) {
-    const roles = usePermissionStoreHook().buttonAuth;
-    const permissionRoles = value;
-
-    const hasPermission = roles.some(role => {
-      return permissionRoles.includes(role);
-    });
+/** 获取当前页面按钮级别的权限 */
+function getAuths(): Array<string> {
+  return router.currentRoute.value.meta.auths as Array<string>;
+}
 
-    if (!hasPermission) {
-      return false;
-    }
-    return true;
-  } else {
-    return false;
-  }
+/** 是否有按钮级别的权限 */
+function hasAuth(value: string | Array<string>): boolean {
+  if (!value) return false;
+  /** 从当前路由的`meta`字段里获取按钮级别的所有自定义`code`值 */
+  const metaAuths = getAuths();
+  const isAuths = isString(value)
+    ? metaAuths.includes(value)
+    : isIncludeAllChildren(value, metaAuths);
+  return isAuths ? true : false;
 }
 
 export {
+  hasAuth,
+  getAuths,
   ascending,
   filterTree,
   initRouter,
-  hasPermissions,
+  isOneOfArray,
   getHistoryMode,
   addAsyncRoutes,
   delAliveRoutes,
@@ -306,5 +342,6 @@ export {
   findRouteByPath,
   handleAliveRoute,
   formatTwoStageRoutes,
-  formatFlatteningRoutes
+  formatFlatteningRoutes,
+  filterNoPermissionTree
 };

+ 4 - 33
src/store/modules/permission.ts

@@ -2,9 +2,7 @@ import { defineStore } from "pinia";
 import { store } from "/@/store";
 import { cacheType } from "./types";
 import { constantMenus } from "/@/router";
-import { cloneDeep } from "lodash-unified";
-import { RouteConfigs } from "/@/layout/types";
-import { ascending, filterTree } from "/@/router/utils";
+import { ascending, filterTree, filterNoPermissionTree } from "/@/router/utils";
 
 export const usePermissionStore = defineStore({
   id: "pure-permission",
@@ -13,40 +11,15 @@ export const usePermissionStore = defineStore({
     constantMenus,
     // 整体路由生成的菜单(静态、动态)
     wholeMenus: [],
-    // 深拷贝一个菜单树,与导航菜单不突出
-    menusTree: [],
-    buttonAuth: [],
     // 缓存页面keepAlive
     cachePageList: []
   }),
   actions: {
-    /** 获取异步路由菜单 */
-    asyncActionRoutes(routes) {
-      if (this.wholeMenus.length > 0) return;
-      this.wholeMenus = filterTree(
-        ascending(this.constantMenus.concat(routes))
-      );
-
-      this.menusTree = cloneDeep(
+    /** 组装整体路由生成的菜单 */
+    handleWholeMenus(routes: any[]) {
+      this.wholeMenus = filterNoPermissionTree(
         filterTree(ascending(this.constantMenus.concat(routes)))
       );
-
-      const getButtonAuth = (arrRoutes: Array<RouteConfigs>) => {
-        if (!arrRoutes || !arrRoutes.length) return;
-        arrRoutes.forEach((v: RouteConfigs) => {
-          if (v.meta && v.meta.authority) {
-            this.buttonAuth.push(...v.meta.authority);
-          }
-          if (v.children) {
-            getButtonAuth(v.children);
-          }
-        });
-      };
-
-      getButtonAuth(this.wholeMenus);
-    },
-    async changeSetting(routes) {
-      await this.asyncActionRoutes(routes);
     },
     cacheOperate({ mode, name }: cacheType) {
       switch (mode) {
@@ -64,8 +37,6 @@ export const usePermissionStore = defineStore({
     /** 清空缓存页面 */
     clearAllCachePage() {
       this.wholeMenus = [];
-      this.menusTree = [];
-      this.buttonAuth = [];
       this.cachePageList = [];
     }
   }

+ 2 - 2
src/store/modules/types.ts

@@ -37,8 +37,8 @@ export type setType = {
 };
 
 export type userType = {
-  token: string;
-  name?: string;
+  username?: string;
+  roles?: Array<string>;
   verifyCode?: string;
   currentPage?: number;
 };

+ 45 - 39
src/store/modules/user.ts

@@ -1,55 +1,56 @@
 import { defineStore } from "pinia";
 import { store } from "/@/store";
 import { userType } from "./types";
-import { router } from "/@/router";
 import { routerArrays } from "/@/layout/types";
+import { router, resetRouter } from "/@/router";
 import { storageSession } from "@pureadmin/utils";
-import { getLogin, refreshToken } from "/@/api/user";
-import { getToken, setToken, removeToken } from "/@/utils/auth";
+import { getLogin, refreshTokenApi } from "/@/api/user";
+import { UserResult, RefreshTokenResult } from "/@/api/user";
 import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
-
-const data = getToken();
-let token = "";
-let name = "";
-if (data) {
-  const dataJson = JSON.parse(data);
-  if (dataJson) {
-    token = dataJson?.accessToken;
-    name = dataJson?.name ?? "admin";
-  }
-}
+import {
+  type DataInfo,
+  setToken,
+  removeToken,
+  sessionKey
+} from "/@/utils/auth";
 
 export const useUserStore = defineStore({
   id: "pure-user",
   state: (): userType => ({
-    token,
-    name,
+    username:
+      storageSession.getItem<DataInfo<number>>(sessionKey)?.username ?? "",
+    // 页面级别权限
+    roles: storageSession.getItem<DataInfo<number>>(sessionKey)?.roles ?? [],
     // 前端生成的验证码(按实际需求替换)
     verifyCode: "",
-    // 登录显示组件判断 0:登录 1:手机登录 2:二维码登录 3:注册 4:忘记密码,默认0:登录
+    // 判断登录页面显示哪个组件(0:登录(默认)、1:手机登录、2:二维码登录、3:注册、4:忘记密码)
     currentPage: 0
   }),
   actions: {
-    SET_TOKEN(token) {
-      this.token = token;
+    /** 存储用户名 */
+    SET_USERNAME(username: string) {
+      this.username = username;
     },
-    SET_NAME(name) {
-      this.name = name;
+    /** 存储角色 */
+    SET_ROLES(roles: Array<string>) {
+      this.roles = roles;
     },
-    SET_VERIFYCODE(verifyCode) {
+    /** 存储前端生成的验证码 */
+    SET_VERIFYCODE(verifyCode: string) {
       this.verifyCode = verifyCode;
     },
-    SET_CURRENTPAGE(value) {
+    /** 存储登录页面显示哪个组件 */
+    SET_CURRENTPAGE(value: number) {
       this.currentPage = value;
     },
     /** 登入 */
     async loginByUsername(data) {
-      return new Promise<void>((resolve, reject) => {
+      return new Promise<UserResult>((resolve, reject) => {
         getLogin(data)
           .then(data => {
             if (data) {
-              setToken(data);
-              resolve();
+              setToken(data.data);
+              resolve(data);
             }
           })
           .catch(error => {
@@ -57,23 +58,28 @@ export const useUserStore = defineStore({
           });
       });
     },
-    /** 登出 清空缓存 */
+    /** 前端登出(不调用接口) */
     logOut() {
-      this.token = "";
-      this.name = "";
+      this.username = "";
+      this.roles = [];
       removeToken();
-      storageSession.clear();
-      useMultiTagsStoreHook().handleTags("equal", routerArrays);
       router.push("/login");
+      useMultiTagsStoreHook().handleTags("equal", [...routerArrays]);
+      resetRouter();
     },
-    /** 刷新token */
-    async refreshToken(data) {
-      removeToken();
-      return refreshToken(data).then(data => {
-        if (data) {
-          setToken(data);
-          return data;
-        }
+    /** 刷新`token` */
+    async handRefreshToken(data) {
+      return new Promise<RefreshTokenResult>((resolve, reject) => {
+        refreshTokenApi(data)
+          .then(data => {
+            if (data) {
+              setToken(data.data);
+              resolve(data);
+            }
+          })
+          .catch(error => {
+            reject(error);
+          });
       });
     }
   }

+ 1 - 1
src/style/element-plus.scss

@@ -33,7 +33,7 @@
 }
 
 .is-dark {
-  z-index: 99999 !important;
+  z-index: 9999 !important;
 }
 
 /* 重置 el-button 中 icon 的 margin */

+ 58 - 28
src/utils/auth.ts

@@ -1,42 +1,72 @@
 import Cookies from "js-cookie";
+import { storageSession } from "@pureadmin/utils";
 import { useUserStoreHook } from "/@/store/modules/user";
 
-const TokenKey = "authorized-token";
-
-type paramsMapType = {
-  name: string;
-  expires: number;
+export interface DataInfo<T> {
+  /** token */
   accessToken: string;
-};
+  /** `accessToken`的过期时间(时间戳) */
+  expires: T;
+  /** 用于调用刷新accessToken的接口时所需的token */
+  refreshToken: string;
+  /** 用户名 */
+  username?: string;
+  /** 当前登陆用户的角色 */
+  roles?: Array<string>;
+}
+
+export const sessionKey = "user-info";
+export const TokenKey = "authorized-token";
 
-/** 获取token */
-export function getToken() {
-  // 此处与TokenKey相同,此写法解决初始化时Cookies中不存在TokenKey报错
-  return Cookies.get("authorized-token");
+/** 获取`token` */
+export function getToken(): DataInfo<number> {
+  // 此处与`TokenKey`相同,此写法解决初始化时`Cookies`中不存在`TokenKey`报错
+  return Cookies.get(TokenKey)
+    ? JSON.parse(Cookies.get(TokenKey))
+    : storageSession.getItem(sessionKey);
 }
 
-/** 设置token以及过期时间(cookies、sessionStorage各一份),后端需要将用户信息和token以及过期时间都返回给前端,过期时间主要用于刷新token */
-export function setToken(data) {
-  const { accessToken, expires, name } = data;
-  // 提取关键信息进行存储
-  const paramsMap: paramsMapType = {
-    name,
-    expires: Date.now() + parseInt(expires),
-    accessToken
-  };
-  const dataString = JSON.stringify(paramsMap);
-  useUserStoreHook().SET_TOKEN(accessToken);
-  useUserStoreHook().SET_NAME(name);
+/**
+ * @description 设置`token`以及一些必要信息并采用无感刷新`token`方案
+ * 无感刷新:后端返回`accessToken`(访问接口使用的`token`)、`refreshToken`(用于调用刷新`accessToken`的接口时所需的`token`,`refreshToken`的过期时间(比如30天)应大于`accessToken`的过期时间(比如2小时))、`expires`(`accessToken`的过期时间)
+ * 将`accessToken`、`expires`这两条信息放在key值为authorized-token的cookie里(过期自动销毁)
+ * 将`username`、`roles`、`refreshToken`、`expires`这四条信息放在key值为`user-info`的sessionStorage里(浏览器关闭自动销毁)
+ */
+export function setToken(data: DataInfo<Date>) {
+  let expires = 0;
+  const { accessToken, refreshToken } = data;
+  expires = new Date(data.expires).getTime();
+  const cookieString = JSON.stringify({ accessToken, expires });
+
   expires > 0
-    ? Cookies.set(TokenKey, dataString, {
-        expires: expires / 86400000
+    ? Cookies.set(TokenKey, cookieString, {
+        expires: (expires - Date.now()) / 86400000
       })
-    : Cookies.set(TokenKey, dataString);
-  sessionStorage.setItem(TokenKey, dataString);
+    : Cookies.set(TokenKey, cookieString);
+
+  function setSessionKey(username: string, roles: Array<string>) {
+    useUserStoreHook().SET_USERNAME(username);
+    useUserStoreHook().SET_ROLES(roles);
+    storageSession.setItem(sessionKey, {
+      refreshToken,
+      expires,
+      username,
+      roles
+    });
+  }
+
+  if (data.username && data.roles) {
+    const { username, roles } = data;
+    setSessionKey(username, roles);
+  } else {
+    const { username, roles } =
+      storageSession.getItem<DataInfo<number>>(sessionKey);
+    setSessionKey(username, roles);
+  }
 }
 
-/** 删除token */
+/** 删除`token`以及key值为`user-info`的session信息 */
 export function removeToken() {
   Cookies.remove(TokenKey);
-  sessionStorage.removeItem(TokenKey);
+  sessionStorage.removeItem(sessionKey);
 }

+ 29 - 23
src/utils/http/index.ts

@@ -1,6 +1,5 @@
 import Axios, { AxiosInstance, AxiosRequestConfig } from "axios";
 import {
-  resultType,
   PureHttpError,
   RequestMethods,
   PureHttpResponse,
@@ -21,7 +20,7 @@ const defaultConfig: AxiosRequestConfig = {
   //   process.env.NODE_ENV === "production"
   //     ? VITE_PROXY_DOMAIN_REAL
   //     : VITE_PROXY_DOMAIN,
-  // 当前使用mock模拟请求,将baseURL制空,如果你的环境用到了http请求,请删除下面的baseURL启用上面的baseURL,并将11行、16行代码注释取消
+  // 当前使用mock模拟请求,将baseURL制空,如果你的环境用到了http请求,请删除下面的baseURL启用上面的baseURL,并将第10行、15行代码注释取消
   baseURL: "",
   timeout: 10000,
   headers: {
@@ -47,7 +46,7 @@ class PureHttp {
   /** 请求拦截 */
   private httpInterceptorsRequest(): void {
     PureHttp.axiosInstance.interceptors.request.use(
-      (config: PureHttpRequestConfig) => {
+      async (config: PureHttpRequestConfig) => {
         const $config = config;
         // 开启进度条动画
         NProgress.start();
@@ -60,26 +59,33 @@ class PureHttp {
           PureHttp.initConfig.beforeRequestCallback($config);
           return $config;
         }
-        const token = getToken();
-        if (token) {
-          const data = JSON.parse(token);
-          const now = new Date().getTime();
-          const expired = parseInt(data.expires) - now <= 0;
-          if (expired) {
-            // token过期刷新
-            useUserStoreHook()
-              .refreshToken(data)
-              .then((res: resultType) => {
-                config.headers["Authorization"] = "Bearer " + res.accessToken;
-                return $config;
-              });
-          } else {
-            config.headers["Authorization"] = "Bearer " + data.accessToken;
-            return $config;
-          }
-        } else {
-          return $config;
-        }
+        /** 请求白名单(通过设置请求白名单,防止token过期后再请求造成的死循环问题) */
+        const whiteList = ["/refreshToken", "/login"];
+        return whiteList.some(v => config.url.indexOf(v) > -1)
+          ? config
+          : new Promise(resolve => {
+              const data = getToken();
+              if (data) {
+                const now = new Date().getTime();
+                const expired = parseInt(data.expires) - now <= 0;
+                if (expired) {
+                  // token过期刷新
+                  useUserStoreHook()
+                    .handRefreshToken({ refreshToken: data.refreshToken })
+                    .then(res => {
+                      config.headers["Authorization"] =
+                        "Bearer " + res.data.accessToken;
+                      resolve($config);
+                    });
+                } else {
+                  config.headers["Authorization"] =
+                    "Bearer " + data.accessToken;
+                  resolve($config);
+                }
+              } else {
+                resolve($config);
+              }
+            });
       },
       error => {
         return Promise.reject(error);

+ 3 - 1
src/views/able/line-tree.vue

@@ -1,5 +1,6 @@
 <script setup lang="ts">
 import { computed } from "vue";
+import { cloneDeep } from "lodash-unified";
 import { transformI18n } from "/@/plugins/i18n";
 import ElTreeLine from "/@/components/ReTreeLine";
 import { extractPathList, deleteChildren } from "@pureadmin/utils";
@@ -9,8 +10,9 @@ defineOptions({
   name: "LineTree"
 });
 
+let menusTree = cloneDeep(usePermissionStoreHook().wholeMenus);
 let menusData = computed(() => {
-  return deleteChildren(usePermissionStoreHook().menusTree);
+  return deleteChildren(menusTree);
 });
 let expandedKeys = extractPathList(menusData.value);
 let dataProps = {

+ 3 - 1
src/views/able/menu-tree.vue

@@ -1,5 +1,6 @@
 <script setup lang="ts">
 import { ref, computed } from "vue";
+import { cloneDeep } from "lodash-unified";
 import type { ElTreeV2 } from "element-plus";
 import { transformI18n } from "/@/plugins/i18n";
 import { useRenderIcon } from "/@/components/ReIcon/src/hooks";
@@ -23,9 +24,10 @@ let dataProps = ref({
   children: "children"
 });
 const treeRef = ref<InstanceType<typeof ElTreeV2>>();
+let menusTree = cloneDeep(usePermissionStoreHook().wholeMenus);
 
 let menusData = computed(() => {
-  return deleteChildren(usePermissionStoreHook().menusTree);
+  return deleteChildren(menusTree);
 });
 
 let expandedKeys = extractPathList(menusData.value);

+ 26 - 32
src/views/login/index.vue

@@ -1,4 +1,12 @@
 <script setup lang="ts">
+import {
+  ref,
+  reactive,
+  watch,
+  computed,
+  onMounted,
+  onBeforeUnmount
+} from "vue";
 import { useI18n } from "vue-i18n";
 import Motion from "./utils/motion";
 import { useRouter } from "vue-router";
@@ -12,7 +20,6 @@ import { initRouter } from "/@/router/utils";
 import { useNav } from "/@/layout/hooks/useNav";
 import { message } from "@pureadmin/components";
 import type { FormInstance } from "element-plus";
-import { storageSession } from "@pureadmin/utils";
 import { $t, transformI18n } from "/@/plugins/i18n";
 import { operates, thirdParty } from "./utils/enums";
 import { useLayout } from "/@/layout/hooks/useLayout";
@@ -22,14 +29,6 @@ import { ReImageVerify } from "/@/components/ReImageVerify";
 import { useRenderIcon } from "/@/components/ReIcon/src/hooks";
 import { useTranslationLang } from "/@/layout/hooks/useTranslationLang";
 import { useDataThemeChange } from "/@/layout/hooks/useDataThemeChange";
-import {
-  ref,
-  reactive,
-  watch,
-  computed,
-  onMounted,
-  onBeforeUnmount
-} from "vue";
 
 import dayIcon from "/@/assets/svg/day.svg?component";
 import darkIcon from "/@/assets/svg/dark.svg?component";
@@ -38,6 +37,7 @@ import globalization from "/@/assets/svg/globalization.svg?component";
 defineOptions({
   name: "Login"
 });
+
 const imgCode = ref("");
 const router = useRouter();
 const loading = ref(false);
@@ -47,11 +47,11 @@ const currentPage = computed(() => {
   return useUserStoreHook().currentPage;
 });
 
+const { t } = useI18n();
 const { initStorage } = useLayout();
 initStorage();
-
-const { t } = useI18n();
 const { dataTheme, dataThemeChange } = useDataThemeChange();
+dataThemeChange();
 const { title, getDropdownItemStyle, getDropdownItemClass } = useNav();
 const { locale, translationCh, translationEn } = useTranslationLang();
 
@@ -66,17 +66,17 @@ const onLogin = async (formEl: FormInstance | undefined) => {
   if (!formEl) return;
   await formEl.validate((valid, fields) => {
     if (valid) {
-      // 模拟请求,需根据实际开发进行修改
-      setTimeout(() => {
-        loading.value = false;
-        storageSession.setItem("info", {
-          username: "admin",
-          accessToken: "eyJhbGciOiJIUzUxMiJ9.test"
+      useUserStoreHook()
+        .loginByUsername({ username: ruleForm.username })
+        .then(res => {
+          if (res.success) {
+            // 获取后端路由
+            initRouter().then(() => {
+              message.success("登录成功");
+              router.push("/");
+            });
+          }
         });
-        initRouter("admin").then(() => {});
-        message.success("登录成功");
-        router.push("/");
-      }, 2000);
     } else {
       loading.value = false;
       return fields;
@@ -84,16 +84,6 @@ const onLogin = async (formEl: FormInstance | undefined) => {
   });
 };
 
-function onHandle(value) {
-  useUserStoreHook().SET_CURRENTPAGE(value);
-}
-
-watch(imgCode, value => {
-  useUserStoreHook().SET_VERIFYCODE(value);
-});
-
-dataThemeChange();
-
 /** 使用公共函数,避免`removeEventListener`失效 */
 function onkeypress({ code }: KeyboardEvent) {
   if (code === "Enter") {
@@ -108,6 +98,10 @@ onMounted(() => {
 onBeforeUnmount(() => {
   window.document.removeEventListener("keypress", onkeypress);
 });
+
+watch(imgCode, value => {
+  useUserStoreHook().SET_VERIFYCODE(value);
+});
 </script>
 
 <template>
@@ -258,7 +252,7 @@ onBeforeUnmount(() => {
                     :key="index"
                     class="w-full mt-4"
                     size="default"
-                    @click="onHandle(index + 1)"
+                    @click="useUserStoreHook().SET_CURRENTPAGE(index + 1)"
                   >
                     {{ t(item.title) }}
                   </el-button>

+ 70 - 26
src/views/permission/button/index.vue

@@ -1,36 +1,80 @@
 <script setup lang="ts">
-import { ref } from "vue";
-import type { StorageConfigs } from "/#/index";
-import { storageSession } from "@pureadmin/utils";
+import { type CSSProperties, computed } from "vue";
+import { hasAuth, getAuths } from "/@/router/utils";
 
 defineOptions({
   name: "PermissionButton"
 });
 
-const auth = ref(
-  storageSession.getItem<StorageConfigs>("info").username || "admin"
-);
-
-function changRole(value) {
-  storageSession.setItem("info", {
-    username: value,
-    accessToken: `eyJhbGciOiJIUzUxMiJ9.${value}`
-  });
-  window.location.reload();
-}
+let width = computed((): CSSProperties => {
+  return {
+    width: "85vw"
+  };
+});
 </script>
 
 <template>
-  <el-card>
-    <template #header>
-      <div class="card-header">
-        <el-radio-group v-model="auth" @change="changRole">
-          <el-radio-button label="admin" />
-          <el-radio-button label="test" />
-        </el-radio-group>
-      </div>
-    </template>
-    <p v-auth="'v-admin'">只有admin可看</p>
-    <p v-auth="'v-test'">只有test可看</p>
-  </el-card>
+  <el-space direction="vertical" size="large">
+    <el-tag :style="width" size="large" effect="dark">
+      当前拥有的code列表:{{ getAuths() }}
+    </el-tag>
+
+    <el-card shadow="never" :style="width">
+      <template #header>
+        <div class="card-header">组件方式判断权限</div>
+      </template>
+      <Auth value="btn_add">
+        <el-button type="success"> 拥有code:'btn_add' 权限可见 </el-button>
+      </Auth>
+      <Auth :value="['btn_edit']">
+        <el-button type="primary"> 拥有code:['btn_edit'] 权限可见 </el-button>
+      </Auth>
+      <Auth :value="['btn_add', 'btn_edit', 'btn_delete']">
+        <el-button type="danger">
+          拥有code:['btn_add', 'btn_edit', 'btn_delete'] 权限可见
+        </el-button>
+      </Auth>
+    </el-card>
+
+    <el-card shadow="never" :style="width">
+      <template #header>
+        <div class="card-header">函数方式判断权限</div>
+      </template>
+      <el-button type="success" v-if="hasAuth('btn_add')">
+        拥有code:'btn_add' 权限可见
+      </el-button>
+      <el-button type="primary" v-if="hasAuth(['btn_edit'])">
+        拥有code:['btn_edit'] 权限可见
+      </el-button>
+      <el-button
+        type="danger"
+        v-if="hasAuth(['btn_add', 'btn_edit', 'btn_delete'])"
+      >
+        拥有code:['btn_add', 'btn_edit', 'btn_delete'] 权限可见
+      </el-button>
+    </el-card>
+
+    <el-card shadow="never" :style="width">
+      <template #header>
+        <div class="card-header">
+          指令方式判断权限(该方式不能动态修改权限)
+        </div>
+      </template>
+      <el-button type="success" v-auth="'btn_add'">
+        拥有code:'btn_add' 权限可见
+      </el-button>
+      <el-button type="primary" v-auth="['btn_edit']">
+        拥有code:['btn_edit'] 权限可见
+      </el-button>
+      <el-button type="danger" v-auth="['btn_add', 'btn_edit', 'btn_delete']">
+        拥有code:['btn_add', 'btn_edit', 'btn_delete'] 权限可见
+      </el-button>
+    </el-card>
+  </el-space>
 </template>
+
+<style lang="scss" scoped>
+:deep(.el-tag) {
+  justify-content: start;
+}
+</style>

+ 56 - 40
src/views/permission/page/index.vue

@@ -1,53 +1,69 @@
 <script setup lang="ts">
-import { ref, unref } from "vue";
-import type { StorageConfigs } from "/#/index";
-import { storageSession } from "@pureadmin/utils";
-import { useRenderIcon } from "/@/components/ReIcon/src/hooks";
+import { initRouter } from "/@/router/utils";
+import { type CSSProperties, ref, computed } from "vue";
+import { useUserStoreHook } from "/@/store/modules/user";
+import { usePermissionStoreHook } from "/@/store/modules/permission";
 
 defineOptions({
   name: "PermissionPage"
 });
 
-let purview = ref<string>(
-  storageSession.getItem<StorageConfigs>("info").username
-);
+let width = computed((): CSSProperties => {
+  return {
+    width: "85vw"
+  };
+});
 
-function changRole() {
-  if (unref(purview) === "admin") {
-    storageSession.setItem("info", {
-      username: "test",
-      accessToken: "eyJhbGciOiJIUzUxMiJ9.test"
-    });
-    window.location.reload();
-  } else {
-    storageSession.setItem("info", {
-      username: "admin",
-      accessToken: "eyJhbGciOiJIUzUxMiJ9.admin"
-    });
-    window.location.reload();
+let username = ref(useUserStoreHook()?.username);
+
+const options = [
+  {
+    value: "admin",
+    label: "管理员角色"
+  },
+  {
+    value: "common",
+    label: "普通角色"
   }
+];
+
+function onChange() {
+  useUserStoreHook()
+    .loginByUsername({ username: username.value })
+    .then(res => {
+      if (res.success) {
+        usePermissionStoreHook().clearAllCachePage();
+        initRouter();
+      }
+    });
 }
 </script>
 
 <template>
-  <el-card>
-    <template #header>
-      <div class="card-header">
-        <span>
-          当前角色:
-          <span style="font-size: 26px">{{ purview }}</span>
-          <p style="color: #ffa500">
-            查看左侧菜单变化(系统管理),模拟后台根据不同角色返回对应路由
-          </p>
-        </span>
-      </div>
-    </template>
-    <el-button
-      type="primary"
-      @click="changRole"
-      :icon="useRenderIcon('user', { color: '#fff' })"
-    >
-      切换角色
-    </el-button>
-  </el-card>
+  <el-space direction="vertical" size="large">
+    <el-tag :style="width" size="large" effect="dark">
+      模拟后台根据不同角色返回对应路由,观察左侧菜单变化(管理员角色可查看系统管理菜单、普通角色不可查看系统管理菜单)
+    </el-tag>
+    <el-card shadow="never" :style="width">
+      <template #header>
+        <div class="card-header">
+          <span>当前角色:{{ username }}</span>
+        </div>
+      </template>
+      <el-select v-model="username" @change="onChange">
+        <el-option
+          v-for="item in options"
+          :key="item.value"
+          :label="item.label"
+          :value="item.value"
+        />
+      </el-select>
+    </el-card>
+  </el-space>
 </template>
+
+<style lang="scss" scoped>
+:deep(.el-tag) {
+  justify-content: start;
+}
+</style>

+ 5 - 5
src/views/tabs/index.vue

@@ -1,5 +1,6 @@
 <script setup lang="ts">
 import { ref, computed } from "vue";
+import { cloneDeep } from "lodash-unified";
 import { transformI18n } from "/@/plugins/i18n";
 import { TreeSelect } from "@pureadmin/components";
 import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
@@ -16,13 +17,12 @@ defineOptions({
 });
 
 const { toDetail, router } = useDetail();
+let menusTree = cloneDeep(usePermissionStoreHook().wholeMenus);
 
 let treeData = computed(() => {
-  return appendFieldByUniqueId(
-    deleteChildren(usePermissionStoreHook().menusTree),
-    0,
-    { disabled: true }
-  );
+  return appendFieldByUniqueId(deleteChildren(menusTree), 0, {
+    disabled: true
+  });
 });
 
 const value = ref<string[]>([]);

+ 1 - 0
types/global.d.ts

@@ -14,6 +14,7 @@ declare module "vue" {
     IconifyIconOffline: typeof import("../src/components/ReIcon")["IconifyIconOffline"];
     IconifyIconOnline: typeof import("../src/components/ReIcon")["IconifyIconOnline"];
     FontIcon: typeof import("../src/components/ReIcon")["FontIcon"];
+    Auth: typeof import("../src/components/ReAuth")["Auth"];
   }
 }
 

+ 4 - 2
types/index.ts

@@ -74,8 +74,10 @@ export interface RouteChildrenConfigsTable {
     showLink?: boolean;
     /** 是否显示父级菜单 `可选` */
     showParent?: boolean;
-    /** 路由权限设置 `可选` */
-    authority?: Array<string>;
+    /** 页面级别权限设置 `可选` */
+    roles?: Array<string>;
+    /** 按钮级别权限设置 `可选` */
+    auths?: Array<string>;
     /** 路由组件缓存(开启 `true`、关闭 `false`)`可选` */
     keepAlive?: boolean;
     /** 内嵌的`iframe`链接 `可选` */