vue3+vite+antd创建动态路由

问题点:

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'
},]

参考:

多级路由时,展开层路由的component不存在时的写法

/*
 * 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'就可以了

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值