Browse Source

release: update `3.6.0`

i18n
xiaoxian521 2 years ago
parent
commit
f14077bc6f
  1. 2
      locales/en.yaml
  2. 2
      locales/zh-CN.yaml
  3. 37
      mock/asyncRoutes.ts
  4. 36
      mock/login.ts
  5. 27
      mock/refreshToken.ts
  6. 2
      package.json
  7. 2
      public/serverConfig.json
  8. 8
      src/api/routes.ts
  9. 43
      src/api/user.ts
  10. 5
      src/components/ReAuth/index.ts
  11. 20
      src/components/ReAuth/src/auth.tsx
  12. 13
      src/directives/auth/index.ts
  13. 2
      src/directives/index.ts
  14. 18
      src/directives/permission/index.ts
  15. 3
      src/layout/components/search/components/SearchModal.vue
  16. 4
      src/layout/components/setting/index.vue
  17. 6
      src/layout/components/sidebar/logo.vue
  18. 2
      src/layout/components/sidebar/mixNav.vue
  19. 5
      src/layout/components/sidebar/vertical.vue
  20. 22
      src/layout/hooks/useNav.ts
  21. 2
      src/layout/types.ts
  22. 4
      src/main.ts
  23. 21
      src/router/index.ts
  24. 2
      src/router/modules/error.ts
  25. 1
      src/router/types.ts
  26. 92
      src/router/utils.ts
  27. 37
      src/store/modules/permission.ts
  28. 4
      src/store/modules/types.ts
  29. 88
      src/store/modules/user.ts
  30. 2
      src/style/element-plus.scss
  31. 86
      src/utils/auth.ts
  32. 52
      src/utils/http/index.ts
  33. 25
      src/views/login/index.vue
  34. 96
      src/views/permission/button/index.vue
  35. 96
      src/views/permission/page/index.vue
  36. 1
      types/global.d.ts
  37. 6
      types/index.ts

2
locales/en.yaml

@ -25,7 +25,7 @@ buttons:
menus:
hshome: Home
hslogin: Login
hserror: Error Page
hsabnormal: Abnormal Page
hsfourZeroFour: "404"
hsfourZeroOne: "403"
hsFive: "500"

2
locales/zh-CN.yaml

@ -25,7 +25,7 @@ buttons:
menus:
hshome: 首页
hslogin: 登录
hserror: 错误页面
hsabnormal: 异常页面
hsfourZeroFour: "404"
hsfourZeroOne: "403"
hsFive: "500"

37
mock/asyncRoutes.ts

@ -1,18 +1,25 @@
// 根据角色动态生成路由
// 模拟后端动态生成路由
import { MockMethod } from "vite-plugin-mock";
/**
* roles "admin""common"
* admin
* common
*/
const permissionRouter = {
path: "/permission",
meta: {
title: "menus.permission",
icon: "lollipop",
rank: 7
rank: 10
},
children: [
{
path: "/permission/page/index",
name: "PermissionPage",
meta: {
roles: ["admin", "common"],
title: "menus.permissionPage"
}
},
@ -21,34 +28,22 @@ const permissionRouter = {
name: "PermissionButton",
meta: {
title: "menus.permissionButton",
authority: []
roles: ["admin", "common"],
auths: ["btn_add", "btn_edit", "btn_delete"]
}
}
]
};
// 添加不同按钮权限到/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: [setDifAuthority("v-admin", permissionRouter)]
};
} else {
return {
code: 0,
info: [setDifAuthority("v-test", permissionRouter)]
};
}
response: () => {
return {
success: true,
data: [permissionRouter]
};
}
}
] as MockMethod[];

36
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[];

27
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[];

2
package.json

@ -1,6 +1,6 @@
{
"name": "pure-admin-thin",
"version": "3.5.0",
"version": "3.6.0",
"private": true,
"scripts": {
"dev": "NODE_OPTIONS=--max-old-space-size=4096 vite",

2
public/serverConfig.json

@ -1,5 +1,5 @@
{
"Version": "3.5.0",
"Version": "3.6.0",
"Title": "PureAdmin",
"FixedHeader": true,
"HiddenSideBar": false,

8
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");
};

43
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
src/components/ReAuth/index.ts

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

20
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;
};
}
});

