搭建日记(三):实现动态路由

对长期纯写前端业务的人来说是处处踩坑的一天。

server端操作:建表、获得路由接口

建立router表,没有结构合理不合理,纯粹能用就行
// 建立router表 models/route
const mongoose = require('mongoose')
const schema = new mongoose.Schema({
    // 菜单标题 访问地址 组件地址 权限角色 上级菜单
    meta:{type:Object,required: true},
    path:{type:String,required: true},
    component:{type:String,required: true},
    role:{type:Array,required: true},
    // 管理员权限
    admin:{type:Array},
    // 主管权限
    manager:{type:Array},
    // 专员权限
    user:{type:Array},
    // 上级菜单路由ID
    parentId:{type:String},
    /*parentId: { type: mongoose.SchemaTypes.ObjectId, ref: 'route' },*/
})

module.exports = mongoose.model('route',schema)

// 数据样例
 {
    _id: new ObjectId("64a9653f4a1bcf09157e6cf1"),
    meta: { title: 'C端配置' },
    path: 'c-config',
    component: 'router-view',
    role: [ 'admin', 'manager' ],
    admin: [ 'role_view', 'role_add', 'role_update' ],
    manager: [ 'role_view' ],
    user: [],
    parentId: null,
    __v: 0
  }
router/admin/index 接口文件改造
// 通用接口判定是否是路由,整理成前端需要的格式树格式
 router.get('/', async (req, res) => {
        const items = await req.Model.find().limit(100)
        if (req.Model.modelName === 'route') {
            console.log('items',items)
            const dataList = JSON.parse(JSON.stringify(items))
            const parentList = []
            dataList.map(item => {
                item.title = item.meta.title
                if(item.parentId === null){
                    const parentId = JSON.stringify(item._id).replace('"', '').replace('"', '')
                    const children = []
                    dataList.filter( v => {
                        if(v.parentId === parentId){
                            children.push(v)
                        }
                    })
                    item.children = children
                    parentList.push(item)
                }
            })
            res.send(parentList)
        }else {
            res.send(items)
        }
    })
新加一个根据用户权限获得路由的接口,按理说权限划分可以细致到角色-用户,但是决定算了
 // 获得用户路由列表
    const route = require('../../models/route')
    app.get('/admin/api/user-router/:role', async (req, res) => {
        const model = await route.find({ role : { $in : [ req.params.role]}})
        const dataList = JSON.parse(JSON.stringify(model))
        const parentList = []
        dataList.map(item => {
            item.title = item.meta.title
            item.action = item[req.params.role]
            if(!item.parentId){
                const parentId = JSON.stringify(item._id).replace('"', '').replace('"', '')
                const children = []
                dataList.filter( v => {
                    if(v.parentId === parentId){
                        children.push(v)
                    }
                })
                item.children = children
                parentList.push(item)
            }
        })
        res.send(parentList)
    })

admin端

