vue3-10

动态路由与菜单
路由文件

a6router.ts

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;
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

    <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

<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 使用
  1. 获取用户信息,例如服务器端可以把用户名、该用户的路由、菜单信息都统一从 token 返回

  2. 前端路由跳转依据,例如跳转前检查 token,如果不存在,表示未登录,就避免跳转至某些路由

  3. 后端 API 访问依据,例如每次发请求携带 token,后端需要身份校验的 API 需要用到

  • 17
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

敲代码的翠花

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值