1 环境准备

后端代码:https://blog.csdn.net/qq_45660133/article/details/128498518

1.1 安装Node.js

官网下载地址 http://nodejs.cn/download,如图所示:
在这里插入图片描述

  1. 安装 Node.js 淘宝镜像加速器( npm )
npm install cnpm -g
# 或使用如下语句解决 npm 速度慢的问题
npm install --registry=https://registry.npm.taobao.org
// 测试是否安装成功
node -v npm -v
  1. 安装 vue-cli
// 最新版本
npm install -g @vue/cli
// 稳定版本
npm install -g @vue/cli@4.5.12
// 测试是否安装成功
vue --version
// 卸载
npm uninstall -g @vue/cli

在这里插入图片描述

  1. 安装Webpack: js打包即压缩(可以忽略)
// 2个安装方式你自己选
npm install webpack -g
npm install webpack-cli -g
// 测试是否安装成功
webpack -v

1.2 创建项目

cmd d:\javaidea\admin-vue\admin-ui    创建一个文件夹放vue项目(admin-ui项目名)

vue create admin-ui     创建项目

根据下面图片选择配置:空格是选择,回车是确认!!
在这里插入图片描述

cd admin-ui         进入刚刚创建的项目

npm run serve   启动项目

2 导入插件

2.1 Element-UI 插件

// element-ui
npm install --save element-ui
// avue
npm i @smallwei/avue -S

在main.js 里面引用element-ui 组件

// 引用 element-ui 以及样式
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
// 引用 Avue 以及样式
import Avue from '@smallwei/avue';

// 安装ElementUI 配置全局
Vue.use(ElementUI, {size: 'small'});
// 安装Avue 配置全局
Vue.use(Avue, {size: 'small'});

2.2 Axios

npm install --save axios
  1. 创建utils文件夹 request.js
import axios from 'axios'

//请求超时时间
axios.defaults.timeout = 10000;
//设置请求头以json格式发送到后端
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
    
export default axios

  1. 创建 api 文件夹login.js,路径:api\user\login.js

data为post请求的携带信息
params是get请求携带的参数

import request from '@/utils/request'

//登录
export function login(data) {
    return request({
        url: '/login',
        method: 'post',
        data: data
    })
}

//注册
export function register(data) {
    return request({
        url: '/auth/register',
        method: 'post',
        data: data
    })
}

export function fetchList(query) {
    return request({
        url: '/admin/user/page',
        method: 'get',
        params: query
    })
}

export function addObj(obj) {
    return request({
        url: '/admin/user',
        method: 'post',
        data: obj
    })
}

export function getMenu() {
    return request({
        url: '/sysMenu/getMenu',
        method: 'get'
    })
}

export function getList(obj) {
  return request({
    url: '/proj/projPlan/getList',
    method: 'get',
    params: obj
  })
}

使用

<script>
import {getMenu} from "@/api/login";
export default {
  created() {
    console.log(this.$router.options.routes);
    getMenu().then(resp => {
        console.log(resp)
    })
    //初始化菜单激活状态
    this.activePath = window.sessionStorage.getItem("activePath");
  },
  methods: {
    // get 用法一
    var params = {"planId": this.planId,"current": page.currentPage,"size": page.pageSize};
    getList(params).then(response => {
      let data = response.data.data.records
      this.tableData = data
      this.page.total = response.data.data.total
      this.tableLoading = false
    }).catch(() => {
      this.tableLoading = false
    }),
    // get 用法二
    getList(Object.assign({
        current: this.page.currentPage,
        size: this.page.pageSize
     }, {
        "planStatus": 0,
        "planId": this.editableTabs[Number(this.editableTabsValue)].planId,
        "deptId": this.deptId
     }, this.searchForm)).then(response => {
        this.tableData = response.data.data.records
        this.page.total = response.data.data.total
        this.tableLoading = false
     }).catch(() => {
        this.tableLoading = false
     }),
    // post 用法
    addObj(row).then(res => {
      this.$message.success('添加成功')
      this.getList(this.page)
    }).catch(() => {
      loading();
    });
  }
};
</script>

2.3 Vuex 状态管理模式

注意:vuex分3.x版本和4.x版本,分别对应vue2.0与3.0,也就是说,vue2.0只能安装vuex3.x版本,最高3.6.2,vue3.0才能装vuex4.x版本。

// 最新版本
npm install vuex --save
// 推荐指定版本号
npm install -g vuex@3.4.0

创建store文件

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex);

export default new Vuex.Store({
    state: {},
    //同步执行操作
    mutations: {},
    //异步执行操作
    actions: {},
    modules: {}
})
import store from './store'

new Vue({
  router,
  store,//引用
  render: h => h(App)
}).$mount('#app');

2.4 Scss

// 安装 sass-loader
npm i -D sass-loader@8.x 
// 安装 node-sass
npm i node-sass@4.14.1 
npm audit fix
npm audit fix --force
npm install

错误解决:
在这里插入图片描述

检查代码中并无写错的地方
其实涉及到这个问题,就是版本原因了,我安装的 scss-loader 版本太高,卸载安装低版本即可
卸载:npm uninstall 名字比如:sass

npm uninstall --save sass-loader // 卸载
npm uninstall --save node-sass // 卸载

2.5 图标

我们使用了 Font Awesome 的图标做为菜单图标,使用前先安装 Font Awesome

npm install font-awesome

导入 Font Awesome (main.js)

// 图标
import 'font-awesome/css/font-awesome.min.css'

3 配置全局响应拦截器(业务逻辑错误)

创建utils文件夹,路径:utils/request.js

import axios from 'axios'
import { Message } from 'element-ui'
import router from '../router'

//请求超时时间
axios.defaults.timeout = 10000;
//设置请求头以json格式发送到后端
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'


//响应拦截器
axios.interceptors.response.use(success => {
  //业务逻辑错误
  if (success.status && success.status == 200) {
    //500 业务逻辑错误,401 未登录,403 权限错误
    if (success.data.code == 500 || success.data.code == 401 || success.data.code == 403) {
      Message.error({ message: success.data.msg });
      return;
    }
    if (success.data.message) {
      Message.success({ message: success.data.msg });
    }
  }
  return success.data;
}, error => {
  //504 服务器有问题,404 页面找不到
  if (error.response.code == 504 || error.response.code == 404) {
    Message.error({ message: '服务器没有了' });
  } else if (error.response.code == 403) {
    Message.error({ message: '权限不足,请联系管理员!' })
  } else if (error.response.code == 401) {
    Message.error({ message: '尚未登录,请登录' })
    router.replace('/');
  } else {
    if (error.response.data.message) {
      Message.error({ message: error.response.data.msg });
    } else {
      Message.error({ message: '未知错误!' });
    }
  }
  return;
});

export default axios

4 登录页面

4.1 配置跨越

创建vue.config.js文件
在这里插入图片描述

let proxyObj = {}//代理对象

proxyObj['/'] = {//代理路径,这里的api 表示如果我们的请求地址有/api的时候,就出触发代理机制
    //websocket
    ws: true,
    //目标地址
    target: 'http://localhost:8080',
    // target: 'http://47.115.143.129:8080',
    //发送请求头中host会设置成target
    changeOrigin: true,// 开启跨域
    //不重写请求地址
    pathReWrite:{
        // 重新路由  localhost:8888/api/login  => www.baidu.com/api/login
        '^/': '/'// 假设我们想把 localhost:8888/api/login 变成www.baidu.com/login 就需要这么做 
    }
};

module.exports = {
    assetsDir: 'static', // 静态资源保存路径
    outputDir: 'admin-shiro-ui', // 打包后生成的文件夹
    lintOnSave: false,
    productionSourceMap: false, // 取消错误日志
    runtimeCompiler: true, // 实时编译
    devServer: {
        open: true,
        host: 'localhost',
        port: 80,
        proxy: proxyObj //代理
    }
};

4.2 配置路由器

image.png

import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '../views/Login.vue'
import Register from '../views/Register.vue'
import Home from '../views/Home.vue'
import Console from '../views/console/index'
// 引用 store 状态管理模式
import store from '../store';

Vue.use(VueRouter)