新增路由编辑页route.vue
<template>
  <div class="route-page-box">
    <el-tree class="route-page-box-tree" :data="dataTree" :props="defaultProps" @node-click="handleNodeClick" :render-content="renderContent"></el-tree>
    <el-form class="route-page-box-form" ref="routeForm" :model="routeForm" @submit.native.prevent="save()">
      <el-form-item label="菜单名称" prop="meta.title">
        <el-input v-model="routeForm.meta.title" class="f-input-m"></el-input>
      </el-form-item>
      <el-form-item label="访问路径" prop="path">
        <el-input v-model="routeForm.path" class="f-input-m"></el-input>
      </el-form-item>
      <el-form-item label="组件地址" prop="component">
        <el-input v-model="routeForm.component" class="f-input-m"></el-input>
      </el-form-item>
      <el-form-item label="上级菜单">
        <el-select class="f-input-m" v-model="routeForm.parentId" placeholder="请选择上级菜单">
          <el-option
              v-for="(pa,p) in parentList"
              :key="p"
              :label="pa.meta.title"
              :value="pa._id">
          </el-option>
        </el-select>
      </el-form-item>
      <el-form-item label="权限角色">
        <el-checkbox-group v-model="routeForm.role">
          <el-checkbox label="admin" name="admin">系统管理员</el-checkbox>
          <el-checkbox label="manager" name="manager">部门管理</el-checkbox>
          <el-checkbox label="user" name="user">部门专员</el-checkbox>
        </el-checkbox-group>
      </el-form-item>
      <el-form-item v-if="routeForm.role.includes('admin')" label="系统管理员权限">
        <el-select class="f-input-m" multiple  v-model="routeForm.admin" placeholder="请选择操作权限">
          <el-option label="新增" value="role_add"></el-option>
          <el-option label="审核" value="role_audit"></el-option>
          <el-option label="查看" value="role_view"></el-option>
          <el-option label="删除" value="role_delete"></el-option>
          <el-option label="编辑" value="role_update"></el-option>
        </el-select>
      </el-form-item>
      <el-form-item v-if="routeForm.role.includes('manager')" label="部门管理权限">
        <el-select class="f-input-m" multiple v-model="routeForm.manager" placeholder="请选择操作权限">
          <el-option label="新增" value="role_add"></el-option>
          <el-option label="审核" value="role_audit"></el-option>
          <el-option label="查看" value="role_view"></el-option>
          <el-option label="删除" value="role_delete"></el-option>
          <el-option label="编辑" value="role_update"></el-option>
        </el-select>
      </el-form-item>
      <el-form-item v-if="routeForm.role.includes('user')" label="专员权限">
        <el-select class="f-input-m" multiple  v-model="routeForm.user" placeholder="请选择操作权限">
          <el-option label="新增" value="role_add"></el-option>
          <el-option label="审核" value="role_audit"></el-option>
          <el-option label="查看" value="role_view"></el-option>
          <el-option label="删除" value="role_delete"></el-option>
          <el-option label="编辑" value="role_update"></el-option>
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" native-type="submit">保存</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
export default {
  data() {
    return {
      listQuery:{
        limit:10
      },
      dataTree: [],
      parentList:[],
      currentId:null,
      routeForm:{
        path:undefined,
        meta:{
          title:undefined,
        },
        role:[],
        admin:[],
        user:[],
        manager:[],
        component:null,
        parentId:null
      },
      defaultProps: {
        children: 'children',
        label: 'title'
      }
    }
  },
  created() {
    this.init()
  },
  methods: {
    init(){
      this.currentId = null
      this.resetForm()
     this.$nextTick( () => {
       this.$refs.routeForm.resetFields()
     })
      this.$http.get("/rest/route").then(res => {
        this.dataTree = res
        this.parentList = res
      })
    },
    resetForm(){
      this.routeForm = {
        path:undefined,
        meta:{
          title:undefined,
        },
        role:[],
        admin:[],
        user:[],
        manager:[],
        component:null,
        parentId:null
      }
    },
    renderContent(h, { node, data, store }) {
      return (
          <div class="custom-tree-node">
            <span>{node.label}</span>
            <span>
              <el-button size="mini" type="text" on-click={ () => this.append(data) }>新增</el-button>
              <el-button size="mini" type="text" on-click={ () => this.remove(node, data) }>删除</el-button>
            </span>
          </div>)
    },
    append(data) {
      this.$set(this.routeForm,'parentId',data._id)
    },

    remove(node, data) {
      this.$confirm(`是否确定要删除 "${data.title}"`, "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
        }).then(async () => {
          const res = await this.$http.delete(`/rest/route/${data._id}`);
          this.$message({
          type: "success",
          message: "删除成功!"
        });
          this.init()
        })
      },
    async save(){
      let res
      if (this.currentId) {
        res = await this.$http.put(`/rest/route/${this.currentId}`, this.routeForm)
      } else {
        res = await this.$http.post('/rest/route', this.routeForm)
      }
      this.init()
      this.$message({
        type: 'success',
        message: '保存成功'
      })
    },
    async handleNodeClick(data) {
      this.currentId = data._id
      const res = await this.$http.get(`/rest/route/${this.currentId}`)
      this.routeForm = res
    }
  }
}
</script>

<style lang="scss">

</style>

<style>
.route-page-box{
  display: flex;
}
.route-page-box-tree{
  flex: 1;
  border: 1px solid #ddd;
  padding: 10px 0;
}
.route-page-box-form{
  flex: 1;
  margin-left: 50px;
}
.custom-tree-node{
  width: 100%;
  display: flex;
  justify-content: space-between;
  padding-right: 20px;
}
</style>
路由文件修改router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'

// 页面引入
import Main from '../views/Main'

Vue.use(VueRouter)

export const defaultRoutes = [
  {
    path: '/Login',
    name:'Login',
    component: () => import('@/views/Login'),
    meta: { isPublic: true,title:'登录' }
  }
]

export const asyncRoutes = [
  {
    path: '/',
    name: 'Main',
    component: Main,
    meta: { title:'系统管理' },
    children:[
      {
        path:'/route',
        meta: { title:'路由管理' },
        component: () => import('@/views/route')
      },
      {
        path:'/user/list',
        meta: { title:'用户管理' },
        component: () => import('@/views/user/List')
      }
    ]
  },
]

