封装第三方组件(22)重构一下后台管理的页面(组件)关系
之前用 vue-router 做了一个后台管理的页面,虽然实现了功能,但是感觉比较麻烦。
原版
我做了一个menu.json,然后依据这个json来绑定菜单,同时还要去设置路由。
这个就比较麻烦,而且为了可以保持组件的状态,同时还有支持组件的扩展性,所以代码结构也比较绕,容易绕晕的那种。
于是想重构一下。
页面结构
想了一下,对于后台管理来说,路由不是太重要,原本我也只是做了一级路由,再下一层的干脆把路由给抛掉了,既然这样,不如干脆不用 router 了。自己做一个简易版的就可以了。
页面结构中规中矩的设计,上面是横向导航和其他信息,比如登录、用户状态等。
左面是菜单,右面是“工作区”。
工作区上面是动态tab,下面是列表组件。
列表组件包含:查询、按钮、列表、分页等,每个部分都是单独的组件。
菜单与动态tab
一般点击菜单会触发路由,然后由路由加载对应的组件。
现在不用路由了,那么怎么办呢?
tab状态
我们先定义一个状态来存放 tab 相关的各种数据
// tab 的状态
tabState () {
return {
trigger: '', // 触发者,菜单触发,要恢复到初始状态,tab触发,保持状态,还有url触发
menu: [], // 菜单的数据
tabData: [], // 存放 tab 信息的数组,绑定tab用
activeModuleId: 0 // 当前激活的 menu/tab 的 id
}
}
-
tabData
v-for 的方式创建 tab 。 -
activeModuleId
当前的激活的tab,也是点击了哪个菜单。默认使用同样的标识,也就是模块ID。 -
menu
存放 menu.json 的内容,备用。避免每次都读取json文件,好吧我知道有缓存,但是不还得import 嘛。 -
trigger
这是一个要不要重置列表状态的问题。
定义菜单的meta
用 json 文件表达一个菜单:
{
"title": "演示项目",
"menu": [
{
"id": 100,
"parentId": 0,
"title": "系统管理",
"componentKind": "group",
"icon": "el-icon-location"
},
{
"id": 101,
"parentId": 0,
"title": "支持平台",
"kind": "group",
"componentKind": "group",
"icon": "el-icon-location"
},
{
"id": 120,
"parentId": 100,
"title": "角色管理",
"componentKind": "list",
"icon": "el-icon-location"
},
{
"id": 121,
"parentId": 100,
"title": "操作日志",
"componentKind": "list",
"icon": "el-icon-location"
},
{
"id": 122,
"parentId": 100,
"title": "变更日志",
"componentKind": "list",
"icon": "el-icon-location"
}
]
}
没有采用嵌套的方式,而是基于关系型数据库做的,因为打算放在数据库里面保存。
绑定菜单
使用 el-menu 实现菜单功能。只支持二级。
<el-menu
:default-active="index"
class="el-menu-vertical-demo"
@select="select"
>
<el-submenu
v-for="item in menu.filter(a => a.parentId===0)"
:key="'menu_' + item.id"
:index="item.id"
>
<template #title>
<i :class="item.icon"></i>
<span>{{item.title}}</span>
</template>
<el-menu-item-group>
<el-menu-item
v-for="item2 in menu.filter(a => a.parentId===item.id)"
:key="'submenu_' + item2.id"
:index="item2.id"
>{{item2.title}}
</el-menu-item>
</el-menu-item-group>
</el-submenu>
</el-menu>
设置单击事件。
// 二级菜单被选中
const select = (index, indexPath) => {
// 设置状态,交给 tab 组件处理,tab 会监听
tabState.trigger = 'menu'
tabState.activeModuleId = parseInt(index)
location.hash = '#' + tabState.activeModuleId
isMenuSetHash = true
}
单击菜单,记入 tab 的状态,等待 tab 的监听。顺便做个 hash 的简易路由。
创建tab
// 监听 单击的菜单,即模块ID
watch(() => tabState.activeModuleId, (moduleId) => {
// 判断是否已经生成标签
if (tabData.findIndex((item) => item.id === moduleId) === -1) {
// 没有标签,创建新标签
const m = tabState.menu.filter(a => a.id === moduleId)[0]
if (typeof m !== 'undefined') {
tabData.push({
id: moduleId, // 模块ID
componentKind: m.componentKind, // 组件字典类型
title: m.title // 标签
})
}
}
},
{ immediate: true })
监听模块ID,然后判断是否已经创建了tab,然后嘛,不用多说了。
<el-tabs
v-model="tabState.activeModuleId"
type="card"
@tab-click="tabClick"
>
<el-tab-pane label="桌面" :name="0">
桌面
</el-tab-pane>
<el-tab-pane
v-for="(item, index) in tabState.tabData"
:key="'tab_' + item.id"
:label="item.title"
:name="item.id"
>
<template #label>
<span>{{item.title}}
<i class="el-icon-error" @click.stop="tabRemove(item.id, index)" ></i>
</span>
</template>
<component
:is="componentKind[item.componentKind]"
:moduleId="item.id"
>
</component>
</el-tab-pane>
</el-tabs>
比较简单的循环。
动态组件与异步组件
tab 搞定后,对应的组件如何加载呢?vue提供了动态组件和异步组件,这是一个非常漂亮的功能。
异步组件
我们先定义几个异步组件:
import { defineAsyncComponent } from 'vue'
/**
* 注册动态组件
*/
export default {
master: defineAsyncComponent(() => import('../base-page/list-form.vue')),
list: defineAsyncComponent(() => import('../base-page/list.vue')),
mod: defineAsyncComponent(() => import('../base-page/form.vue')),
form: defineAsyncComponent(() => import('../base-page/form.vue'))
}
然后做一个动态组件
<component
:is="componentKind[item.componentKind]"
:moduleId="item.id"
>
这样就搞定了,支持扩展组件,需要啥组件加入啥组件即可。
这个看着是不是有点眼熟?
对,就是 router 里面路由设置时使用的动态路由,去掉 defineAsyncComponent 就一模一样的。
为啥不放到菜单里面呢?放在菜单里面,那不相当于我又自己做了一个路由嘛。
好吧,菜单首先是一个json,没法放这个异步组件,然后不方便复用,菜单加载的大多都是 list,菜单里面直接写list很方便,如果写路径,那就太麻烦了。
最后,我真的不想自己做一个路由。
支持扩展组件
默认是使用固定的几个组件,但是也可以使用其他组件,只需要在动态组件那里注册一下,设定一个key,就可以使用了。