// 判断 session 缓存中是否存在 token
if (window.sessionStorage.getItem("token")){
    // 设置到 store 状态管理器中
    store.commit("set_token", window.sessionStorage.getItem("token"))
}

const routes = [
    {
        path: '/',
        name: 'Login',
        component: Login,
        hidden: true//隐藏路由
    },
    {
        path: '/register',
        name: '注册',
        component: Register,
        hidden: true//隐藏路由
    },
    {
        path: '/home',//路径
        name: '控制台',//名字
        redirect: 'console',//重定向路由
        component: Home,//文件地址
        hidden: true,//隐藏路由
        children: [//子级菜单
            {
                path: '/console',
                name: '控制台',
                component: Console
            }
        ]
    }
]

const router = new VueRouter({
    routes
})

export default router

4.3 封装 get、post 请求后端接口

image.png

import request from '@/utils/request'

//登录
export function login(data) {
    return request({
        url: '/login',
        method: 'post',
        data: data
    })
}

//注销登录
export function logout(query) {
    return request({
        url: '/logout',
        method: 'get',
        params: query
    })
}

4.4 在 store 中设置 token

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex);

export default new Vuex.Store({
    state: {
        token: ''
    },
    // 同步执行操作
    mutations: {
        //获取token
        set_token(state, token){
            //设置 token
            state.token = token;
            //设置 token 到 session 缓存里面
            window.sessionStorage.token = token
        }
    },
    //异步执行操作
    actions: {},
    modules: {}
})

4.5 将 token 设置到 Authorization 请求头中

添加请求拦截器,在发送请求前的操作,判断是否存在token,如果存在将每个页面header添加token

import axios from 'axios'
import { Message } from 'element-ui'
import router from '../router'
// 引用 store 状态管理模式
import store from '../store';

//请求超时时间
axios.defaults.timeout = 10000;
//设置请求头以json格式发送到后端
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8';

// 添加请求拦截器
axios.interceptors.request.use(function (config) {
    // 在发送请求前的操作
    // 判断是否存在token,如果存在将每个页面header添加token
    if (store.state.token) {
        config.headers['Authorization'] = store.state.token
    }
    return config
}, function (error) {
    //发生错误跳转到登录页面
    router.push('/login')
    return Promise.reject(error)
})

//响应拦截器
axios.interceptors.response.use(success => {
    console.log("success状态码:",success)
    //业务逻辑错误
    if (success.status && success.status == 200) {
        //500 业务逻辑错误,401 未登录,403 权限错误
        if (success.data.code == 500 || success.data.code == 401 || success.data.code == 403) {
            Message.error({ message: success.data.message });
            return;
        }
        if (success.data.message) {
            Message.success({ message: success.data.message });
        }
    }
    return success.data;
}, error => {
    console.log("error状态码:",error)
    //504 服务器有问题,404 页面找不到
    if (error.response.code == 504 || error.response.data.code == 404) {
        Message.error({ message: '服务器没有了' });
    } else if (error.response.code == 403) {
        Message.error({ message: '权限不足,请联系管理员!' })
    } else if (error.response.code == 401) {
        Message.error({ message: '尚未登录,请登录' })
        router.replace('/');
    } else {
        if (error.response.data.message) {
            Message.error({ message: error.response.data.message });
        } else {
            Message.error({ message: '未知错误!' });
        }
    }
    return;
});

export default axios

4.6 创建Login.vue

<template>
  <div class="login">
    <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
      <h3 class="title">后台管理系统</h3>
      <el-form-item prop="userName">
        <el-input v-model="loginForm.userName"
                  auto-complete="false"
                  placeholder="账号"
                  type="text">
        </el-input>
      </el-form-item>
      <el-form-item prop="password">
        <el-input v-model="loginForm.password"
                  auto-complete="false"
                  placeholder="密码"
                  type="password">
        </el-input>
      </el-form-item>
      <el-form-item v-if="captchaOnOff" prop="code">
        <el-input v-model="loginForm.code"
                  auto-complete="false"
                  placeholder="验证码"
                  style="width: 63%">
        </el-input>
        <div class="login-code">
          <img :src="codeUrl" class="login-code-img"/>
        </div>
      </el-form-item>
      <el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
      <el-form-item style="width:100%;">
        <el-button :loading="loading" size="medium" style="width:100%;" type="primary" @click="submitLogin">
          <span v-if="!loading">登 录</span>
          <span v-else>登 录 中...</span>
        </el-button>
        <div v-if="register" style="float: right;">
          <router-link :to="'/register'" class="link-type">立即注册</router-link>
        </div>
      </el-form-item>
    </el-form>
    <!--  底部  -->
    <div class="el-login-footer">
      <span>Copyright © 2018-2022 admin All Rights Reserved.</span>
    </div>
  </div>
</template>

<script>
import {login} from '@/api/user/login'

export default {
  name: "Login",
  data() {
    return {
      loginForm: {
        userName: "admin",
        password: "123456",
        rememberMe: false,
        code: ""
      },
      loginRules: {//错误提示信息
        userName: [{required: true, trigger: "blur", message: "请输入您的账号"}],
        password: [{required: true, trigger: "blur", message: "请输入您的密码"}],
        code: [{required: true, trigger: "change", message: "请输入验证码"}]
      },
      loading: false,
      // 验证码开关
      captchaOnOff: true,
      // 注册开关
      register: false
    };
  },
  methods: {
    
  }
};
</script>

<style lang="scss" rel="stylesheet/scss">
.login {
  display: flex;
  position: fixed;
  justify-content: center;
  align-items: center;
  height: 100%;
  width: 100%;
  background-image: url("../assets/images/login-background.jpg");
  background-size: cover;
}

.title {
  margin: 0px auto 30px auto;
  text-align: center;
  color: #707070;
}

.login-form {
  border-radius: 6px;
  background: #ffffff;
  width: 400px;
  padding: 25px 25px 5px 25px;

  .el-input {
    height: 38px;

    input {
      height: 38px;
    }
  }

  .input-icon {
    height: 39px;
    width: 14px;
    margin-left: 2px;
  }
}

.login-tip {
  font-size: 13px;
  text-align: center;
  color: #bfbfbf;
}

.login-code {
  width: 33%;
  height: 38px;
  float: right;

  img {
    cursor: pointer;
    vertical-align: middle;
  }
}

.el-login-footer {
  height: 40px;
  line-height: 40px;
  position: fixed;
  bottom: 0;
  width: 100%;
  text-align: center;
  color: #fff;
  font-family: Arial;
  font-size: 12px;
  letter-spacing: 1px;
}

.login-code-img {
  height: 38px;
}
</style>

4.7 验证码

<el-form-item prop="code" v-if="captchaOnOff">
  <el-input v-model="loginForm.code"
            auto-complete="false"
            placeholder="验证码"
            style="width: 63%">
  </el-input>
  <div class="login-code">
    <img :src="codeUrl" @click="updateCaptcha" class="login-code-img"/>
  </div>
</el-form-item>
//获取验证码
codeUrl: '/captcha?time=' + new Date(),

如图所示:
在这里插入图片描述

4.8 登录

<el-form-item style="width:100%;">
  <el-button 
             :loading="loading" 
             size="medium" 
             type="primary" 
             style="width:100%;" 
             @click="submitLogin">
    <span v-if="!loading">登 录</span>
    <span v-else>登 录 中...</span>
  </el-button>
  <div style="float: right;" v-if="register">
    <router-link class="link-type" :to="'/register'">立即注册</router-link>
  </div>
</el-form-item>
<script>
import { login } from '@/api/user/login'

