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
This commit is contained in:
RealityBoy 2022-10-25 12:17:13 +08:00 committed by GitHub
parent cedc84d31a
commit 6ef4cf9fb6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 605 additions and 386 deletions

View File

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

View File

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

View File

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

View File

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

36
mock/login.ts Normal file
View File

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

View File

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

27
mock/refreshToken.ts Normal file
View File

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

View File

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

View File

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

8
pnpm-lock.yaml generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -92,8 +92,8 @@ onBeforeMount(() => {
// //
mapJson() mapJson()
.then(({ info }) => { .then(({ data }) => {
let points: object = info.map(v => { let points: object = data.map(v => {
return { return {
lnglat: [v.lng, v.lat], lnglat: [v.lng, v.lat],
...v ...v

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,10 +9,16 @@ import {
import { router } from "./index"; import { router } from "./index";
import { isProxy, toRaw } from "vue"; import { isProxy, toRaw } from "vue";
import { loadEnv } from "../../build"; import { loadEnv } from "../../build";
import { cloneDeep } from "lodash-unified";
import { useTimeoutFn } from "@vueuse/core"; import { useTimeoutFn } from "@vueuse/core";
import { RouteConfigs } from "/@/layout/types"; 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"; import { usePermissionStoreHook } from "/@/store/modules/permission";
const IFrame = () => import("/@/layout/frameView.vue"); const IFrame = () => import("/@/layout/frameView.vue");
// https://cn.vitejs.dev/guide/features.html#glob-import // 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[]) { function filterTree(data: RouteComponent[]) {
const newTree = cloneDeep(data).filter( const newTree = cloneDeep(data).filter(
(v: { meta: { showLink: boolean } }) => v.meta?.showLink !== false (v: { meta: { showLink: boolean } }) => v.meta?.showLink !== false
@ -49,6 +55,37 @@ function filterTree(data: RouteComponent[]) {
return newTree; 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) */ /** 批量删除缓存路由(keepalive) */
function delAliveRoutes(delAliveRouteList: Array<RouteConfigs>) { function delAliveRoutes(delAliveRouteList: Array<RouteConfigs>) {
delAliveRouteList.forEach(route => { delAliveRouteList.forEach(route => {
@ -115,13 +152,13 @@ function addPathMatch() {
} }
/** 初始化路由 */ /** 初始化路由 */
function initRouter(name: string) { function initRouter() {
return new Promise(resolve => { return new Promise(resolve => {
getAsyncRoutes({ name }).then(({ info }) => { getAsyncRoutes().then(({ data }) => {
if (info.length === 0) { if (data.length === 0) {
usePermissionStoreHook().changeSetting(info); usePermissionStoreHook().handleWholeMenus(data);
} else { } else {
formatFlatteningRoutes(addAsyncRoutes(info)).map( formatFlatteningRoutes(addAsyncRoutes(data)).map(
(v: RouteRecordRaw) => { (v: RouteRecordRaw) => {
// 防止重复添加路由 // 防止重复添加路由
if ( if (
@ -144,7 +181,7 @@ function initRouter(name: string) {
resolve(router); resolve(router);
} }
); );
usePermissionStoreHook().changeSetting(info); usePermissionStoreHook().handleWholeMenus(data);
} }
addPathMatch(); addPathMatch();
}); });
@ -275,30 +312,29 @@ function getHistoryMode(): RouterHistory {
} }
} }
/** 是否有权限 */ /** 获取当前页面按钮级别的权限 */
function hasPermissions(value: Array<string>): boolean { function getAuths(): Array<string> {
if (value && value instanceof Array && value.length > 0) { return router.currentRoute.value.meta.auths as Array<string>;
const roles = usePermissionStoreHook().buttonAuth; }
const permissionRoles = value;
const hasPermission = roles.some(role => { /** 是否有按钮级别的权限 */
return permissionRoles.includes(role); function hasAuth(value: string | Array<string>): boolean {
}); if (!value) return false;
/** 从当前路由的`meta`字段里获取按钮级别的所有自定义`code`值 */
if (!hasPermission) { const metaAuths = getAuths();
return false; const isAuths = isString(value)
} ? metaAuths.includes(value)
return true; : isIncludeAllChildren(value, metaAuths);
} else { return isAuths ? true : false;
return false;
}
} }
export { export {
hasAuth,
getAuths,
ascending, ascending,
filterTree, filterTree,
initRouter, initRouter,
hasPermissions, isOneOfArray,
getHistoryMode, getHistoryMode,
addAsyncRoutes, addAsyncRoutes,
delAliveRoutes, delAliveRoutes,
@ -306,5 +342,6 @@ export {
findRouteByPath, findRouteByPath,
handleAliveRoute, handleAliveRoute,
formatTwoStageRoutes, formatTwoStageRoutes,
formatFlatteningRoutes formatFlatteningRoutes,
filterNoPermissionTree
}; };

View File

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

View File

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

View File

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

View File

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

View File

@ -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"; export interface DataInfo<T> {
/** token */
type paramsMapType = {
name: string;
expires: number;
accessToken: string; accessToken: string;
}; /** `accessToken`的过期时间(时间戳) */
expires: T;
/** 获取token */ /** 用于调用刷新accessToken的接口时所需的token */
export function getToken() { refreshToken: string;
// 此处与TokenKey相同此写法解决初始化时Cookies中不存在TokenKey报错 /** 用户名 */
return Cookies.get("authorized-token"); username?: string;
/** 当前登陆用户的角色 */
roles?: Array<string>;
} }
/** 设置token以及过期时间cookies、sessionStorage各一份后端需要将用户信息和token以及过期时间都返回给前端过期时间主要用于刷新token */ export const sessionKey = "user-info";
export function setToken(data) { export const TokenKey = "authorized-token";
const { accessToken, expires, name } = data;
// 提取关键信息进行存储 /** 获取`token` */
const paramsMap: paramsMapType = { export function getToken(): DataInfo<number> {
name, // 此处与`TokenKey`相同,此写法解决初始化时`Cookies`中不存在`TokenKey`报错
expires: Date.now() + parseInt(expires), return Cookies.get(TokenKey)
accessToken ? JSON.parse(Cookies.get(TokenKey))
}; : storageSession.getItem(sessionKey);
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, { ? Cookies.set(TokenKey, cookieString, {
expires: expires / 86400000 expires: (expires - Date.now()) / 86400000
}) })
: Cookies.set(TokenKey, dataString); : Cookies.set(TokenKey, cookieString);
sessionStorage.setItem(TokenKey, dataString);
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);
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -1,36 +1,80 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { type CSSProperties, computed } from "vue";
import type { StorageConfigs } from "/#/index"; import { hasAuth, getAuths } from "/@/router/utils";
import { storageSession } from "@pureadmin/utils";
defineOptions({ defineOptions({
name: "PermissionButton" name: "PermissionButton"
}); });
const auth = ref( let width = computed((): CSSProperties => {
storageSession.getItem<StorageConfigs>("info").username || "admin" return {
); width: "85vw"
};
function changRole(value) { });
storageSession.setItem("info", {
username: value,
accessToken: `eyJhbGciOiJIUzUxMiJ9.${value}`
});
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> <el-button type="success" v-auth="'btn_add'">
<p v-auth="'v-test'">只有test可看</p> 拥有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>

View File

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

View File

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

1
types/global.d.ts vendored
View File

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

View File

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