13
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']\"");
}
}
};

2
src/directives/index.ts

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

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']\"");
}
}
};

3
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({

4
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();
}

6
src/layout/components/sidebar/logo.vue

@ -51,6 +51,12 @@ const { title } = useNav();
margin-top: 5px;
.sidebar-title {
display: block;
width: 160px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
color: #1890ff;
font-weight: 600;
font-size: 20px;

2
src/layout/components/sidebar/mixNav.vue

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

5
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);

22
src/layout/hooks/useNav.ts

@ -1,26 +1,25 @@
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 { 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(() => {
@ -39,7 +38,7 @@ export function useNav() {
});
const avatarsStyle = computed(() => {
return username ? { marginRight: "10px" } : "";
return username.value ? { marginRight: "10px" } : "";
});
const isCollapse = computed(() => {
@ -68,10 +67,7 @@ export function useNav() {
/** 退出登录 */
function logout() {
useMultiTagsStoreHook().handleTags("equal", [...routerArrays]);
storageSession.removeItem("info");
router.push("/login");
resetRouter();
useUserStoreHook().logOut();
}
function backHome() {

2
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
src/main.ts

@ -44,6 +44,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();

21
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,
@ -96,10 +97,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;
@ -107,7 +108,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) {
@ -118,8 +124,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 => {

2
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
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;
};

92
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,14 @@ 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);
resolve(router);
} else {
formatFlatteningRoutes(addAsyncRoutes(info)).map(
formatFlatteningRoutes(addAsyncRoutes(data)).map(
(v: RouteRecordRaw) => {
// 防止重复添加路由
if (
@ -144,7 +182,7 @@ function initRouter(name: string) {
resolve(router);
}
);
usePermissionStoreHook().changeSetting(info);
usePermissionStoreHook().handleWholeMenus(data);
}
addPathMatch();
});
@ -275,30 +313,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 +343,6 @@ export {
findRouteByPath,
handleAliveRoute,
formatTwoStageRoutes,
formatFlatteningRoutes
formatFlatteningRoutes,
filterNoPermissionTree
};

37
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 = [];
}
}

4
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;
};

88
src/store/modules/user.ts

@ -1,45 +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:忘记密码)
currentPage: 0
}),
actions: {
SET_TOKEN(token) {
this.token = token;
/** 存储用户名 */
SET_USERNAME(username: string) {
this.username = username;
},
/** 存储角色 */
SET_ROLES(roles: Array<string>) {
this.roles = roles;
},
SET_NAME(name) {
this.name = name;
/** 存储前端生成的验证码 */
SET_VERIFYCODE(verifyCode: string) {
this.verifyCode = verifyCode;
},
/** 存储登录页面显示哪个组件 */
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 => {
@ -47,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);
});
});
}
}

2
src/style/element-plus.scss

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

86
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);
}

52
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的接口(通过设置请求白名单,防止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);

25
src/views/login/index.vue

@ -7,9 +7,9 @@ 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 { useLayout } from "/@/layout/hooks/useLayout";
import { useUserStoreHook } from "/@/store/modules/user";
import { bg, avatar, illustration } from "./utils/static";
import { useRenderIcon } from "/@/components/ReIcon/src/hooks";
import { ref, reactive, toRaw, onMounted, onBeforeUnmount } from "vue";
@ -32,6 +32,7 @@ initStorage();
const { t } = useI18n();
const { dataTheme, dataThemeChange } = useDataThemeChange();
dataThemeChange();
const { title, getDropdownItemStyle, getDropdownItemClass } = useNav();
const { locale, translationCh, translationEn } = useTranslationLang();
@ -45,17 +46,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;
@ -63,8 +64,6 @@ const onLogin = async (formEl: FormInstance | undefined) => {
});
};
dataThemeChange();
/** 使用公共函数,避免`removeEventListener`失效 */
function onkeypress({ code }: KeyboardEvent) {
if (code === "Enter") {

96
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>

96
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">
模拟后台根据不同角色返回对应路由具体参考完整版pure-admin代码
</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>

1
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"];
}
}

6
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`链接 `可选` */

Loading…
Cancel
Save