export default {
  name: "Login",
  data() {
    return {
      codeUrl: '/captcha?time=' + new Date(),
      loginForm: {
        userName: "admin",
        password: "123456",
        rememberMe: false,
        code: ""
      },
      loginRules: {//错误提示信息
        userName: [{ required: true, trigger: "blur", message: "请输入您的账号" }],
        password: [{ required: true, trigger: "blur", message: "请输入您的密码" }],
        code: [{ required: true, trigger: "change", message: "请输入验证码" }]
      },
      loading: false,
      // 验证码开关
      captchaOnOff: true,
      // 注册开关
      register: false
    };
  },
  methods: {
    //获取验证码
    updateCaptcha() {
      this.codeUrl = '/captcha?time=' + new Date();
    },
    //登录事件
    submitLogin() {
      this.$refs.loginForm.validate(valid => {
        if (valid) {
          console.log(this.loginForm);
          this.loading = true;
          this.postRequest("/login", this.loginForm).then(resp => {
            if (resp) {
              this.loading = false;
              console.log(resp);
              //页面跳转
              let path = this.$route.query.redirect;
              this.$router.replace(
                  path == "/" || path == undefined ? "/home" : path
              );
              // this.$router.replace('/home');
            } else {
              this.loading = false;
            }
          });
        } else {
          this.$message.error("请输入所有字段");
          return false;
        }
      });
    }
  }
};
</script>

4.9 完整代码

<template>
  <div class="login">
    <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
      <h3 class="title">后台管理系统</h3>
      <el-form-item prop="userName">
        <el-input v-model="loginForm.userName"
                  auto-complete="false"
                  placeholder="账号"
                  type="text">
        </el-input>
      </el-form-item>
      <el-form-item prop="password">
        <el-input v-model="loginForm.password"
                  auto-complete="false"
                  placeholder="密码"
                  type="password">
        </el-input>
      </el-form-item>
      <el-form-item v-if="captchaOnOff" prop="code">
        <el-input v-model="loginForm.code"
                  auto-complete="false"
                  placeholder="验证码"
                  style="width: 63%">
        </el-input>
        <div class="login-code">
          <img :src="codeUrl" class="login-code-img" @click="updateCaptcha"/>
        </div>
      </el-form-item>
      <el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
      <el-form-item style="width:100%;">
        <el-button :loading="loading" size="medium" style="width:100%;" type="primary" @click="submitLogin">
          <span v-if="!loading">登 录</span>
          <span v-else>登 录 中...</span>
        </el-button>
        <div v-if="register" style="float: right;">
          <router-link :to="'/register'" class="link-type">立即注册</router-link>
        </div>
      </el-form-item>
    </el-form>
    <!--  底部  -->
    <div class="el-login-footer">
      <span>Copyright © 2018-2022 admin All Rights Reserved.</span>
    </div>
  </div>
</template>

<script>
import {login} from '@/api/user/login'

export default {
  name: "Login",
  data() {
    return {
      codeUrl: '/captcha?time=' + new Date(),
      loginForm: {
        userName: "admin",
        password: "123456",
        rememberMe: false,
        code: ""
      },
      loginRules: {//错误提示信息
        userName: [{required: true, trigger: "blur", message: "请输入您的账号"}],
        password: [{required: true, trigger: "blur", message: "请输入您的密码"}],
        code: [{required: true, trigger: "change", message: "请输入验证码"}]
      },
      loading: false,
      // 验证码开关
      captchaOnOff: true,
      // 注册开关
      register: false
    };
  },
  methods: {
    //获取验证码
    updateCaptcha() {
      this.codeUrl = '/captcha?time=' + new Date();
    },
    //登录事件
    submitLogin() {
      this.$refs.loginForm.validate(valid => {
        if (valid) {
          console.log(this.loginForm);
          this.loading = true;
          login(this.loginForm).then(resp => {
            if (resp) {
              this.loading = false;
              //存储用户token
              this.$store.commit("set_token", resp.data.token);
              //存储用户信息
              window.sessionStorage.setItem('user', resp.data);
              //页面跳转
              let path = this.$route.query.redirect;
              this.$router.replace(
                  path == "/" || path == undefined ? "/home" : path
              );
              // this.$router.replace('/home');
            } else {
              this.loading = false;
            }
          });
        } else {
          this.$message.error("请输入所有字段");
          return false;
        }
      });
    }
  }
};
</script>

<style lang="scss" rel="stylesheet/scss">
.login {
  display: flex;
  position: fixed;
  justify-content: center;
  align-items: center;
  height: 100%;
  width: 100%;
  background-image: url("../assets/images/login-background.jpg");
  background-size: cover;
}

.title {
  margin: 0px auto 30px auto;
  text-align: center;
  color: #707070;
}

.login-form {
  border-radius: 6px;
  background: #ffffff;
  width: 400px;
  padding: 25px 25px 5px 25px;

  .el-input {
    height: 38px;

    input {
      height: 38px;
    }
  }

  .input-icon {
    height: 39px;
    width: 14px;
    margin-left: 2px;
  }
}

.login-tip {
  font-size: 13px;
  text-align: center;
  color: #bfbfbf;
}

.login-code {
  width: 33%;
  height: 38px;
  float: right;

  img {
    cursor: pointer;
    vertical-align: middle;
  }
}

.el-login-footer {
  height: 40px;
  line-height: 40px;
  position: fixed;
  bottom: 0;
  width: 100%;
  text-align: center;
  color: #fff;
  font-family: Arial;
  font-size: 12px;
  letter-spacing: 1px;
}

.login-code-img {
  height: 38px;
}
</style>

5 侧边栏

Vuex 是一个专为 Vue.js 应用程序开发的 状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
表字段:
image.png
后端响应菜单栏的格式

[
  {
    "mid": 1,
    "url": "/",
    "path": "/home",
    "component": "Home",
    "name": "商品管理",
    "iconcls": null,
    "keepalive": null,
    "requireauth": true,
    "parentid": 0,
    "enabled": true,
    "children": [
      {
        "mid": 9,
        "url": null,
        "path": "/product/product",
        "component": "product/product/list",
        "name": "商品列表",
        "iconcls": null,
        "keepalive": null,
        "requireauth": true,
        "parentid": 1,
        "enabled": true,
        "children": null
      },
      {
        "mid": 10,
        "url": null,
        "path": "/product/order",
        "component": "product/order/list",
        "name": "订单列表",
        "iconcls": null,
        "keepalive": null,
        "requireauth": true,
        "parentid": 1,
        "enabled": true,
        "children": null
      }
    ]
  },
  {
    "mid": 2,
    "url": "/",
    "path": "/home",
    "component": "Home",
    "name": "会员管理",
    "iconcls": null,
    "keepalive": null,
    "requireauth": true,
    "parentid": 0,
    "enabled": true,
    "children": [
      {
        "mid": 11,
        "url": null,
        "path": "/member/grade",
        "component": "menber/grade",
        "name": "会员等级",
        "iconcls": null,
        "keepalive": null,
        "requireauth": true,
        "parentid": 2,
        "enabled": true,
        "children": null
      }
    ]
  },
  {
    "mid": 3,
    "url": "/",
    "path": "/home",
    "component": "Home",
    "name": "系统管理",
    "iconcls": "el-icon-setting",
    "keepalive": null,
    "requireauth": true,
    "parentid": 0,
    "enabled": true,
    "children": [
      {
        "mid": 5,
        "url": null,
        "path": "/system/user",
        "component": "system/user/index",
        "name": "用户管理",
        "iconcls": "el-icon-user",
        "keepalive": null,
        "requireauth": true,
        "parentid": 3,
        "enabled": true,
        "children": null
      },
      {
        "mid": 6,
        "url": null,
        "path": "/system/role",
        "component": "system/role/index",
        "name": "角色管理",
        "iconcls": null,
        "keepalive": null,
        "requireauth": true,
        "parentid": 3,
        "enabled": true,
        "children": null
      },
      {
        "mid": 7,
        "url": null,
        "path": "/system/menu",
        "component": "system/menu/index",
        "name": "菜单栏管理",
        "iconcls": null,
        "keepalive": null,
        "requireauth": true,
        "parentid": 3,
        "enabled": true,
        "children": null
      }
    ]
  },
  {
    "mid": 4,
    "url": "/",
    "path": "/home",
    "component": "Home",
    "name": "系统工具",
    "iconcls": "el-icon-s-tools",
    "keepalive": null,
    "requireauth": true,
    "parentid": 0,
    "enabled": true,
    "children": [
      {
        "mid": 8,
        "url": null,
        "path": "/tools/logs",
        "component": "tools/logs/index",
        "name": "日志管理",
        "iconcls": null,
        "keepalive": null,
        "requireauth": true,
        "parentid": 4,
        "enabled": true,
        "children": null
      }
    ]
  }
]

