vue3+element-plus实现动态菜单和动态路由动态按钮的后台权限管理系统(前后端分离)
目录
前言
本篇文章旨在从零搭建一个动态路由动态菜单的后台管理系统初始环境,如果您只有个别地方没有实现,那么可以根据目录选择性的阅读您所需的内容
本文在原基础上进行了一些改动(改善了原先不太合理的地方),有什么问题可以在评论区提出。
本文使用技术: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: [],
tabsActive: ''//默认激活页
}
},
/**
* 用来封装计算属性 有缓存功能 类似于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: 100vh;background-color: #545C64">
<!-- 项目logo图 -->
<div style="display: flex;justify-content: center">
<el-image style="width: 40px;height: 40px" :src="require('@/assets/website_logo.png')"></el-image>
</div>
<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>标签呢?
由于我们会采用递归,如果把所有代码都写到一个组件里面,那么所有元素都会重复了,比如说上面这个项目logo会被重复显示,那么这肯定是不行的,所以我们需要将渲染菜单的具体操作放在另一个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)">
<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>
<style scoped>
.icon_class {
width: 20px;
height: 20px;
}
.child-item .el-menu-item {
background-color: #1F2D3D !important;
}
.child-item .el-menu-item:hover {
background-color: #001528 !important;
}
.child-item .el-submenu__title {
background-color: #1F2D3D !important;
}
.child-item .el-submenu__title:hover {
background-color: #001528 !important;
}
</style>
在上面的代码中:
1、我使用nodeType的值来判断是否存在子菜单。因为我在数据库存储的菜单字段中规定nodeType=1就是目录,具有子菜单,=2就是没有子菜单,是页面,=3就是按钮。你也可以通过其他的方法来判断,比如说,菜单children的长度是否大于零。如果有子菜单,那么我们还需要再一次遍历,也就是递归,无论我们有多少级菜单都能够遍历出来,当然,理论上层次深了会爆栈,但是正经人谁会弄那么深是吧?
2、我采用父子组件传递数据的方法把菜单数据传入渲染菜单的子组件中。这个数据定义在pinia中,那为什么子组件不直接获取,而是通过父组件传入呢?因为递归参数是要当前运算的结果,这个运算的数据一点是变化的(不变化就直接死循环了),所以要父组件传入。
动态按钮
步骤:1、获取按钮数据;2、权限对比
获取按钮数据
已经实现(在上面的【获取用户数据】部分:menuCode)
权限对比
在需要的页面中引入,并判断
场景:假设我们的商品上架只有具备“goods-up”权限码的用户才能使用
indexof表示判断一个数组是否包含一个元素,如果包含就返回索引,不包含就返回-1。
完结!
el-tabs美化
使用element-plus的tabs标签
分析功能:
- 点击菜单栏的时候,如果没有这个tab页,就新增,如果有就跳转
- 点击tabs的时候跳转对应的路由
- 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)
分析
- 因为我是使用解构的方式会导致得到的数据失去响应式,所以使用storeToRefs。当然如果你不想使用storeToRefs那么就不用解构赋值的方式也没什么问题
- 使用标签页的name属性绑定tabsList存储的路由路径
现在我们能够便遍历显示了,但是还不够,我们需要点击菜单的时候,想这个数组新增元素
我们找到前面的渲染菜单的组件,在没有子菜单的菜单上面添加点击事件(代码上面已经给出<MenuTree>组件)
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(node) {
//回调函数的默认参数是el-tab-pane的name值,也就是我们绑定的path
//判断删除的是否是活动页,如果是则删除并跳到前一页,
if (node === store.tabsActive) {
//获取当前页的索引,此处无需担心是首元素,因为我的首元素“首页”是设置的不能关闭,就不会触发这个方法,如果你的全部元素都能被删除,记得处理数组下标越界的问题
let index = store.tabsList.findIndex(item => item.path === node)
//激活值改为当前索引的前一个
store.setActive(store.tabsList[index - 1].path)
}
store.delTabs(node)
}
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
}
]
}
}
源码地址
进入这个仓库找到
更多推荐
所有评论(0)