const createRouter = () =>  new VueRouter({
  base: process.env.BASE_URL,
  scrollBehavior : () => ({ y:0 }),
  routes:defaultRoutes
})

const router = createRouter()

export function resetRouter() {
  const newRouter = createRouter()
  router.matcher = newRouter.matcher // reset router
}

export default router
前端用户信息改成状态管理,store-modules-user,再加一个路由管理router
// user.js
import router, { resetRouter } from '@/router'
import Vue from "vue";
/* localStorage信息 获取 新增 删除 */
export function getToken(){
    return localStorage.getItem('token')
}
export function getUsername(){
    return localStorage.getItem('username')
}
export function getRole(){
    return localStorage.getItem('role')
}
export function setLocal(token, username, role) {
    localStorage.setItem('role', role)
    localStorage.setItem('username', username)
    localStorage.setItem('token', token)
}
export function removeLocal() {
    localStorage.removeItem('role')
    localStorage.removeItem('username')
    localStorage.removeItem('token')
}
const state = {
    token: undefined,
    role:undefined,
    username:undefined
}

const mutations = {
    SET_TOKEN: (state, token) => {
        state.token = token
    },
    SET_USERNAME: (state, username) => {
        state.username = username
    },
    SET_ROLE: (state, role) => {
        state.role = role
    }
}

const actions = {
   login({commit},logonInfo) {
        return new Promise((resolve, reject) => {
            Vue.prototype.$http.post('login', logonInfo).then(res => {
                // 获得用户信息
                const { role, username, token } = res.data
                commit('SET_ROLE', role)
                commit('SET_USERNAME', username)
                commit('SET_TOKEN', token)
                setLocal(token, username, role)
            }).catch( err => {
                reject(err)
            })
        })
    },
    // 刷新路由
    async changeRoutes({ dispatch }) {
        resetRouter()
        // 根据权限获得路由表
        const accessRoutes = await dispatch('route/getRoutes', getRole, {
            root: true
        })
        router.addRoute(accessRoutes)
    }
}

export default {
    namespaced: true,
    state,
    mutations,
    actions
}

// router.js

import  { asyncRoutes, defaultRoutes  } from '@/router'
import Vue from 'vue'

// 遍历后台传来的路由字符串,转换为组件对象
function filterAsyncRoutes(asyncRouterMap) {
    const res = []
    asyncRouterMap.forEach(route => {
        route.meta = route.meta || {}
        route.meta.actions = route.actions
        route.meta.noCache = !route.meta.keepAlive
        if (route.component) {
            route.component = loadView(route.component)
        }
        if (route.children != null && route.children && route.children.length > 0) {
            route.children = filterAsyncRoutes(route.children)
        }
        res.push(route)
    })

    return res
}
// 路由懒加载
export const loadView = (view) => {
    // 请务必检查组件地址是否正确
    return (resolve) => require([`@/views/${view}`], resolve)
}

const state = {
    routes: [],
    addRoutes: []
}

const mutations = {
    SET_ROUTES: (state, routes) => {
        state.addRoutes = routes
        state.routes = defaultRoutes.concat(routes)
    }
}

const actions = {
    getRoutes({ commit },userRole) {
        return new Promise((resolve, reject) => {
            Vue.prototype.$http.get(`/user-router/${userRole}`).then( res => {
                const accessedRoutes = filterAsyncRoutes(res)
                const routes = asyncRoutes.concat(accessedRoutes)
                console.log('routes',routes)
                commit('SET_ROUTES', routes)
                resolve(routes)
            }).catch(error => {
                reject(error)
            })
        })
    }
}

export default {
    namespaced: true,
    state,
    mutations,
    actions
}
新增路由权限判定文件 permission.js
/* 路由权限判定 */
import router from './router'
import store from './store'
import { getToken ,getRole} from "@/store/modules/user"

/* 设置路由白名单 */
const whiteList = ['/Login']
router.beforeEach( async (to,from,next) => {
    /* 判定token */
    const hasToken = getToken()
    if(hasToken){
        if (to.path === '/Login') {
            next({ path: '/' })
        }
        /* 判定是本地是否有后端路由 */
        const isRouter = store.getters.permission_routes
        if(isRouter.length === 0){
            // 重新获取 生成菜单路由
            const accessRoutes = await store.dispatch('router/getRoutes',getRole())
            for(let i = 0 ;i<accessRoutes.length; i +=1){
                const elemeent = accessRoutes[i]
                router.addRoute(elemeent)
            }
            next({ ...to, replace: true })
        }else {
            next()
        }
    }else {
        if (whiteList.indexOf(to.path) !== -1) {
            next()
        } else {
            next(`/Login}`)
        }
    }
})