在 src 目录下创建一个名为 store 的目录并新建一个名为 index.js 文件用来配置 Vuex。

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex);

export default new Vuex.Store({
    state: {
    	routes:[],
      permission: {}
    },
    //同步执行操作
    mutations: {
    	//初始化
    	initRoutes(state, data) {
            state.routes = data;
    	},
		// 获取权限
      set_permission(state, permission){
          //设置 permission
          state.permission = permission;
      }
    },
    //异步执行操作
    actions: {},
    modules: {}
})
state全局state对象,用于保存所有组件的公共数据
getters监听state值的最新状态(计算属性)
actions异步执行mutations方法
mutations唯一可以改变state值的方法(同步执行)

修改 main.js 增加刚才配置的 store/index.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

// 菜单栏路由器
import {initMenu} from "./utils/menus";

//使用钩子函数对路由进行权限跳转
router.beforeEach((to, from, next) => {
  if (to.path == '/') {
    next()
  } else {
    initMenu(router, store);
    next();
  }
});

Vue.config.productionTip = false

new Vue({
  router,
  store,//引用
  render: h => h(App)
}).$mount('#app')

封装菜单请求工具类
后端接口返回的数据中 component 的值为String,我们需要将其转换为前端所需的对象并且我们需要将数据放入到路由的配置里。所以我们需要封装菜单请求工具类实现我们的需求。

import { getMenu } from "@/api/user/login";

export const initMenu = (router, store) => {
    if (store.state.routes.length > 0) {
        return;
    }
    //查询菜单栏
    getMenu().then(res => {
        if (res) {
            //格式化Router
            let fmtRoutes = formatRoutes(res.data);
            //添加到router
            router.addRoutes(fmtRoutes);
            //将数据存入vuex
            store.commit('initRoutes', fmtRoutes);
        }
    })
		// 获取权限信息
    getPermission().then(res => {
        if (res) {
            //将数据存入vuex
            store.commit('set_permission', res.data);
        }
    })
};

export const formatRoutes = (routes) => {
    let fmtRoutes = [];
    routes.forEach(router => {
        let {
            path,
            component,
            name,
            iconCls,
            children,
        } = router;
        if (children && children instanceof Array) {
            //递归
            children = formatRoutes(children);
        }
        let fmRouter = {
            path: path,
            name: name,
            iconCls: iconCls,
            children: children,
            //这里注意了,数据库里的路径要对页面的路径,不能出错了
            component:()=>import(`@/views/${component}.vue`)
        };
        fmtRoutes.push(fmRouter);
    });
    return fmtRoutes;
};

导航守卫
菜单数据在用户点击刷新按钮时可能出现丢失的情况,解决办法

  1. 每个页面添加初始化菜单的方法,这显然很麻烦 。
  2. 路由导航守卫 。

vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的,单个路由独享的, 或者组件级的。
记住参数或查询的改变并不会触发进入离开的导航守卫。我们可以通过观察 $route 对象来应对这些变化,或使用 beforeRouteUpdate 的组件内守卫。

import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '../views/Login.vue'
import Register from '../views/Register.vue'
import Home from '../views/Home.vue'
import Console from '../views/console/index'

Vue.use(VueRouter)

const routes = [
    {
        path: '/',
        name: 'Login',
        component: Login,
        hidden: true//隐藏路由
    },
    {
        path: '/register',
        name: '注册',
        component: Register,
        hidden: true//隐藏路由
    },
    {
        path: '/home',//路径
        name: '控制台',//名字
        redirect: 'console',//重定向路由
        component: Home,//文件地址
        hidden: true,//隐藏路由
        children: [//子级菜单
            {
                path: '/console',
                name: '控制台',
                component: Console
            }
        ]
    }
]

const router = new VueRouter({
    // mode: 'history', // 去掉url中的#
    routes
})

export default router

当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫resolve 完之前一直处于 等待中
每个守卫方法接收三个参数:

to: Route即将要进入的目标路由对象。
from: Route当前导航正要离开的路由。
next: Function一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。
next()进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是confirmed (确认的)。
next(false)中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。next(‘/’) 或者 next({ path: ‘/’ }) : 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。你可以向 next 传递任意位置对象,且允许设置诸如 replace:true 、 name: ‘home’ 之类的选项以及任何用在 router-link 的 to prop 或router.push 中的选项。
next(error)(2.4.0+) 如果传入 next 的参数是一个 Error 实例,则导航会被终止且该错误会被传递给router.onError() 注册过的回调。

确保要调用 next 方法,否则钩子就不会被 resolved。

// 菜单栏路由器
import {initMenu} from "./utils/menus";

//使用钩子函数对路由进行权限跳转
router.beforeEach((to, from, next) => {
  if (to.path == '/') {
    next()
  } else {
    initMenu(router, store);
    next();
  }
});
<el-menu
         router
         unique-opened
         text-color="#fff"
         :collapse="isCollapse"
         :collapse-transition="false"
         active-text-color="#409EFF"
         style="height: 100%"
         :default-active="activePath"
         background-color="#344a5f">
  <div class="nav-head">
    <img src="@/assets/images/tp6.jpg"/>
    <!--v-show  显示:true/隐藏:false-->
    <div class="title" v-show="isCollapse?false:true">Admin权限管理系统</div>
  </div>
  <el-submenu :index="index+''" v-for="(item,index) in routes" v-if="!item.hidden" :key="index">
    <template slot="title">
      <i style="color: #ffffff;margin-right: 5px" :class="item.iconCls"></i>
      <span>{{item.name}}</span>
    </template>
    <el-menu-item :index="children.path"
                  v-for="(children,indexj) in item.children"
                  :key="indexj"
                  @click="saveNavState(children.path)">
      <i style="color: #ffffff;margin-right: 5px" :class="children.iconCls"></i>
      {{children.name}}
    </el-menu-item>
  </el-submenu>
</el-menu>

<script>
    export default {
        name: "Home",
        data() {
            return {
                isCollapse: false
            };
        },
        computed: {//数据初始化
            routes() {
                return this.$store.state.routes;
            }
        }
    }
</script>

6 首页

<template>
  <div class="box">
    <el-container>
      <el-aside :width="isCollapse?'66px':'200px'" class="nav-wrap">
        <div style="height: 100%">
          <el-menu
              :collapse="isCollapse"
              :collapse-transition="false"
              :default-active="activePath"
              active-text-color="#409EFF"
              background-color="#344a5f"
              router
              style="height: 100%"
              text-color="#fff"
              unique-opened>
            <div class="nav-head">
              <img src="@/assets/images/tp6.png"/>
              <!--v-show  显示:true/隐藏:false-->
              <div v-show="isCollapse?false:true" class="title">Admin权限管理系统</div>
            </div>
            <el-submenu v-for="(item,index) in routes" v-if="!item.hidden" :key="index" :index="index+''">
              <template slot="title">
                <i :class="item.iconCls" style="color: #ffffff;margin-right: 5px"></i>
                <span>{{ item.name }}</span>
              </template>
              <el-menu-item v-for="(children,indexj) in item.children"
                            :key="indexj"
                            :index="children.path"
                            @click="saveNavState(children.path)">
                <i :class="children.iconCls" style="color: #ffffff;margin-right: 5px"></i>
                {{ children.name }}
              </el-menu-item>
            </el-submenu>
          </el-menu>
        </div>
      </el-aside>
      <el-container>
        <el-header class="homeHeader">
          <el-row style="width: 250px">
            <el-col :span="4">
              <div style="font-size: 25px;margin-left: -8px">
                <i :class="isCollapse?'el-icon-s-unfold':'el-icon-s-fold'"
                   style="cursor: pointer;"
                   @click="icons">
                </i>
              </div>
            </el-col>
            <el-col :span="20">
              <div style="margin-top: 9px">
                <el-breadcrumb v-if="this.$router.currentRoute.path!='/home'"
                               separator-class="el-icon-arrow-right">
                  <el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
                  <el-breadcrumb-item>{{ this.$router.currentRoute.name }}</el-breadcrumb-item>
                </el-breadcrumb>
              </div>
            </el-col>
          </el-row>
          <el-row style="width: 250px">
            <el-col :span="10">
              <div style="line-height: 56px">
                <span style="font-size: 25px;">
                  <el-tooltip class="item" content="刷新" effect="dark" placement="bottom">
                    <i class="el-icon-refresh" style="cursor: pointer;margin-right: 8px;"
                       @click="ref"></i>
                  </el-tooltip>
                  <el-tooltip class="item" content="全屏" effect="dark" placement="bottom">
                    <i class="el-icon-full-screen" style="cursor: pointer;margin-right: 8px"
                       @click=""></i>
                  </el-tooltip>
                  <span class="lock-wrap" @click="lockChange">
                    <el-tooltip :content="lockFlag?`点击锁定`:`点击解锁`" class="item" effect="dark"
                                placement="bottom">
                        <i class="el-icon-unlock" style="cursor: pointer;margin-right: 8px"></i>
                    </el-tooltip>
                  </span>
                </span>
              </div>
            </el-col>
            <el-col :span="14">
              <el-dropdown class="userInfo" @command="commandHandler">
                <span class="nav-head">
                    <i><img :src="require('@/assets/images/tp6.png')"></i>
                    <span style="margin-left: 2px">
                    {{ user.name }}<i class="el-icon-arrow-down"></i>
                    </span>
                </span>
                <el-dropdown-menu slot="dropdown">
                  <el-dropdown-item command="userInfo">个人中心</el-dropdown-item>
                  <el-dropdown-item command="setting">设置</el-dropdown-item>
                  <el-dropdown-item command="logout">注销登录</el-dropdown-item>
                </el-dropdown-menu>
              </el-dropdown>
            </el-col>
          </el-row>
        </el-header>
        <el-main class="main-wrap">
          <router-view class="homeRouterView"/>
        </el-main>
      </el-container>
    </el-container>
  </div>
