xiaoxian521
2 years ago
37 changed files with 554 additions and 320 deletions
-
2locales/en.yaml
-
2locales/zh-CN.yaml
-
33mock/asyncRoutes.ts
-
36mock/login.ts
-
27mock/refreshToken.ts
-
2package.json
-
2public/serverConfig.json
-
8src/api/routes.ts
-
43src/api/user.ts
-
5src/components/ReAuth/index.ts
-
20src/components/ReAuth/src/auth.tsx
-
13src/directives/auth/index.ts
-
2src/directives/index.ts
-
18src/directives/permission/index.ts
-
3src/layout/components/search/components/SearchModal.vue
-
4src/layout/components/setting/index.vue
-
6src/layout/components/sidebar/logo.vue
-
2src/layout/components/sidebar/mixNav.vue
-
5src/layout/components/sidebar/vertical.vue
-
22src/layout/hooks/useNav.ts
-
2src/layout/types.ts
-
4src/main.ts
-
21src/router/index.ts
-
2src/router/modules/error.ts
-
1src/router/types.ts
-
92src/router/utils.ts
-
37src/store/modules/permission.ts
-
4src/store/modules/types.ts
-
84src/store/modules/user.ts
-
2src/style/element-plus.scss
-
86src/utils/auth.ts
-
32src/utils/http/index.ts
-
23src/views/login/index.vue
-
84src/views/permission/button/index.vue
-
86src/views/permission/page/index.vue
-
1types/global.d.ts
-
6types/index.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[]; |
@ -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[]; |
@ -1,10 +1,10 @@ |
|||||
import { http } from "../utils/http"; |
import { http } from "../utils/http"; |
||||
|
|
||||
type Result = { |
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"); |
||||
}; |
}; |
@ -1,26 +1,39 @@ |
|||||
import { http } from "../utils/http"; |
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 */ |
/** 刷新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 });
|
|
||||
// };
|
|
@ -0,0 +1,5 @@ |
|||||
|
import auth from "./src/auth"; |
||||
|
|
||||
|
const Auth = auth; |
||||
|
|
||||
|
export { Auth }; |
@ -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; |
||||
|
}; |
||||
|
} |
||||
|
}); |
@ -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,2 +1,2 @@ |
|||||
export * from "./permission"; |
|
||||
|
export * from "./auth"; |
||||
export * from "./elResizeDetector"; |
export * from "./elResizeDetector"; |
@ -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']\""); |
|
||||
} |
|
||||
} |
|
||||
}; |
|
@ -1,42 +1,72 @@ |
|||||
import Cookies from "js-cookie"; |
import Cookies from "js-cookie"; |
||||
|
import { storageSession } from "@pureadmin/utils"; |
||||
import { useUserStoreHook } from "/@/store/modules/user"; |
import { useUserStoreHook } from "/@/store/modules/user"; |
||||
|
|
||||
const TokenKey = "authorized-token"; |
|
||||
|
|
||||
type paramsMapType = { |
|
||||
name: string; |
|
||||
expires: number; |
|
||||
|
export interface DataInfo<T> { |
||||
|
/** token */ |
||||
accessToken: string; |
accessToken: string; |
||||
}; |
|
||||
|
/** `accessToken`的过期时间(时间戳) */ |
||||
|
expires: T; |
||||
|
/** 用于调用刷新accessToken的接口时所需的token */ |
||||
|
refreshToken: string; |
||||
|
/** 用户名 */ |
||||
|
username?: string; |
||||
|
/** 当前登陆用户的角色 */ |
||||
|
roles?: Array<string>; |
||||
|
} |
||||
|
|
||||
/** 获取token */ |
|
||||
export function getToken() { |
|
||||
// 此处与TokenKey相同,此写法解决初始化时Cookies中不存在TokenKey报错
|
|
||||
return Cookies.get("authorized-token"); |
|
||||
|
export const sessionKey = "user-info"; |
||||
|
export const TokenKey = "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 |
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() { |
export function removeToken() { |
||||
Cookies.remove(TokenKey); |
Cookies.remove(TokenKey); |
||||
sessionStorage.removeItem(TokenKey); |
|
||||
|
sessionStorage.removeItem(sessionKey); |
||||
} |
} |
@ -1,36 +1,80 @@ |
|||||
<script setup lang="ts"> |
<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({ |
defineOptions({ |
||||
name: "PermissionButton" |
name: "PermissionButton" |
||||
}); |
}); |
||||
|
|
||||
const auth = ref( |
|
||||
storageSession.getItem<StorageConfigs>("info").username || "admin" |
|
||||
); |
|
||||
|
|
||||
function changRole(value) { |
|
||||
storageSession.setItem("info", { |
|
||||
username: value, |
|
||||
accessToken: `eyJhbGciOiJIUzUxMiJ9.${value}` |
|
||||
|
let width = computed((): CSSProperties => { |
||||
|
return { |
||||
|
width: "85vw" |
||||
|
}; |
||||
}); |
}); |
||||
window.location.reload(); |
|
||||
} |
|
||||
</script> |
</script> |
||||
|
|
||||
<template> |
<template> |
||||
<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> |
<template #header> |
||||
<div class="card-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> |
</div> |
||||
</template> |
</template> |
||||
<p v-auth="'v-admin'">只有admin可看</p> |
|
||||
<p v-auth="'v-test'">只有test可看</p> |
|
||||
|
<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-card> |
||||
|
</el-space> |
||||
</template> |
</template> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
:deep(.el-tag) { |
||||
|
justify-content: start; |
||||
|
} |
||||
|
</style> |
@ -1,53 +1,69 @@ |
|||||
<script setup lang="ts"> |
<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({ |
defineOptions({ |
||||
name: "PermissionPage" |
name: "PermissionPage" |
||||
}); |
}); |
||||
|
|
||||
let purview = ref<string>( |
|
||||
storageSession.getItem<StorageConfigs>("info").username |
|
||||
); |
|
||||
|
|
||||
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" |
|
||||
|
let width = computed((): CSSProperties => { |
||||
|
return { |
||||
|
width: "85vw" |
||||
|
}; |
||||
}); |
}); |
||||
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> |
</script> |
||||
|
|
||||
<template> |
<template> |
||||
<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> |
<template #header> |
||||
<div class="card-header"> |
<div class="card-header"> |
||||
<span> |
|
||||
当前角色: |
|
||||
<span style="font-size: 26px">{{ purview }}</span> |
|
||||
<p style="color: #ffa500"> |
|
||||
查看左侧菜单变化(系统管理),模拟后台根据不同角色返回对应路由 |
|
||||
</p> |
|
||||
</span> |
|
||||
|
<span>当前角色:{{ username }}</span> |
||||
</div> |
</div> |
||||
</template> |
</template> |
||||
<el-button |
|
||||
type="primary" |
|
||||
@click="changRole" |
|
||||
:icon="useRenderIcon('user', { color: '#fff' })" |
|
||||
> |
|
||||
切换角色 |
|
||||
</el-button> |
|
||||
|
<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-card> |
||||
|
</el-space> |
||||
</template> |
</template> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
:deep(.el-tag) { |
||||
|
justify-content: start; |
||||
|
} |
||||
|
</style> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue