问题点:
1.后端返回的JSON数组如何转换成vue-router可以接收的格式。(主要问题在compoent从string转换成组件上)
2.将转换后的数组给ant-design的MENU组件渲染。
这边我用了mock模拟后端返回。因为后端不会帮我们拼树形格式,所以默认后端返回的格式:
/**
* 我们新增路由一般会填写
@pamars id 主键ID,新增一个权限时需要后端自动加一个uuid
@pamars index 权重
@pamars title 名称
@pamars component 路由文件
@pamars path 路由地址
@pamars parent 父级
@pamars icon 图标
@pamars name 唯一标识
*
**/
let routers = [{
id: "0",
index:0,
title: "首页",
path: '/index',
component: 'index/index',
parent: "",
icon: 'HomeOutlined',
name: 'Index'
}, {
id: "1",
index:1,
title: "权限管理",
path: '/permission',
component: 'permission/index',
parent: "",
icon: 'TeamOutlined',
name: 'Permission'
},]
参考:
/*
* layout/index.vue 整个项目的上中下+侧边导航布局
*/
<template>
<a-layout>
<a-layout-sider v-model:collapsed="collapsed" :trigger="null" collapsible>
<AsideTabs ></AsideTabs>
</a-layout-sider>
<a-layout>
<a-layout-header style="background: #fff; padding: 0">
<Header v-model:value="collapsed" />
<TopTabs />
</a-layout-header>
<a-layout-content
:style="{ margin: '58px 16px 24px 16px', padding: '24px', background: '#fff', minHeight: '280px' }"
>
<AppMain />
</a-layout-content>
</a-layout>
</a-layout>
</template>
<script lang="ts">
import {
UserOutlined,
VideoCameraOutlined,
UploadOutlined,
} from '@ant-design/icons-vue';
import Header from "./header/index.vue"
import AsideTabs from "./aside-tabs/index.vue"
import AppMain from "./app-main.vue"
import TopTabs from "./top-tabs/index.vue"
import {ref,defineComponent} from "vue"
export default defineComponent({
components: {
UserOutlined,
VideoCameraOutlined,
UploadOutlined,
Header,
AsideTabs,
AppMain,
TopTabs
},
setup() {
const collapsed = ref(false)
return {
selectedKeys: ref<string[]>(['1']),
collapsed: collapsed,
};
},
})
</script>
<style>
#components-layout-demo-custom-trigger .trigger {
font-size: 18px;
line-height: 64px;
padding: 0 24px;
cursor: pointer;
transition: color 0.3s;
}
#components-layout-demo-custom-trigger .trigger:hover {
color: #1890ff;
}
#components-layout-demo-custom-trigger .logo {
height: 32px;
background: rgba(255, 255, 255, 0.3);
margin: 16px;
}
.site-layout .site-layout-background {
background: #fff;
}
</style>
<!--
* @Descripttion: aside-tabs/index.vue侧边栏菜单
* @version:
* @Author: dal
* @Date: 2021-05-27 10:04:52
* @LastEditors: dal
* @LastEditTime: 2021-09-16 17:38:31
-->
<template>
<div>
<img class="logo" :src="logo" />
<a-menu
theme="dark"
mode="inline"
v-model:openKeys="openKeys"
v-model:selectedKeys="selectedKeys"
@select="handleClick"
>
<template v-for="item in permission_routes">
<a-menu-item
v-if="hasOnlyChild(item) && !item.hidden"
:key="`${item.onlyOneChild.path}`"
>
<router-link :to="`${item.onlyOneChild.path}`">
<Icon
v-if="item.onlyOneChild.meta?.icon"
:icon="item.onlyOneChild.meta?.icon"
/>
<span>{{ item.onlyOneChild.meta.title }}</span>
</router-link>
</a-menu-item>
<sub-menu
v-else-if="!item.hidden"
:key="`${item.path}`"
:menu-info="item"
:parentPath="item.path"
/>
</template>
</a-menu>
</div>
</template>
<script lang="ts">
import { ref, defineComponent, reactive, onMounted, toRefs, watch } from "vue";
import { useRouter } from "vue-router";
import img from "../../assets/logo.png";
import { mapGetters } from "vuex";
import SubMenu from "./sub-menu.vue";
import { Icon } from "@/components/icon/icon";
interface STATE {
selectedKeys: Array<string | undefined>;
openKeys: Array<string | undefined>;
}
export default defineComponent({
components: {
SubMenu,
Icon,
},
computed: {
...mapGetters(["permission_routes"]),
},
setup() {
const logo = ref<string>(img);
let state: STATE = reactive({
selectedKeys: [],
openKeys: [""],
});
const router = useRouter().getRoutes();
watch(useRouter().currentRoute, (val) => {
state.selectedKeys = [val.path];
});
onMounted(() => {
let currentRoute = useRouter().currentRoute;
state.selectedKeys = [currentRoute.value.path];
state.openKeys = currentRoute.value.matched.map((v) => {
if (v.path !== currentRoute.value.path) {
return v.path;
}
});
});
return {
...toRefs(state),
logo: logo,
router: router,
};
},
methods: {
handleClick(e: any) {
console.log(e.keyPath);
this.selectedKeys = e.keyPath;
},
hasOnlyChild(item: any) {
if (!item.children || item.children.length == 1) {
item.onlyOneChild = item.children?.length > 0 ? item.children[0] : item;
return true;
} else {
return false;
}
},
},
});
</script>
<style lang="scss" scoped>
.logo {
max-width: 100px;
max-height: 40px;
display: block;
margin: 10px auto;
}
</style>
<!--
* @Descripttion: sub-menu 侧边导航的具体渲染,判断children是否为空去渲染子菜单组件
* @version:
* @Author: dal
* @Date: 2021-06-28 15:42:53
* @LastEditors: dal
* @LastEditTime: 2021-09-17 14:23:38
-->
<template>
<a-sub-menu :key="menuInfo.path" v-bind="$props" :title="menuInfo.meta.title">
<template v-for="item in menuInfo.children">
<a-menu-item
v-if="hasOnlyChild(item) && !item.hidden"
:key="`${item.onlyOneChild.path}`"
>
<router-link :to="`${item.onlyOneChild.path}`">
<Icon
v-if="item.onlyOneChild.meta?.icon"
:icon="item.onlyOneChild.meta?.icon"
/>
<span>{{ item.onlyOneChild.meta.title }}</span>
</router-link>
</a-menu-item>
<sub-menu
v-else-if="!item.hidden"
:key="`${item.path}`"
:menu-info="item"
:parentPath="`${item.path}`"
/>
</template>
</a-sub-menu>
</template>
<script lang="ts">
import { defineComponent, onMounted } from "vue";
import { Menu } from "ant-design-vue";
import { Icon } from "@/components/icon/icon";
export default defineComponent({
name: "SubMenu",
components: { Icon },
// must add isSubMenu: true
isSubMenu: true,
props: {
parentPath: {
type: String,
default: "",
},
...Menu.SubMenu.props,
// Cannot overlap with properties within Menu.SubMenu.props
menuInfo: {
type: Object,
default: () => ({}),
},
},
setup(props) {
function hasOnlyChild(item: any) {
if (!item.children || item.children?.length == 1) {
item.onlyOneChild = item.children?.length > 0 ? item.children[0] : item;
return true;
} else {
return false;
}
}
onMounted(() => {
console.log(props.menuInfo);
});
return {
menuInfo: props.menuInfo,
hasOnlyChild: hasOnlyChild,
};
},
});
</script>
<style>
</style>
/*
* 在aside-tabs/index.vue中。用vuex的mapGetters获取了permission_routes,
* 在store/index.js中我让permission_routes 导向了下面的这个模块的routes值
* @Descripttion: store/modules/permission.ts Vuex权限模块
* @version:
* @Author: dal
* @Date: 2021-06-02 14:28:13
* @LastEditors: dal
* @LastEditTime: 2021-09-17 15:03:50
*/
// import {asyncRoutes,constantRoutes} from "@/router"
import ROUTER, { constantRoutes } from "@/router"
import Layout from "@/layout/index.vue"
import { getPromission } from "@/api/user"
import { dealRouter } from "@/utils/auth"
/**
* Use meta.role to determine if the current user has permission
* @param roles
* @param route
*/
function hasPermission(roles: any[], route: { meta: { roles: string | any[] } }) {
if (route.meta && route.meta.roles) {
return roles.some((role: any) => route.meta.roles.includes(role))
} else {
return true
}
}
/**
* Filter asynchronous routing tables by recursion
* @param routes asyncRoutes
* @param roles
*/
export function filterAsyncRoutes(routes: any[], roles: any) {
const res: any[] = []
routes.forEach((route: any) => {
const tmp = { ...route }
if (hasPermission(roles, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, roles)
}
res.push(tmp)
}
})
return res
}
const state = {
routes: [],
addRoutes: []
}
const mutations = {
SET_ROUTES: ({ permission }: any, routes: any) => {
permission.addRoutes = routes
permission.routes = constantRoutes.concat(routes)
}
}
const actions = {
generateRoutes({ commit }: any, key: string) {
return new Promise(async resolve => {
let accessedRoutes
console.log(key)
await getPromission(key).then((res) => {
if (res.code === 20000) {
console.log(res)
accessedRoutes = dealRouter(res.result)
ROUTER.addRoute(
{
path: '',
component: Layout,
children: accessedRoutes
}
)
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
}
})
// if (roles.includes('admin')) {
// accessedRoutes = asyncRoutes || []
// } else {
// accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
// }
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
/* 这个文件只要是封装接口里返回的原始数组
* dealRouter将原始数组拼装成Menu组件能识别的数组类型
* component 主要针对多级菜单的时候要设置为空的,这里处理完router之后需要用vue-router的addRouter添加到路由中。
* @Descripttion: util/auth.ts
* @version:
* @Author: dal
* @Date: 2021-05-28 15:00:57
* @LastEditors: dal
* @LastEditTime: 2021-09-17 15:03:53
*/
import Cookies from "js-cookie"
import { router } from "@/interfaces"
const TokenKey = 'Admin-Token'
import { RouteRecordRaw } from "vue-router"
import { h, resolveComponent } from "vue";
export function getToken() {
return Cookies.get(TokenKey)
}
export function setToken(token: string) {
return Cookies.set(TokenKey, token)
}
export function removeToken() {
return Cookies.remove(TokenKey)
}
const modules = import.meta.glob('../views/**/**.vue');
console.log(modules)
export function dealRouter(routers: RouteRecordRaw[], node: router | null | undefined): RouteRecordRaw[] {
let routeRaw: RouteRecordRaw[] = []
routers.forEach((v: any) => {
if (node) {
if (v.meta.id === node.parent) {
v.children = v.children ? v.children : []
v.children = [...v.children, {
name: node.name,
path: node.path,
component: node.component ? modules[/* @vite-ignore */ `../views/${node.component}.vue`] : {
render() {
return h(resolveComponent('router-view'))
}
},
meta: { title: node.title, icon: node.icon, affix: false, id: node.id }
}]
v.redirect = v.children[0].path
} else if (v.children) {
v.children = dealRouter(v.children, node)
}
routeRaw = routers
} else if (v.parent) {
routeRaw = dealRouter(routeRaw, v)
} else {
let routeObj: RouteRecordRaw = {
name: v.name,
path: v.path,
meta: { title: v.title, icon: v.icon, affix: true, id: v.id },
component: v.component ? modules[/* @vite-ignore */ `../views/${v.component}.vue`] : {
render() {
return h(resolveComponent('router-view'))
}
}
}
routeRaw = [...routeRaw, routeObj]
}
})
return routeRaw
}
目录是模仿vue-element-admin写的。
最外层的permission.ts作为页面初始化的权限拦截器。
/*
* @Descripttion: permission.ts
* @version:
* @Author: dal
* @Date: 2021-06-02 15:42:10
* @LastEditors: dal
* @LastEditTime: 2021-09-17 15:14:07
*/
import router from './router'
import store from './store'
import { message } from 'ant-design-vue';
import { getToken } from '@/utils/auth' // get token from cookie
import getPageTitle from '@/utils/get-page-title'
const whiteList = ['/login'] // no redirect whitelist
router.beforeEach(async (to, from, next) => {
// set page title
document.title = getPageTitle(to.meta.title)
// determine whether the user has logged in
const hasToken = getToken()
if (hasToken) {
if (to.path === '/login') {
// if is logged in, redirect to the home page
next({ path: '/' })
} else {
// determine whether the user has obtained his permission roles through getInfo
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
next()
} else {
try {
// get user info
// note: roles must be a object array! such as: ['admin'] or ,['developer','editor']
const { key } = await store.dispatch('getInfo')
// generate accessible routes map based on roles
const accessRoutes = await store.dispatch('generateRoutes', key)
console.log("允许", accessRoutes)
// dynamically add accessible routes
router.addRoute(accessRoutes)
// hack method to ensure that addRoutes is complete
// set the replace: true, so the navigation will not leave a history record
next({ ...to, replace: true })
} catch (error: any) {
// remove token and go to login page to re-login
await store.dispatch('resetToken')
message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
}
}
}
} else {
/* has no token*/
if (whiteList.indexOf(to.path) !== -1) {
// in the free login whitelist, go directly
next()
} else {
// other pages that do not have permission to access are redirected to the login page.
next(`/login?redirect=${to.path}`)
}
}
})
router.afterEach(() => {
// finish progress bar
})
在main.ts中import './permission.ts'就可以了