</template>

<script>
import {logout} from '@/api/user/login'

export default {
  name: "Home",
  data() {
    return {
      //菜单展开/收起
      isCollapse: false,
      breadList: [], // 路由集合
      user: {
        name: '张三',
        userFace: '',
      },
      // 被激活的链接地址
      activePath: '',
      lockFlag: true
    };
  },
  created() {
    //初始化菜单激活状态
    this.activePath = window.sessionStorage.getItem('activePath')
  },
  computed: {//数据初始化
    routes() {
      // console.log("菜单2:", this.$store.state.routes)
      return this.$store.state.routes;
    }
  },
  methods: {
    //页面锁定事件
    lockChange() {
      if (this.lockFlag) {
        localStorage.setItem('lockFlag', 0);
        this.lockFlag = false;
        this.$message({
          type: 'success',
          message: '页面锁定成功!'
        });
      } else {
        // const uInfo = JSON.parse(localStorage.getItem("userInfo")); // 用户
        this.$prompt('', '请输入密码', {
          inputType: 'password',
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          inputValidator: (value) => {
            // let hash = md5(value)//密码
            // return hash.toUpperCase()===uInfo.passwd
          },
          inputErrorMessage: '密码输入不正确'
        }).then(({value}) => {
          this.$message({
            type: 'success',
            message: '解锁成功'
          });
          // Message.error({message:'尚未登录,请登录'});
          localStorage.setItem('lockFlag', 1);
          this.lockFlag = true
        }).catch(() => {
        });
      }
    },
    //刷新页面
    ref() {
      this.$router.go(0)
    },
    //菜单展开/收起
    icons() {
      this.isCollapse = !this.isCollapse;
    },
    // 保存链接的激活状态
    saveNavState(activePath) {
      window.sessionStorage.setItem('activePath', activePath);
      this.activePath = activePath;
    },
    //回调事件
    commandHandler(cmd) {
      if (cmd == 'logout') {
        this.$confirm('此操作将注销登录,是否继续?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          //注销
          logout().then(resp => {
            if (resp.code == 200) {
              //清楚菜单状态
              window.sessionStorage.removeItem('activePath');
              //清除用户信息
              window.sessionStorage.removeItem("user");
              //清除用户token
              window.sessionStorage.removeItem("set_token");
              //清空菜单和Authorization
              this.$store.commit('initRoutes', []);
              this.$store.commit('set_token', '');
              //跳转登录页
              this.$router.replace("/")
            }
          });
        }).catch(() => {
          this.$message({
            type: 'info',
            message: '已取消操作'
          })
        })
      }
      if (cmd == 'userInfo') {
        this.$router.push('/userInfo');
      }
    }
  }
}
</script>
<style lang="scss">
.box {
  margin: -8px;
  padding: 0;
  height: 100%;
}

/*---- 侧边栏 start ----*/
.nav-wrap {
  top: 0;
  left: 0;
  height: 100vh;
}

//头部背景
.nav-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 15px;
  box-sizing: border-box;
  height: 60px;
}

//头部字体大小
.nav-head .title {
  font-size: 14px;
  color: #fff;
}

//log
.nav-head img {
  width: 48px;
  height: 48px;
  border-radius: 24px;
  margin-left: -8px;
}

//二级菜单背景色
.el-menu-item {
  /*background-color: rgb(31,45,61) !important;*/
}

//鼠标悬浮背景色
.el-menu-item:hover {
  outline: 0 !important;
  color: #409EFF !important;
}

//点击选择背景色
.el-menu-item.is-active {
  color: #409EFF !important;
  background: rgb(31, 45, 61) !important;
}

/*---- 侧边栏 end ----*/

/*---- 头部 start ----*/
.homeHeader {
  -webkit-box-shadow: 0 3px 16px 0 rgba(0, 0, 0, .1);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 15px;
  box-sizing: border-box;
}

.homeHeader .userInfo {
  cursor: pointer;
}

/*---- 头部 end ----*/

/*---- 内容 start ----*/
.main-wrap {
  border: 20px solid #f7f7f7;
  border-bottom: none;
  -webkit-box-sizing: border-box
}

.homeRouterView {
  /*margin-top: 10px;*/
}

/*---- 内容 end ----*/

</style>

7 菜单管理

接口:@/api/admin/menu

import request from '@/utils/request'

// 查询菜单栏
export function getMenuList(query) {
    return request({
        url: '/sysMenu/getMenuList',
        method: 'get',
        params: query
    })
}

// 查询下拉树
export function fetchMenuTree(lazy, parentId) {
    return request({
        url: '/sysMenu/tree',
        method: 'get',
        params: { lazy: lazy, parentId: parentId }
    })
}

// 添加
export function addObj(obj) {
    return request({
        url: '/sysMenu/addObj',
        method: 'post',
        data: obj
    })
}

// 根据 id 获取菜单信息
export function getObj(id) {
    return request({
        url: '/sysMenu/getObj',
        method: 'get',
        params: { mid: id }
    })
}

// 删除
export function delObj(id) {
    return request({
        url: '/sysMenu/delObj',
        method: 'delete',
        params: { mid: id }
    })
}

// 修改
export function putObj(obj) {
    return request({
        url: '/sysMenu/putObj',
        method: 'put',
        data: obj
    })
}

树表格:index.vue

<template>
  <div>
    <!-- 搜索 -->
    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true">
      <el-form-item label="菜单名称:" prop="menuName">
        <el-input
          v-model="queryParams.name"
          placeholder="请输入菜单名称"
          clearable
          @keyup.enter.native="handleQuery"
        />
      </el-form-item>
      <el-form-item label="组件路径:" prop="path">
        <el-input
          v-model="queryParams.path"
          placeholder="请输入组件路径"
          clearable
          @keyup.enter.native="handleQuery"
        />
      </el-form-item>
      <el-form-item label="状态:" prop="enabled">
        <el-select v-model="queryParams.enabled" placeholder="请选择" clearable>
          <el-option
            v-for="item in options"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          />
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button icon="el-icon-search" size="mini" type="primary" @click="handleQuery">搜索</el-button>
        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>

    <!-- 按钮 -->
    <el-row :gutter="10" class="mb8">
      <el-col :span="1.5">
        <el-button
          v-if="permissions.system_menu_add"
          type="primary"
          plain
          icon="el-icon-plus"
          size="mini"
          @click="addOrUpdateHandle(false)"
        >新增</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button type="info" plain icon="el-icon-sort" size="mini" @click="toggleExpandAll">展开/折叠</el-button>
      </el-col>
      <!-- <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar> -->
    </el-row>

    <!-- default-expand-all:默认展开下级菜单 -->
    <el-table
      style="width: 100%;margin-top: 10px;"
      height="520"
      v-if="refreshTable"
      :default-expand-all="isExpandAll"
      :data="menuList"
      row-key="mid"
      border
      v-loading="loading"
      :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
    >
      <el-table-column prop="name" label="菜单名称" :show-overflow-tooltip="true" width="180"></el-table-column>
      <el-table-column prop="iconCls" label="图标" align="center" width="100">
        <template slot-scope="scope">
          <i :class="scope.row.iconCls" />
        </template>
      </el-table-column>
      <el-table-column prop="sortOrder" label="排序" width="80" sortable></el-table-column>
      <!-- <el-table-column prop="path" label="组件路径" :show-overflow-tooltip="true"></el-table-column> -->
      <el-table-column prop="component" label="组件路径" :show-overflow-tooltip="true"></el-table-column>
      <el-table-column prop="permission" label="权限标识" :show-overflow-tooltip="true"></el-table-column>
      <el-table-column prop="type" label="类型" width="80" align="center">
        <template slot-scope="scope">
          <el-tag type="success" v-if="scope.row.type == '0'">菜单</el-tag>
          <el-tag type="info" v-if="scope.row.type == '1'">按钮</el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="keepAlive" label="是否保持激活" width="80" align="center">
        <template slot-scope="scope">
          <el-tag type="info" v-if="scope.row.keepAlive == '0'">关闭</el-tag>
          <el-tag type="success" v-if="scope.row.keepAlive == '1'">开启</el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="enabled" label="是否启用" width="80" align="center">
        <template slot-scope="scope">
          <el-tag type="info" v-if="scope.row.enabled == '0'">关闭</el-tag>
          <el-tag type="success" v-if="scope.row.enabled == '1'">开启</el-tag>
        </template>
      </el-table-column>
      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
        <template slot-scope="scope">
          <el-button
            v-if="permissions.system_menu_add"
            type="text"
            icon="el-icon-plus"
            @click="addOrUpdateHandle(false,scope.row.mid)"
          >添加</el-button>
          <el-button
            v-if="permissions.system_menu_edit"
            type="text"
            icon="el-icon-edit"
            @click="addOrUpdateHandle(true,scope.row.mid)"
          >修改</el-button>
          <el-button
            v-if="permissions.system_menu_del"
            type="text"
            icon="el-icon-delete"
            @click="handleDelete(scope.row)"
          >删除</el-button>
        </template>
      </el-table-column>
    </el-table>

    <table-form v-if="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="getList"></table-form>
  </div>
</template>

<script>
import { getMenuList, delObj } from "@/api/admin/menu";
import TableForm from "./menu-form";

export default {
  name: "index",
  components: { TableForm },
  data() {
    return {
      addOrUpdateVisible: false,
      // 遮罩层
      loading: true,
      // 菜单表格树数据
      menuList: [],
      // 查询参数
      queryParams: {
        name: undefined,
        path: undefined,
        enabled: ""
      },
      // 是否启用选择
      options: [
        {
          value: "0",
          label: "关闭"
        },
        {
          value: "1",
          label: "开启"
        }
      ],
      // 是否展开,默认全部折叠
      isExpandAll: false,
      // 重新渲染表格状态
      refreshTable: true,
      // 权限
      permissions: {}
    };
  },
  created() {
    this.getList();
    // 获取权限码
    this.permissions = this.$store.state.permission;
  },
  methods: {
    // 添加/修改弹窗
    addOrUpdateHandle(isEdit, id) {
      this.addOrUpdateVisible = true;
      // 调用子窗口并传值
      this.$nextTick(() => {
        this.$refs.addOrUpdate.init(isEdit, id);
      });
    },
    // 查询/搜索操作 
    getList() {
      this.loading = true;
      getMenuList(this.queryParams).then(res => {
        this.menuList = res.data;
        this.loading = false;
      });
    },
    // 搜索按钮操作 
    handleQuery() {
      this.getList();
    },
    // 重置按钮操作 
    resetQuery() {
      this.queryParams = {};
      this.handleQuery();
    },
    // 展开/折叠操作 
    toggleExpandAll() {
      this.refreshTable = false;
      this.isExpandAll = !this.isExpandAll;
      this.$nextTick(() => {
        this.refreshTable = true;
      });
    },
    // 删除
    handleDelete(row) {
      this.$confirm('是否确认删除名称为"' + row.name + '"的数据项?', "警告", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
      })
        .then(function() {
          return delObj(row.mid);
        })
        .then(() => {
          this.getList();
          this.$router.go(0);
        });
    }
  }
};
</script>

<style scoped>
</style>

添加/修改弹窗:menu-from.vue

<template>
  <!-- 添加或修改菜单对话框 -->
  <el-dialog :title="!form.mid ? '新增' : '修改'" :visible.sync="visible">
    <el-form ref="dataForm" :model="form" :rules="rules" label-width="80px">
      <el-row>
        <el-col :span="12">
          <el-form-item label="菜单类型" prop="type">
            <el-radio-group v-model="form.type" size="small">
              <el-radio-button label="0">菜单</el-radio-button>
              <el-radio-button label="1">按钮</el-radio-button>
            </el-radio-group>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="上级菜单">
            <treeselect
              v-model="form.parentId"
              :options="menuOptions"
              :normalizer="normalizer"
              :show-count="true"
              placeholder="选择上级菜单"
            />
          </el-form-item>
        </el-col>
      </el-row>
      <el-form-item label="图标" prop="iconCls" v-if="form.type == '0'">
        <avue-input-icon v-model="form.iconCls" :icon-list="iconList"></avue-input-icon>
      </el-form-item>
      <el-form-item label="名称" prop="name">
        <el-input v-model="form.name" placeholder="请输入菜单名称" />
      </el-form-item>
      <el-form-item label="路由地址" prop="component" v-if="form.type != '1'">
        <el-input v-model="form.component" placeholder="请输入路由地址" />
      </el-form-item>
      <el-form-item label="权限标识" prop="permission" v-if="form.type == '1'">
        <el-input v-model="form.permission" placeholder="请权限标识" maxlength="50" />
      </el-form-item>
      <el-row>
        <el-col :span="12">
          <el-form-item label="排序" prop="sortOrder">
            <el-input-number v-model="form.sortOrder" controls-position="right" :min="0" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="路由缓冲" prop="keepAlive" v-if="form.type != '1'">
            <el-radio-group v-model="form.keepAlive">
              <el-radio-button label="0">否</el-radio-button>
              <el-radio-button label="1">是</el-radio-button>
            </el-radio-group>
          </el-form-item>
        </el-col>
      </el-row>
    </el-form>
    <div slot="footer" class="dialog-footer">
      <el-button type="primary" @click="dataFormSubmit">确 定</el-button>
      <el-button @click="visible = false">取 消</el-button>
    </div>
  </el-dialog>
</template>

<!-- 添加svg图标库 -->
<script src="//at.alicdn.com/t/font_2621503_zcbiqy2g1i.js"></script>
<script>
import { fetchMenuTree, getObj, addObj, putObj, delObj } from "@/api/admin/menu";
import Treeselect from "@riophae/vue-treeselect";
import iconList from "@/const/iconList";
import TableForm from "./";
import "@riophae/vue-treeselect/dist/vue-treeselect.css";

export default {
  name: "Menu",
  components: { Treeselect, TableForm },
  data() {
    return {
      // 遮罩层
      loading: true,
      // 菜单树选项
      menuOptions: [],
      // 是否显示弹出层
      visible: false,
      // 图标
      iconList: iconList,
      form: {
        name: undefined,
        component: undefined,
        iconCls: undefined,
        permission: undefined,
        type: "0",
        keepAlive: "0",
        sortOrder: 999,
        mid: undefined,
        parentId: undefined
      },
      // 表单校验
      rules: {
        name: [
          { required: true, message: "菜单名称不能为空", trigger: "blur" }
        ],
        sortOrder: [
          { required: true, message: "菜单顺序不能为空", trigger: "blur" }
        ],
        component: [
          { required: true, message: "路由地址不能为空", trigger: "blur" }
        ],
        keepAlive: [
          { required: true, message: "路由缓冲不能为空", trigger: "blur" }
        ],
        permission: [
          { required: true, message: "权限标识不能为空", trigger: "blur" }
        ]
      }
    };
  },
  methods: {
    // 父传子
    init(isEdit, id) {
      if (id != null) {
        this.form.parentId = id;
      }
      this.visible = true;
      this.getTreeselect();
      this.$nextTick(() => {
        this.$refs["dataForm"].resetFields();
        if (isEdit) {
          getObj(id).then(response => {
            this.form = response.data;
          });
        } else {
          this.form.mid = undefined;
        }
      });
    },
    // 表单提交
    dataFormSubmit() {
      this.$refs["dataForm"].validate(valid => {
        if (valid) {
          if (this.form.parentId == undefined) {
            this.form.parentId = 0;
          }
          if (this.form.mid) {
            // 修改
            putObj(this.form).then(data => {
              this.visible = false;
              this.$emit("refreshDataList");
            });
          } else {
            // 添加
            addObj(this.form).then(data => {
              this.visible = false;
              this.$emit("refreshDataList");
            });
          }
        }
      });
    },
    // 查询菜单下拉树结构 
    getTreeselect() {
      fetchMenuTree().then(response => {
        this.menuOptions = [];
        const menu = { mid: 0, name: "根菜单", children: [] };
        menu.children = response.data;
        this.menuOptions.push(menu);
      });
    },
    // 转换菜单数据结构 
    normalizer(node) {
      if (node.children && !node.children.length) {
        delete node.children;
      }
      return {
        id: node.mid,
        label: node.name,
        children: node.children
      };
    }
  }
};
</script>

阿里云图标:iconList.js

export default [
	{
		label: '阿里云图标',
		list: [
			{
				label: '工具',
				value: 'el-icon-s-tools',
			},
			{
				label: '设置',
				value: 'el-icon-setting',
			},
			{
				label: '角色管理',
				value: 'el-icon-user-solid',
			},
			{
				label: '角色',
				value: 'el-icon-user',
			},
			{
				label: '客户',
				value: 'el-icon-s-custom',
			},
			{
				label: '电话',
				value: 'el-icon-phone',
			},
			{
				label: '电话呼叫',
				value: 'el-icon-phone-outline',
			},
			{
				label: '更多',
				value: 'el-icon-more',
			},
			{
				label: '更多1',
				value: 'el-icon-more-outline',
			},
			{
				label: '五角星',
				value: 'el-icon-star-on',
			},
			{
				label: '五角星1',
				value: 'el-icon-star-off',
			},
			{
				label: '商城',
				value: 'el-icon-s-goods',
			},
			{
				label: '商城1',
				value: 'el-icon-goods',
			},
			{
				label: '帮助',
				value: 'el-icon-s-help',
			},
			{
				label: '帮助1',
				value: 'el-icon-help',
			},
			{
				label: '减少',
				value: 'el-icon-minus',
			},
			{
				label: '添加',
				value: 'el-icon-plus',
			},
			{
				label: '检查',
				value: 'el-icon-check',
			},
			{
				label: '关闭',
				value: 'el-icon-close',
			},
			{
				label: '图片',
				value: 'el-icon-picture',
			},
			{
				label: '图片1',
				value: 'el-icon-picture-outline',
			},
			{
				label: '图片2',
				value: 'el-icon-picture-outline-round',
			},
			{
				label: '上传',
				value: 'el-icon-upload',
			},
			{
				label: '上传1',
				value: 'el-icon-upload2',
			},
			{
				label: '下载',
				value: 'el-icon-download',
			},
			{
				label: '相机',
				value: 'el-icon-camera-solid',
			},
			{
				label: '相机1',
				value: 'el-icon-camera',
			},
			{
				label: '视频',
				value: 'el-icon-video-camera-solid',
			},
			{
				label: '视频1',
				value: 'el-icon-video-camera',
			},
			{
				label: '通知',
				value: 'el-icon-message-solid',
			},
			{
				label: '通知1',
				value: 'el-icon-bell',
			},
			{
				label: '短信',
				value: '#icon-duanxin2',
			},
			{
				label: '工具箱',
				value: 'el-icon-s-cooperation',
			},
			{
				label: '操作',
				value: 'el-icon-s-order',
			},
			{
				label: '屏幕',
				value: 'el-icon-s-platform',
			},
			{
				label: '折叠',
				value: 'el-icon-s-fold',
			},
			{
				label: '折叠1',
				value: 'el-icon-s-unfold',
			},
			{
				label: '折线',
				value: 'el-icon-s-operation',
			},
			{
				label: '提升',
				value: 'el-icon-s-promotion',
			},
			{
				label: '首页',
				value: 'el-icon-s-home',
			},
			{
				label: '释放',
				value: 'el-icon-s-release',
			},
			{
				label: '优惠券',
				value: 'el-icon-s-ticket',
			},
			{
				label: '管理',
				value: 'el-icon-s-management',
			},
			{
				label: '打开',
				value: 'el-icon-s-open',
			},
			{
				label: '购物',
				value: 'el-icon-s-shop',
			},
			{
				label: '市场',
				value: 'el-icon-s-marketing',
			},
			{
				label: '目标',
				value: 'el-icon-s-flag',
			},
			{
				label: '信息',
				value: 'el-icon-s-comment',
			},
			{
				label: '金融',
				value: 'el-icon-s-finance',
			},
			{
				label: '宣布',
				value: 'el-icon-s-claim',
			},
			{
				label: '用户',
				value: 'el-icon-s-custom',
			},
			{
				label: '提示',
				value: 'el-icon-s-opportunity',
			},
			{
				label: '数据',
				value: 'el-icon-s-data',
			},
			{
				label: '质检',
				value: 'el-icon-s-check',
			},
			{
				label: '全部',
				value: 'el-icon-menu',
			},
			{
				label: '分享',
				value: 'el-icon-share',
			},
			{
				label: '播放',
				value: 'el-icon-video-pause',
			},
			{
				label: '暂停',
				value: 'el-icon-video-play',
			},
			{
				label: '刷新',
				value: 'el-icon-refresh',
			},
			{
				label: '向后',
				value: 'el-icon-refresh-right',
			},
			{
				label: '撤回',
				value: 'el-icon-refresh-left',
			},
			{
				label: '完成',
				value: 'el-icon-finished',
			},
			{
				label: '排序',
				value: 'el-icon-sort',
			},
			{
				label: '全屏',
				value: 'el-icon-rank',
			},
			{
				label: '可见',
				value: 'el-icon-view',
			},
			{
				label: '日历',
				value: 'el-icon-date',
			},
			{
				label: '标记',
				value: 'el-icon-edit-outline',
			},
			{
				label: '文件',
				value: 'el-icon-folder',
			},
			{
				label: '文件类型',
				value: 'el-icon-folder-opened',
			},
			{
				label: '文件添加',
				value: 'el-icon-folder-add',
			},
			{
				label: '文件移除',
				value: 'el-icon-folder-remove',
			},
			{
				label: '文件删除',
				value: 'el-icon-folder-delete',
			},
			{
				label: '文件质检',
				value: 'el-icon-folder-checked',
			},
			{
				label: '文件复制',
				value: 'el-icon-document-copy',
			},
			{
				label: '票据',
				value: 'el-icon-tickets',
			},
			{
				label: '打印',
				value: 'el-icon-printer',
			},
			{
				label: '回形针',
				value: 'el-icon-paperclip',
			},
			{
				label: '收纳盒',
				value: 'el-icon-takeaway-box',
			},
			{
				label: '搜索',
				value: 'el-icon-search',
			},
			{
				label: '显示屏',
				value: 'el-icon-monitor',
			},
			{
				label: '吸磁',
				value: 'el-icon-attract',
			},
			{
				label: '手机',
				value: 'el-icon-mobile',
			},
			{
				label: '剪切',
				value: 'el-icon-scissors',
			},
			{
				label: '雨伞',
				value: 'el-icon-umbrella',
			},
			{
				label: '耳机',
				value: 'el-icon-headset',
			},
			{
				label: '刷子',
				value: 'el-icon-brush',
			},
			{
				label: '鼠标',
				value: 'el-icon-mouse',
			},
			{
				label: '配合',
				value: 'el-icon-coordinate',
			},
			{
				label: 'magic',
				value: 'el-icon-magic-stick',
			},
			{
				label: '阅读',
				value: 'el-icon-reading',
			},
			{
				label: '数据线',
				value: 'el-icon-data-line',
			},
			{
				label: '饼图',
				value: 'el-icon-pie-chart',
			},
			{
				label: '收藏夹',
				value: 'el-icon-collection-tag',
			},
			{
				label: '电影',
				value: 'el-icon-film',
			},
			{
				label: '急救箱',
				value: 'el-icon-suitcase',
			},
			{
				label: '急救箱1',
				value: 'el-icon-suitcase-1',
			},
			{
				label: '收藏夹1',
				value: 'el-icon-collection',
			},
			{
				label: '笔记本',
				value: 'el-icon-notebook-1',
			},
			{
				label: '笔记本1',
				value: 'el-icon-notebook-2',
			},
			{
				label: '卷纸',
				value: 'el-icon-toilet-paper',
			},
			{
				label: '办公楼',
				value: 'el-icon-office-building',
			},
			{
				label: '学校',
				value: 'el-icon-school',
			},
			{
				label: '台灯',
				value: 'el-icon-table-lamp',
			},
			{
				label: '首页1',
				value: 'el-icon-house',
			},
			{
				label: '禁止抽烟',
				value: 'el-icon-no-smoking',
			},
			{
				label: '抽烟',
				value: 'el-icon-smoking',
			},
			{
				label: '购物车',
				value: 'el-icon-shopping-cart-full',
			},
			{
				label: '购物车1',
				value: 'el-icon-shopping-cart-1',
			},
			{
				label: '购物车2',
				value: 'el-icon-shopping-cart-2',
			},
			{
				label: '购物袋',
				value: 'el-icon-shopping-bag-1',
			},
			{
				label: '购物袋1',
				value: 'el-icon-shopping-bag-2',
			},
			{
				label: '礼物',
				value: 'el-icon-present',
			},
			{
				label: '卡',
				value: 'el-icon-bank-card',
			},
			{
				label: '金钱',
				value: 'el-icon-money',
			},
			{
				label: '数据库',
				value: 'el-icon-coin',
			},
			{
				label: '钱包',
				value: 'el-icon-wallet',
			},
			{
				label: '折扣',
				value: 'el-icon-discount',
			},
			{
				label: '折扣1',
				value: 'el-icon-price-tag',
			},
			{
				label: '新闻',
				value: 'el-icon-news',
			},
			{
				label: '指导',
				value: 'el-icon-guide',
			},
			{
				label: 'male',
				value: 'el-icon-male',
			},
			{
				label: 'female',
				value: 'el-icon-female',
			},
			{
				label: '手指',
				value: 'el-icon-thumb',
			},
			{
				label: 'cpu',
				value: 'el-icon-cpu',
			},
			{
				label: '链接',
				value: 'el-icon-link',
			},
			{
				label: '关联',
				value: 'el-icon-connection',
			},
			{
				label: '打开1',
				value: 'el-icon-open',
			},
			{
				label: '关闭',
				value: 'el-icon-turn-off',
			},
			{
				label: '聊天',
				value: 'el-icon-chat-round',
			},
			{
				label: '聊天1',
				value: 'el-icon-chat-line-round',
			},
			{
				label: '短信',
				value: 'el-icon-chat-square',
			},
			{
				label: '信息',
				value: 'el-icon-chat-dot-round',
			},
			{
				label: '信息1',
				value: 'el-icon-chat-dot-square',
			},
			{
				label: '信息2',
				value: 'el-icon-chat-line-square',
			},
			{
				label: '消息',
				value: 'el-icon-message',
			},
			{
				label: 'postcard',
				value: 'el-icon-postcard',
			},
			{
				label: '方向',
				value: 'el-icon-position',
			},
			{
				label: '静音',
				value: 'el-icon-turn-off-microphone',
			},
			{
				label: '语音',
				value: 'el-icon-microphone',
			},
			{
				label: '关闭通知',
				value: 'el-icon-close-notification',
			},
			{
				label: '帮助',
				value: 'el-icon-bangzhu',
			},
			{
				label: '时间',
				value: 'el-icon-time',
			},
			{
				label: '截取',
				value: 'el-icon-crop',
			},
			{
				label: '目标',
				value: 'el-icon-aim',
			},
			{
				label: '关机',
				value: 'el-icon-switch-button',
			},
			{
				label: '权限',
				value: '#icon-quanxian',
			},
			{
				label: '屏幕',
				value: 'el-icon-full-screen',
			},
			{
				label: 'mic',
				value: 'el-icon-mic',
			},
			{
				label: '指向盘',
				value: 'el-icon-stopwatch',
			},
			{
				label: 'medal',
				value: 'el-icon-medal-1',
			},
			{
				label: '奖章',
				value: 'el-icon-medal',
			},
			{
				label: '奖杯',
				value: 'el-icon-trophy',
			},
			{
				label: '奖杯1',
				value: 'el-icon-trophy-1',
			},
			{
				label: '医疗箱',
				value: 'el-icon-first-aid-kit',
			},
			{
				label: 'place',
				value: 'el-icon-place',
			},
			{
				label: '定位',
				value: 'el-icon-location',
			},
			{
				label: '定位1',
				value: 'el-icon-location-outline',
			},
			{
				label: '定位2',
				value: 'el-icon-location-information',
			},
			{
				label: '闹钟',
				value: 'el-icon-alarm-clock',
			},
			{
				label: 'timer',
				value: 'el-icon-timer',
			},
			{
				label: '手表',
				value: 'el-icon-watch-1',
			},
			{
				label: '手表1',
				value: 'el-icon-watch',
			},
			{
				label: '关锁',
				value: 'el-icon-lock',
			},
			{
				label: '开锁',
				value: 'el-icon-unlock',
			},
			{
				label: '钥匙',
				value: 'el-icon-key',
			},
			{
				label: '客服',
				value: 'el-icon-service',
			},
			{
				label: '自行车',
				value: 'el-icon-bicycle',
			},
			{
				label: '卡车',
				value: 'el-icon-truck',
			},
			{
				label: 'sunset',
				value: 'el-icon-sunset',
			},
		]
	}
]

  border: true,
  index: true,
  indexLabel: '序号',
  stripe: true,//高亮
  menuAlign: 'center',
  editBtn: false,//编辑
  delBtn: false,//删除
  searchMenuSpan: 6,
  menuWidth: 300,
  dialogWidth: '50%',
  align: 'center',//居中
  addBtn: false,//添加按钮
  viewBtn: true,//查看

下拉树菜单

npm install --save @riophae/vue-treeselect
<el-form-item label="上级菜单">
  <treeselect
    v-model="form.parentId"
    :options="menuOptions"
    :normalizer="normalizer"
    :show-count="true"
    placeholder="选择上级菜单"
  />
</el-form-item>
<script>
import { fetchMenuTree } from "@/api/admin/menu";
import Treeselect from "@riophae/vue-treeselect";
import "@riophae/vue-treeselect/dist/vue-treeselect.css";

export default {
  name: "Menu",
  components: { Treeselect },
  data() {
    return {
      // 菜单树选项
      menuOptions: [],
      form: {
        menuId: undefined,
        parentId: undefined
      },
    };
  },
  methods: {
    /** 查询菜单下拉树结构 */
    getTreeselect() {
      fetchMenuTree().then(response => {
        this.menuOptions = [];
        const menu = { mid: 0, name: "根菜单", children: [] };
        /** 后端返回菜单树 */
        menu.children = response.data;
        this.menuOptions.push(menu);
      });
    },
    /** 转换菜单数据结构 */
    normalizer(node) {
      console.log('菜单2;',node.mid)
      if (node.children && !node.children.length) {
        delete node.children;
      }
      return {
        id: node.mid,
        label: node.name,
        children: node.children
      };
    }
  }
};
</script>

Logo

前往低代码交流专区

更多推荐