动态路由与菜单
路由文件
a6router.ts
TypeScript
import { createRouter, createWebHashHistory } from "vue-router";
import { useStorage } from "@vueuse/core";
import { Route, Menu } from "../model/Model8080";
const clientRoutes = [
{
path: "/login",
name: "login",
component: () => import("../views/A6Login.vue"),
},
{
path: "/404",
name: "404",
component: () => import("../views/A6NotFound.vue"),
},
{
path: "/",
name: "main",
component: () => import("../views/A6Main.vue"),
},
{
path: "/:pathMatcher(.*)*",
name: "remaining",
redirect: "/404",
},
];
const router = createRouter({
history: createWebHashHistory(),
routes: clientRoutes,
});
export const serverMenus = useStorage<Menu[]>("serverMenus", []);
const serverRoutes = useStorage<Route[]>("serverRoutes", []);
addServerRoutes(serverRoutes.value);
export function addServerRoutes(routeList: Route[]) {
for (const r of routeList) {
if (r.parentName) {
router.addRoute(r.parentName, {
path: r.path,
component: () => import(r.component),
name: r.name,
});
}
}
serverRoutes.value = routeList;
}
export function resetRoutes() {
for (const r of clientRoutes) {
router.addRoute(r);
}
serverRoutes.value = null;
serverMenus.value = null;
}
export default router;
TypeScript
export interface Route {
path: string;
component: string;
name: string;
parentName: string;
}
export interface Menu {
id: number;
pid: number;
title: string;
icon?: string;
routePath?: string;
routeComponent?: string;
routeName?: string;
routeParentName?: string;
children?: Menu[];
}
本文件重要的函数及变量
-
addServerRoutes 函数向路由表中添加由服务器提供的路由,路由分成两部分
-
clientRoutes 这是客户端固定的路由
-
serverRoutes 这是服务器变化的路由,存储于 localStorage
-
-
resetRoutes 函数用来将路由重置为 clientRoutes
-
vue-router@4 中的 addRoute 方法会【覆盖】同名路由,这是这种实现的关键
-
因此,服务器返回的路由最好是 main 的子路由,这样重置时就会比较简单,用之前的 main 一覆盖就完事了
-
-
serverMenus 变量记录服务器变化的菜单,存储于 localStorage
登录组件
动态路由应当在登录时生成,A6Login.vue
TypeScript
<template>
<div class="login">
<a-form :label-col="{ span: 6 }" autocomplete="off">
<a-form-item label="用户名" v-bind="validateInfos.username">
<a-input v-model:value="dto.username" />
</a-form-item>
<a-form-item label="密码" v-bind="validateInfos.password">
<a-input-password v-model:value="dto.password" />
</a-form-item>
<a-form-item :wrapper-col="{ offset: 6, span: 16 }">
<a-button type="primary" @click="onClick">Submit</a-button>
</a-form-item>
</a-form>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { Form } from 'ant-design-vue'
import { useRouter } from 'vue-router'
import axios from '../api/request'
import { useRequest } from 'vue-request'
import { AxiosRespToken, LoginDto, AxiosRespMenuAndRoute } from '../model/Model8080'
import { resetRoutes, addServerRoutes, serverMenus } from '../router/a6router'
const dto = ref({username:'', password:''})
const rules = ref({
username: [
{required: true, message:'用户名必填'}
],
password:[
{required: true, message:'密码必填'}
]
})
const { validateInfos, validate } = Form.useForm(dto, rules)
const router = useRouter()
const { runAsync:login } = useRequest<AxiosRespToken, LoginDto[]>((dto)=> axios.post('/api/loginJwt', dto), {manual:true})
const { runAsync:menu } = useRequest<AxiosRespMenuAndRoute, string[]>((username)=> axios.get(`/api/menu/${username}`), {manual:true})
async function onClick() {
try {
await validate()
const loginResp = await login(dto.value
if(loginResp.data.code === 200) { // 登录成功
const token = loginResp.data.data.token
const menuResp = await menu(dto.value.username)
const routeList = menuResp.data.data.routeList
addServerRoutes(routeList)
serverMenus.value = menuResp.data.data.menuTree
router.push('/')
})
} catch (e) {
console.error(e)
}
}
onMounted(()=>{
resetRoutes()
})
</script>
<style scoped>
.login {
margin: 200px auto;
width: 25%;
padding: 20px;
height: 180px;
background-color: antiquewhite;
}
</style>
-
登录成功后去请求
/api/menu/{username}
获取该用户的菜单和路由 -
router.push 方法用来以编程方式跳转至主页路由
主页组件
A6Main.vue
TypeScript
<template>
<div class="a6main">
<a-layout>
<a-layout-header> </a-layout-header>
<a-layout>
<a-layout-sider>
<a-menu mode="inline" theme="dark">
<template v-for="m1 of serverMenus">
<a-sub-menu v-if="m1.children" :key="m1.id" :title="m1.title">
<template #icon><a-icon :icon="m1.icon"></a-icon></template>
<a-menu-item v-for="m2 of m1.children" :key="m2.id">
<template #icon><a-icon :icon="m2.icon"></a-icon></template>
<router-link v-if="m2.routePath" :to="m2.routePath">{{
m2.title
}}</router-link>
<span v-else>{{ m2.title }}</span>
</a-menu-item>
</a-sub-menu>
<a-menu-item v-else :key="m1.id">
<template #icon><a-icon :icon="m1.icon"></a-icon></template>
<router-link v-if="m1.routePath" :to="m1.routePath">{{
m1.title
}}</router-link>
<span v-else>{{ m1.title }}</span>
</a-menu-item>
</template>
</a-menu>
</a-layout-sider>
<a-layout-content>
<router-view></router-view>
</a-layout-content>
</a-layout>
</a-layout>
</div>
</template>
<script setup lang="ts">
import AIcon from "../components/AIcon3"; // jsx icon 组件
import { serverMenus } from "../router/a6router";
</script>
<style scoped>
.a6main {
height: 100%;
background-color: rgb(220, 225, 255);
box-sizing: border-box;
}
.ant-layout-header {
height: 50px;
background-color: darkseagreen;
}
.ant-layout-sider {
background-color: lightsalmon;
}
.ant-layout-content {
background-color: aliceblue;
}
.ant-layout-footer {
background-color: darkslateblue;
height: 30px;
}
.ant-layout {
height: 100%;
}
.ant-layout-has-sider {
height: calc(100% - 50px);
}
</style>
token 使用
-
获取用户信息,例如服务器端可以把用户名、该用户的路由、菜单信息都统一从 token 返回
-
前端路由跳转依据,例如跳转前检查 token,如果不存在,表示未登录,就避免跳转至某些路由
-
后端 API 访问依据,例如每次发请求携带 token,后端需要身份校验的 API 需要用到