对长期纯写前端业务的人来说是处处踩坑的一天。
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>