到这里进行了路由CRUD的操作,新增后刷新页面查看后台的路由数组是否正确,通过router.js文件处理的数据是否可用,确认数据可以用之后开始改造Main文件,把右侧菜单做了封装这一块折腾了很久,要么路径不对,要么配的时候顶级菜单组件地址没有写到Main,总之是被不牢固的基础狠狠地教育了一波;

新建Layout文件夹->Sidebar目录新建index和SideItem

path.resolve 会报错,折腾了一下

// index.js
<template>
  <el-menu background-color="#49586D" text-color="#EFF3F6" active-text-color="#2CA9E1" :unique-opened="false"
           :collapse-transition="false" :default-active="activePath" router mode="vertical">
    <!-- SideItem组件 -->
    <side-item
        v-for="(route,index) in permission_routes.splice(1,permission_routes.length)"
        :key="index"
        :route="route"
        :basePath="route.path"
    />
  </el-menu>
</template>

<script>
import {mapGetters} from 'vuex'
import SideItem from './SideItem.vue'

export default {
  name: 'SideBar',
  components: { SideItem },
  data() {
    return {
      activePath: ''
    }
  },
  created() {
    this.activePath = this.$route.path
  },
  computed: {
    // 获取所有路由
    ...mapGetters([
      'permission_routes'
    ]),

  }
};
</script>

//SideItem.js
<template>
  <div v-if="!route.hidden">
    <!-- 没有子菜单 -->
    <el-menu-item v-if="!route.children" :index="basePath">
      <i :class="route.meta.icon"></i>
      <span slot="title">{{route.meta.title}}</span>
    </el-menu-item>

    <!-- 有子菜单, 且显示根菜单 -->
    <el-submenu v-else :index="basePath">
      <template slot="title">
        <i :class="route.meta.icon"></i>
        <span slot="title">{{route.meta.title}}</span>
      </template>
      <!-- SideItem组件递归 -->
      <side-item
          v-for="(child,index) in route.children"
          :key="index"
          :route="child"
          :basePath="resolvePath(child.path)"
      />
    </el-submenu>

  </div>
</template>

<script>
import path from 'path'

export default {
  name: 'SideItem',
  props: {
    route: {
      type: Object,
      required: true
    },
    basePath: {
      type: String,
      default: ''
    }
  },
  methods: {
    // 路径拼接
    resolvePath(routePath) {
     return path.resolve(this.basePath, routePath)
    }
  },

};
加了一个PageMain,vue组件用于显示内容
<template>
  <section class="main-box">
    <transition name="fade-transform" mode="out-in">
      <keep-alive>
        <router-view :key="key" />
      </keep-alive>
    </transition>
  </section>
</template>
<script>
export default {
name: "PageMain",
  computed: {
    key() {
      return this.$route.path
    }
  }
}
</script>

<style scoped>
.main-box{
  min-height: calc(100vh - 60px);
  width: 100%;
  position: relative;
  overflow: hidden;
  padding: 10px 10px 0 10px;
}
</style>
Main,vue文件 - 调了一下布局
<template>
  <div class="page-container">
    <div class="page-container-left">
      <sidebar></sidebar>
    </div>
    <div class="page-container-right">
      <el-header class="page-container-right-header">
        <el-dropdown>
          <i class="el-icon-setting" style="margin-right: 15px"></i>
          <el-dropdown-menu slot="dropdown">
            <el-dropdown-item>查看</el-dropdown-item>
            <el-dropdown-item>新增</el-dropdown-item>
            <el-dropdown-item>删除</el-dropdown-item>
          </el-dropdown-menu>
        </el-dropdown>
        <span>123</span>
      </el-header>
     <page-main></page-main>
    </div>
  </div>
</template>


<style>
.page-container{
  width: 100vw;
  height: 100vh;
  display: flex;
}
.page-container-left{
  width: 200px;
  height: 100%;
  background: #49586D;
}
.page-container-right{
  width: calc(100% - 200px );
  height: 100%;
}
.page-container-right-header{
  background: #F2F6FC;
  color: #000;
  display: flex;
  justify-content: flex-end;
  align-items: center;
  font-size: 16px;
  font-weight: 600;
  letter-spacing: 2px;
}
</style>

<script>
import PageMain from '@/Layout/PageMain'
import Sidebar from '@/Layout/Sidebar'
export default {
  components:{
    Sidebar,
    PageMain
  },
  computed: {
  },
  data() {
    return {}
  },
  methods:{
  }
};
</script>

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值