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"; |
|||
|
|||
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"; |
|||
|
|||
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 });
|
|||
// };
|
@ -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"; |
@ -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 { 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>; |
|||
} |
|||
|
|||
/** 获取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 |
|||
? 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); |
|||
} |
@ -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}` |
|||
let width = computed((): CSSProperties => { |
|||
return { |
|||
width: "85vw" |
|||
}; |
|||
}); |
|||
window.location.reload(); |
|||
} |
|||
</script> |
|||
|
|||
<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> |
|||
<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-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> |
@ -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 |
|||
); |
|||
|
|||
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> |
|||
|
|||
<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> |
|||
<div class="card-header"> |
|||
<span> |
|||
当前角色: |
|||
<span style="font-size: 26px">{{ purview }}</span> |
|||
<p style="color: #ffa500"> |
|||
查看左侧菜单变化(系统管理),模拟后台根据不同角色返回对应路由 |
|||
</p> |
|||
</span> |
|||
<span>当前角色:{{ username }}</span> |
|||
</div> |
|||
</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-space> |
|||
</template> |
|||
|
|||
<style lang="scss" scoped> |
|||
:deep(.el-tag) { |
|||
justify-content: start; |
|||
} |
|||
</style> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue