vue3+element-plus实现动态菜单和动态路由动态按钮的后台权限管理系统(前后端分离)

目录

前言

前期准备

布局准备

路由准备

路由守卫准备

登录

获取用户数据

动态路由

第一步:获取路由数据

第二步:处理路由格式

第三步:创建动态路由

动态菜单

获取菜单数据

渲染菜单

动态按钮

获取按钮数据

权限对比

el-tabs美化

分析功能:

实现

后端响应的用户权限数据

源码地址


前言

本篇文章旨在从零搭建一个动态路由动态菜单的后台管理系统初始环境,如果您只有个别地方没有实现,那么可以根据目录选择性的阅读您所需的内容

本文在原基础上进行了一些改动(改善了原先不太合理的地方),有什么问题可以在评论区提出。

本文使用技术:vue3,pinia状态管理,element-plus,axios(这些需要读者自己安装,本文没有指出)

注意在本文章中,有些方法并没有粘出来,比如一些发送请求的方法,因为没有必要,需要根据读者自己的情况进行修改,如果你看到一些方法并没有写出来,那多半就是发送请求的方法。

效果预览

不同角色登录的菜单不同

前期准备

布局准备

本文需要使用axios,路由,pinia,安装element-plus,并且本文vue3是基于js而非ts的,这些环境如何搭建不做描述,需要读者自己完成。

在这之前我们需要一个布局

假设我们的home.vue组件,是如下

<template>
  <div class="common-layout">
    <el-container>
      <el-aside width="200px">
        <div>
          <!-- 菜单侧栏导航栏 -->
          <menus></menus>
        </div>
      </el-aside>
      <el-container>
        <el-header>
          <div class="head_class">
            <!-- 头部内容 可以忽略-->
            <Crumbs></Crumbs>
            <div>
              <el-button type="primary" text @click="logOut">注销</el-button>
            </div>
          </div>
        </el-header>
        <el-main>
          <!-- 主要内容 -->
          <div>
                <router-view></router-view>
          </div>
        </el-main>
      </el-container>
    </el-container>
  </div>
</template>

当然上面的router-view是第二级路由了,第一级路由是在app.vue文件里面

路由准备

准备静态路由(路由安装这里就不说了):在router目录下创建index.js文件(本文中的所有文件名自己都可以随意命名),这个文件主要是初始化路由,并且规定一些静态路由也就是公共路由。

路由守卫准备

在router目录下创建一个permission.js,这个文件用于创建路由守卫,并且动态路由在此文件中创建(如果你觉得麻烦,可以统统都弄到index.js也行)。

注意:如果你选择在router目录下的permission文件中定义路由守卫,记得在min.js中引入!

登录

凡事要从登录开始是吧?咱们登录这里没什么特殊的,就两点:1、保存后端传的令牌token;2、跳转主页

获取用户数据

我们需要获取用户的所有权限数据,例如:菜单权限数据(树形结构),路由数据,权限码数据

为什么获取用户信息的方法不放在登录那里,却放在路由守卫中呢?因为用户权限数据要求要即时准确,定义在路由守卫中可以保证每次请求的权限数据都是最新的(页面每刷新一次,都会重新获取权限数据)

路由守卫代码(router/permission.js)

import router from "@/router/index";
import {useMenusStore} from "@/store/permission";
import Home from '@/views/home/index.vue'
import NProgress from 'nprogress' // 进度条
import 'nprogress/nprogress.css'//进度条样式
import {getToken} from "@/util/token/getToken";
//路由白名单(静态路由)
const Whitelist = ['/', '/login', '/error', '/404']
//全局路由守卫
router.beforeEach((to, from, next) => {
    NProgress.start();
    const store = useMenusStore()
    const hasetoken = getToken('token')//通过判断token是否存在来确定用户是否登录
    if (to.path == "/login" && hasetoken) {
        //如果已经登录却还在访问登录页,直接跳转首页
        next('/home')
    }
    if (Whitelist.indexOf(to.path) == -1) {
        //当前路由没有在白名单里面
        if (store.RouterList.length == 0 && hasetoken) {//store.RouterList.length == 0:通过判断RouterList数组的长度来确定是否已经生成动态路由

            //已经登录,但是还没有获取路由数据,或者说丢失了路由数据,立即获取用户信息,并创建动态路
            // getPermission:该方法会发送请求获取用户的权限数据(一般情况会将获取用户信息和生成动态路由步骤分开操作,我这里比较懒,就直接写在一起了)
            store.getPermission().then(() => {
                const routerlist = store.RouterList//用户的路由数据
                //创建动态路由
                router.addRoute(//该方法只会添加一个路由(因为我的子路由都在这个home路由下)如果你要添加的路由是一个数组,请使用addRoutes函数
                    {
                        path: '/home',
                        name: 'Home',
                        component: Home,
                        redirect: '/home/homepage',
                        children: routerlist//注意routerlist的数据必须是格式化成路由格式了才行,具体格式化方法就在pinia中的setRouterList方法
                    }
                )
               return  next({...to, replace: true})//确保路由加载完毕,注意这里他不会继续往下执行了,他会重新进入路由守卫。为了避免误会,这里就直接return
                //而这第二次在进入这个守卫时动态路由已经添加完成,RouterList的长度已经不为0了,所以就不会进入这个判断里面,转而执行下面的eles
            })
        } else {
            /*两种情况进入:1:已经获取了路由(一定有token),2,没有登录(一定没有路由)*/
            NProgress.done()
            // if (hasetoken && to.matched.length != 0) {
            if (hasetoken && router.hasRoute(to.name)) {
                /*to.matched.length === 0或者router.hasRoute(to.name)判断当前输入的路由是否具有,即使登陆了,如果输入不能访问的路由也要404*/
                /*已经获取了路由并且具备访问路由权限,需要放行*/
                next()
            } else {
                next('/404')
            }/*没有登录或者登录了但是访问路由不对,需要404,也可以选择跳转登录页*/
        }
    } else {
        NProgress.done()
        /*当前路由在白名单里面,直接放行*/
        next()
    }
})

在上面代码中getPermission方法会发送请求获取用户的权限数据,并将结果存入pinia中

这个方法定义在pinia中

pinia的具体代码

import {defineStore} from "pinia" // 定义容器
import {logout} from '@/api/index'
import {getLoginUserPermissions} from '@/api/home/permission/menus'

export const useMenusStore = defineStore('permission', {
    /**
     * 存储全局状态,类似于vue2中的data里面的数据
     * 1.必须是箭头函数: 为了在服务器端渲染的时候避免交叉请求导致数据状态污染
     * 和 TS 类型推导
     */
    state: () => {
        return {
            menuList: [],//菜单信息(树结构)
            menuCodes: [],//权限码
            RouterList: [],//路由信息
            roleIds: [],//角色Id
            tabsList: [{id: 1, name: '首页', path: 'homepage'}],
            tabsActive: 'homepage'//默认激活页
        }
    },
    /**
     * 用来封装计算属性 有缓存功能  类似于computed
     */
    getters: {},
    /**
     * 编辑业务逻辑  类似于methods
     */
    actions: {
        //处理路由格式:data-->后端传入的路由信息
        setRouterList(data) {
            data.forEach(item => {
                //定义一个对象-->routerInfo格式化后端传入的路由信息
                let routerInfo = {
                    path: item.path,//组件访问路径
                    name: item.name,
                    meta: {name: item.name},
                    component: () => import(`@/views/${item.linkUrl}`),//组件的位置

                }
                this.RouterList.push(routerInfo)
            })
        },
        getPermission() {
            //获取用户的权限信息
            return getLoginUserPermissions().then(res => {
                this.setRouterList(res.data.data.routers)//设置路由数据
                this.menuCodes = res.data.data.permissionsCode//设置权限码集合
                this.menuList = res.data.data.menus//设置菜单数据
            })
        },
        logout() {
            /*注销登录*/
            return logout().then(res => {
                if (res.data.code == 200) {
                    window.sessionStorage.clear()//清除本地所有缓存
                    this.$reset()//清除状态管理所有数据
                    return true
                } else return false
            })
        },
        setTabs(node) {
            this.tabsList.push(node)
        },
        delTabs(node) {
            /*返回和node不相同的元素,就相当于把相同的元素删掉了*/
            this.tabsList = this.tabsList.filter(item => {
                if (item.path == node) {
                    return false
                } else return true
            })
        },
        setActive(value) {
            this.tabsActive = value
        },
    },
    // 持久化设置
    persist: {
        enabled: true,	//开启
        storage: sessionStorage,	//修改存储位置
        key: 'permissions',	//设置存储的key,在这里是存在sessionStorage时的键
        paths: ['tabsList', 'tabsActive'],//指定要持久化的字段
    },
})

获取用户数据已经完成,如果你想看看后端响应的用户权限数据是什么格式,可以滑到文章底部

动态路由

步骤:1、获取路由数据;2、处理路由格式;3、在路由守卫中添加路由

第一步:获取路由数据

已经实现(在上面【获取用户数据】部分:RouteList)

第二步:处理路由格式

:我后端传来的数据都在home路由下,并且不再有子路由,所以我的routerInfo中没有children属性,如果读者添加的路由在此处还有嵌套路由,请在该对象中添加children属性,并且处理好数据。

当前步骤的代码在pinia中,在上面已经给处具体代码,这里就不再粘上了

第三步:创建动态路由

当前代码在路由守卫中,具体代码在上面已经给出,此处仅限讲解

在路由守卫中使用Router提供的addrRoute函数动态创建路由

1、addRoute函数只会添加一个根路由(可以有子路由),我在此处只有一个根路由home,如果你需要动态创建多个根路由,请遍历你的路由数组,循环使用addRoute添加(vue2可以使用addRoutes,但是Vue Router 4已经将其移除);

2、当第一次执行next({...to, replace: true})时,他不会继续往下执行了,会直接重新进入守卫(为了方便理解,我在此处直接return),在第二次进入就不要再进入这个动态添加路由的判断了,否则就会死循环白屏。所以这几个判断很重要,没处理好就会jj

3、为什么要在路由守卫中创建动态路由?那岂不是每次经过守卫都会重新创建路由吗?是的,因为每次页面刷新或者路由跳转,动态添加的路由都会丢失!而定义在守卫中就能确保,添加的路由丢失了立马就能创建回来

4、不要想着走捷径,直接对静态路由数组的数据进行增加而实现动态路由,这样是行不通的,因为路由初始化完成后,你对路由数组直接修改,router是无法感知到路由数据变化的,所以必须使用人家提供的addRoute或者addRoutes函数来实现

动态菜单

步骤:1、获取菜单数据;2、渲染菜单

获取菜单数据

已经实现(在【获取用户数据】部分:menuList)

渲染菜单

使用明确:element-plus中menu主要分为两种状态:有子菜单(目录)和没有子菜单,我们需要根据这两种分别渲染

我们就可以粘代码修修改就可以了,

新建一个vue文件:我们假定这个组件是menus.vue

<template>
  <div style="height: 750px">
    <el-row>
      <el-col :offset="8">
        <el-image style="width: 40px;height: 40px" :src="require('@/assets/website_logo.png')"></el-image>
      </el-col>
    </el-row>
    <el-menu
        background-color="#545c64"
        class="el-menu-vertical-demo"
        :default-active="tabsActive"
        text-color="#fff"
        router
    >
      <menu-tree :menuList="menuList"></menu-tree>
    </el-menu>
  </div>
</template>

<script setup>
import MenuTree from "@/components/menu/MenuTree";
import {useMenusStore} from "@/store/permission";
import {storeToRefs} from 'pinia'

let {menuList, tabsActive} = storeToRefs(useMenusStore())
</script>

<style scoped>
.icon_class {
  width: 20px;
  height: 20px;
}

.menu_title {
  margin-left: 20px;
}

.el-menu {
  min-height: 100%;
}

.el-menu-vertical-demo:not(.el-menu--collapse) {
  width: 200px;
  min-height: 90%;
}

</style>

你可能会问:怎么代码这么少?而且只有<el-menu>标签?渲染菜单的<el-sub-menu>和<el-menu-item>标签呢?

由于我们会采用递归,如果把所有代码都写到一个组件里面,那么所有元素都会重复了,比如说上面这个“后台管理系统”标题会被重复显示,那么这肯定是不行的,所以我们需要将渲染菜单的具体操作放在另一个vue文件里面,具体的代码就在这个<menuTree> 组件里面。当然我们还需要将pinia中存入的菜单信息传给这个组件,让其渲染

下面我们来看看这个<menuTree >组件,

<template>
  <div>
    <template v-for="item in prop.menuList" :key="item.path">
      <!--      分为两种方式渲染:有子菜单和没有子菜单-->
      <el-sub-menu
          :index="item.path"
          v-if="item.nodeType === 1"
          class="child-item"
      >
        <template #title>
          <div>
            <el-icon v-show="item.iconId!=null">
              <el-image :src="$imgBaseUrl+item.iconId" alt="" class="icon_class">
                <template #error>
                  <div></div>
                </template>
              </el-image>
            </el-icon>
            <span>{{ item.name }}</span>
          </div>
        </template>
        <!--        有子菜单的继续遍历(递归)-->
        <MenuTree :menuList="item.children"></MenuTree>
      </el-sub-menu>
      <!--      没有子菜单-->
      <el-menu-item :index="item.path" v-if="item.nodeType===2" @click="clickOnMenu(item)" class="el-menu-item">
        <el-icon v-show="item.iconId!=null">
          <el-image :src="$imgBaseUrl+item.iconId" alt="" class="icon_class">
            <template #error>
              <div></div>
            </template>
          </el-image>
        </el-icon>
        <span>{{ item.name }}</span>
      </el-menu-item>
    </template>
  </div>
</template>

<script setup>
import {useMenusStore} from "@/store/permission";
// eslint-disable-next-line no-undef
let prop = defineProps(['menuList'])
let store = useMenusStore()

function clickOnMenu(node) {
  let hasNode = store.tabsList.filter(item => item.id === node.id)
  if (hasNode.length === 0) {
    store.setTabs(node)
  }
  store.setActive(node.path)
}
</script>

在上面的代码中:

1、我使用nodeType的值来判断是否存在子菜单。因为我在数据库存储的菜单字段中规定nodeType=1就是目录,具有子菜单,=2就是没有子菜单,是页面,=3就是按钮。你也可以通过其他的方法来判断,比如说,菜单children的长度是否大于零。如果有子菜单,那么我们还需要再一次遍历,也就是递归,无论我们有多少级菜单都能够遍历出来,当然,理论上层次深了会爆栈,但是正经人谁会弄那么深是吧?

2、我采用父子组件的传递数据的方法把菜单数据传入渲染菜单的子组件中。这个数据定义在pinia中,那为什么子组件不直接获取,而是通过父组件传入呢?因为递归参数是要当前运算的结果,这个运算的数据一点是变化的(不变化就直接死循环了),所以要父组件传入

动态按钮

步骤:1、获取按钮数据;2、权限对比

获取按钮数据

已经实现(在上面的【获取用户数据】部分:menuCode)

权限对比

在需要的页面中引入,并判断

场景:假设我们的商品上架只有具备“goods-up”权限码的用户才能使用

indexof表示判断一个数组是否包含一个元素,如果包含就返回索引,不包含就返回-1。

完结!

el-tabs美化

使用element-plus的tabs标签

分析功能:

  1. 点击菜单栏的时候,如果没有这个tab页,就新增,如果有就跳转
  2. 点击tabs的时候跳转对应的路由
  3. tabs可以被删除

实现

我们从官网上复制代码,放在哪里呢?

<el-tabs type="border-card">
    <el-tab-pane label="User">User</el-tab-pane>
    <el-tab-pane label="Config">Config</el-tab-pane>
    <el-tab-pane label="Role">Role</el-tab-pane>
    <el-tab-pane label="Task">Task</el-tab-pane>
  </el-tabs>

放在路由展示的地方,还记得我们前面的【前期准备--布局准备】那里吗?

<div class="common-layout">
    <el-container>
      <el-aside width="200px">
        <div>
          <!-- 菜单侧栏导航栏 -->
          <menus></menus>
        </div>
      </el-aside>
      <el-container>
        <el-header>
          <div class="head_class">
            <!-- 头部 -->
            <Crumbs></Crumbs>
            <div>
              <el-button type="primary" text @click="logOut">注销</el-button>
            </div>
          </div>
        </el-header>
        <el-main>
          <!-- 内容 -->
          <div>
            <el-tabs type="border-card">
                <el-tab-pane label="User">
                    
                </el-tab-pane>
                <router-view></router-view>
          </div>
        </el-main>
      </el-container>
    </el-container>
  </div>

    
 </el-tabs>

注:不要把<router-view>标签放进循环里面,会出现一些问题。比如生命周期函数会多次调用等

当然,只是这样是不行的,由于这个标签页是动态的,我们需要一个数组来代替它的源数据,这个数组呢并不是固定的。在点击菜单栏的时候就需要添加一个,我们也可以在tabs上面点击删除按钮,删除一个标签页。也就是说,对这个数组的操作是跨页面跨组件的。所以我们优先考虑定义在pinia中。

在前面的pinia基础上,我们新增一个tabsList

并提供一个新增和删除的方法

此处代码在上面已经给出(pinia中)

 现在,我们有了这样一个数组,就可以遍历生成了,我们在之前的基础上,修改这部分代码,

从pinia中引入tabsList数组,在标签页中遍历它

 <div>
            <el-tabs type="border-card">
              <el-tab-pane :label="item.name" :key="item.name"
                           v-for="item in tabsList" :name="item.path"  >
                
              </el-tab-pane>
                <router-view></router-view>
            </el-tabs>
          </div>



import {useMenusStore} from '@/store/permission'
import {useRouter} from "vue-router";
import {ref} from "vue";
import { storeToRefs } from 'pinia'

const store = useMenusStore()
const router = useRouter()
let {tabsList,tabsActive}=storeToRefs(store)

分析 

  1. storeToRefs方法在这里的主要作用是将pinia中的数据变为响应式。如果不使用这个,数组修改后,标签页并不会随之渲染出来
  2. 使用标签页的name属性绑定tabsList存储的路由路径

现在我们能够便遍历显示了,但是还不够,我们需要点击菜单的时候,想这个数组新增元素

我们找到前面的渲染菜单的组件,在没有子菜单的菜单上面添加点击事件

 function clickOnMenu(node){
//判断tabList数组是否有当前点击的页面
      let hasNode=store.tabsList.filter(item=>item.path==node.path)
      if (hasNode.length==0 || hasNode==null){
//如果数组里面不存在,新增一个
        store.setTabs(node)
      }
      
    }

当点击菜单的时候,就需要新增一个标签页,如果已经存在,就直接跳转到这个标签页。那么问题来了,在上面的代码中,我们能够新增标签页了,但是如何跳转到这个标签页呢

我们需要借助标签页的这个属性:

 name属性标识了每一个标签页(tab-pane),而tab的v-model属性绑定了当前激活的标签页。tab绑定的值是哪一个tab-pane的name,那么哪一个tab-pane就是激活页

先不要着急定义这个激活值,我们仔细分析:在点击菜单的时候,不仅要向数组中添加一个新的标签页,还要切换到这个激活的标签页。也就是说,对这个激活值执行赋值的动作是在菜单组件中,而绑定(获取)这个激活值是在展示标签页的组件中,这又是一个跨页面跨组件的共享数据,我们仍然优先定义在pinia中。我们给他一个初始值,在进入页面的时候默认打开首页,

仍然需要提供一个修改的接口

代码在上面已经给出,这里就不在重复了

我们有了这个激活值,就需要绑定,并且考虑到在点击不同的标签页的时候需要切换路由,我们需要在激活值改变的时候,就切换到这个激活值对应的路由上。

修改标签页的代码,我们将数组中的路由路径(path)绑定为name值,closable表示当前标签是否可以被删除,我这里只有首页是不能被删除的

<el-main>
          <!-- 内容 -->
          <div>
            <el-tabs type="border-card" v-model="tabsActive" @tab-change="gotoActive" @tab-remove="closeTabs">
              <el-tab-pane :label="item.name" :key="item.name"
                           v-for="item in tabsList" :name="item.path" :closable="item.isClose==1" >
                <router-view></router-view>
              </el-tab-pane>
            </el-tabs>
          </div>
        </el-main>

<script setup>
import menus from "@/components/menu/index.vue";
import {ElMessage, ElMessageBox} from 'element-plus'
import Crumbs from "@/components/CommonComponent/crumbs/index.vue";
import {useMenusStore} from '@/store/permission'
import {useRouter} from "vue-router";
import {ref} from "vue";
import { storeToRefs } from 'pinia'
import {getToken} from "@/util/token/getToken";

const store = useMenusStore()
const router = useRouter()
let {tabsList,tabsActive}=storeToRefs(store)

function logOut() {
  ElMessageBox.confirm('确认退出登录?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning',
  }).then(async () => {
    /*点击的确认*/
    const b = await store.logout()
    if (b) {
      router.push('/login')
    } else {
      ElMessage({
        showClose: true,
        message: '注销失败',
        type: 'error',
      })
    }
  }).catch(() => {
    /*点击的取消*/
  })
}

function closeTabs(name){
  //判断删除的是否是活动页,如果是则删除并跳到首页,
  if(name==store.tabsActive){
    store.setActive('homepage')
    console.log("删除的是当前页",tabsActive)
  }
  store.delTabs(name)
}
function gotoActive(name) {
  /*防止退出登录时清楚缓存时触发(active的值发生变动)*/
  if (getToken("token")) {
    store.setActive(name)
    router.push(name)
  }
}

</script>

还记得之前我们菜单点击那里吗?那里只实现了添加标签页的功能还没有实现跳转路由的功能,我们现在只需要在点击事件里面添加切换激活值就行了

后端响应的用户权限数据

{
    "code": 200,
    "msg": "OK",
    "data": {
        "menus": [
            {
                "id": "1",
                "name": "首页",
                "menuCode": "homepage",
                "parentId": "0",
                "nodeType": 2,
                "sort": 0,
                "linkUrl": "home/content/index.vue",
                "iconId": "1",
                "level": 0,
                "path": "homepage",
                "createTime": null,
                "isClose": 0,
                "children": [],
                "iconInfo": null
            },
            {
                "id": "5",
                "name": "店铺管理",
                "menuCode": "store_manage",
                "parentId": "0",
                "nodeType": 2,
                "sort": 20,
                "linkUrl": "home/content/ShopManage/index.vue",
                "iconId": "6",
                "level": 0,
                "path": "shop",
                "createTime": "2023-07-13 16:29:38",
                "isClose": 1,
                "children": [],
                "iconInfo": null
            },
            {
                "id": "6",
                "name": "商品管理",
                "menuCode": "goods_manage",
                "parentId": "0",
                "nodeType": 1,
                "sort": 1,
                "linkUrl": "",
                "iconId": "5",
                "level": 0,
                "path": "goods",
                "createTime": "2023-07-13 16:42:17",
                "isClose": 1,
                "children": [
                    {
                        "id": "20305",
                        "name": "商品发布",
                        "menuCode": "goods_add",
                        "parentId": "6",
                        "nodeType": 2,
                        "sort": null,
                        "linkUrl": "home/content/GoodsManage/AddGoods.vue",
                        "iconId": null,
                        "level": null,
                        "path": "addgoods",
                        "createTime": "2023-08-26 14:39:21",
                        "isClose": 1,
                        "children": [],
                        "iconInfo": null
                    },
                    {
                        "id": "20311",
                        "name": "spu管理",
                        "menuCode": "spu-manage",
                        "parentId": "6",
                        "nodeType": 2,
                        "sort": null,
                        "linkUrl": "home/content/GoodsManage/SpuManage.vue",
                        "iconId": null,
                        "level": null,
                        "path": "spu-manage",
                        "createTime": "2023-10-05 22:07:56",
                        "isClose": 1,
                        "children": [],
                        "iconInfo": null
                    },
                    {
                        "id": "20312",
                        "name": "sku管理",
                        "menuCode": "sku-manage",
                        "parentId": "6",
                        "nodeType": 2,
                        "sort": null,
                        "linkUrl": "home/content/GoodsManage/SkuManage.vue",
                        "iconId": null,
                        "level": null,
                        "path": "sku-manage",
                        "createTime": "2023-10-08 20:41:06",
                        "isClose": 1,
                        "children": [],
                        "iconInfo": null
                    }
                ],
                "iconInfo": null
            },
            {
                "id": "7",
                "name": "订单系统",
                "menuCode": "order_system",
                "parentId": "0",
                "nodeType": 1,
                "sort": 2,
                "linkUrl": "",
                "iconId": "10",
                "level": 0,
                "path": "order",
                "createTime": "2023-07-13 16:43:15",
                "isClose": 1,
                "children": [],
                "iconInfo": null
            },
            {
                "id": "8",
                "name": "库存系统",
                "menuCode": "warehouse_system",
                "parentId": "0",
                "nodeType": 1,
                "sort": 3,
                "linkUrl": "",
                "iconId": "4",
                "level": 0,
                "path": "warehouse",
                "createTime": "2023-07-13 16:44:36",
                "isClose": 1,
                "children": [],
                "iconInfo": null
            }
        ],
        "permissionsCode": [
            "homepage",
            "store_manage",
            "goods_manage",
            "order_system",
            "warehouse_system",
            "goods_add",
            "spu-manage",
            "sku-manage"
        ],
        "routers": [
            {
                "id": "1",
                "name": "首页",
                "menuCode": "homepage",
                "parentId": "0",
                "nodeType": 2,
                "sort": 0,
                "linkUrl": "home/content/index.vue",
                "iconId": "1",
                "level": 0,
                "path": "homepage",
                "createTime": null,
                "isClose": 0,
                "children": null,
                "iconInfo": null
            },
            {
                "id": "5",
                "name": "店铺管理",
                "menuCode": "store_manage",
                "parentId": "0",
                "nodeType": 2,
                "sort": 20,
                "linkUrl": "home/content/ShopManage/index.vue",
                "iconId": "6",
                "level": 0,
                "path": "shop",
                "createTime": "2023-07-13 16:29:38",
                "isClose": 1,
                "children": null,
                "iconInfo": null
            },
            {
                "id": "20305",
                "name": "商品发布",
                "menuCode": "goods_add",
                "parentId": "6",
                "nodeType": 2,
                "sort": null,
                "linkUrl": "home/content/GoodsManage/AddGoods.vue",
                "iconId": null,
                "level": null,
                "path": "addgoods",
                "createTime": "2023-08-26 14:39:21",
                "isClose": 1,
                "children": null,
                "iconInfo": null
            },
            {
                "id": "20311",
                "name": "spu管理",
                "menuCode": "spu-manage",
                "parentId": "6",
                "nodeType": 2,
                "sort": null,
                "linkUrl": "home/content/GoodsManage/SpuManage.vue",
                "iconId": null,
                "level": null,
                "path": "spu-manage",
                "createTime": "2023-10-05 22:07:56",
                "isClose": 1,
                "children": null,
                "iconInfo": null
            },
            {
                "id": "20312",
                "name": "sku管理",
                "menuCode": "sku-manage",
                "parentId": "6",
                "nodeType": 2,
                "sort": null,
                "linkUrl": "home/content/GoodsManage/SkuManage.vue",
                "iconId": null,
                "level": null,
                "path": "sku-manage",
                "createTime": "2023-10-08 20:41:06",
                "isClose": 1,
                "children": null,
                "iconInfo": null
            }
        ]
    }
}

 

源码地址

我的毕业设计: 后面再说 (gitee.com)

进入这个仓库找到

  • 43
    点赞
  • 170
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

潜水阿宝

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

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

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

打赏作者

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

抵扣说明:

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

余额充值