任何业务系统均可能会涉及到对树型类数据的管理,如菜单管理、组织机构管理等。而在对树型类数据进行管理的时候通常都须要选择父节点,虽然elementui也有树型组件,可是若是直接使用,要完成该功能,须要编写的代码量也仍是很多,因此咱们要想更方便的时候,就得须要在其基础上进行进一步的封装。前端
数型组件通常都须要必定规范的数据结构。vue
以下效果:java
├── 节点1
├── 节点11
└── 节点111
└── 节点12
├── 节点2
├── 节点21
└── 节点22
复制代码
其标准的数据结构:node
[
{
"id": 1,
"name": "节点1",
"children": [
{
"id": 11,
"name": "节点11",
"children": [
{
"id": 111,
"name": "节点111",
}
]
},
{
"id": 12,
"name": "节点12"
}
]
},
{
"id": 2,
"name": "节点2",
"children": [
{
"id": 21,
"name": "节点21"
},
{
"id": 22,
"name": "节点22"
}
]
}
]
复制代码
数据库的存储通常结构为:git
[
{ "id": 1, "parentId": 0, "name": "节点1" },
{ "id": 11, "parentId": 1, "name": "节点11" },
{ "id": 111, "parentId": 11, "name": "节点111" },
{ "id": 12, "parentId": 1, "name": "节点12" },
{ "id": 2, "parentId": 0, "name": "节点2" },
{ "id": 21, "parentId": 2, "name": "节点21" },
{ "id": 22, "parentId": 2, "name": "节点22" }
]
复制代码
elementui的树型组件是不支持id/parentId模式的,须要组装成children模式,因此直接使用数据库的列表数据是不能直接展现成树状结构的。这就须要对原始的数据进行转换,常见的转换方式有两种,其实就是由哪一端处理。数据库
本文为了方便,采用的是前端转换的方式,其实无论是哪一端,均可以写成通用的方法,只是java这边写成通用方法没有js方便,因此本框架选择在前端进行该转换动做。json
接口仍是通用的查询接口,区别在于入参须要把pageSize调大一点,以菜单为例后端
请求地址api
{{api_base_url}}/sys/menu/list
bash
数据类型
application/json
请求示例:
{
"pageNum": 1,
"pageSize": 10000
}
复制代码
响应示例:
{
"code": 0,
"msg": "查询菜单成功",
"data": {
"pageNum": 1,
"pageSize": 10000,
"recordCount": 16,
"totalPage": 1,
"rows": [{
"id": 1,
"parentId": 0,
"name": "系统设置",
"sort": 10.0,
"routeName": "sys",
"icon": "sys",
"isShow": 2,
"createTime": "2020-06-25 21:05:01",
"updateTime": "2020-06-25 21:05:03",
"isDeleted": 1
}, {
"id": 2,
"parentId": 1,
"name": "菜单管理",
"sort": 1.0,
"routeName": "sys:menu:index",
"isShow": 2,
"createTime": "2020-06-25 21:06:34",
"updateTime": "2020-06-25 21:06:36",
"isDeleted": 1
}, {
"id": 3,
"parentId": 1,
"name": "用户管理",
"sort": 2.0,
"routeName": "sys:user:index",
"isShow": 2,
"createTime": "2020-06-25 21:07:05",
"updateTime": "2020-06-25 21:07:09",
"isDeleted": 1
}, {
"id": 4,
"parentId": 1,
"name": "角色管理",
"sort": 3.0,
"routeName": "sys:role:index",
"isShow": 2,
"createTime": "2020-06-25 21:07:37",
"updateTime": "2020-06-25 21:07:41",
"isDeleted": 1
}, {
"id": 5,
"parentId": 1,
"name": "字典管理",
"sort": 4.0,
"routeName": "sys:dict:index",
"isShow": 2,
"createTime": "2020-06-25 21:08:08",
"updateTime": "2020-06-25 21:08:11",
"isDeleted": 1
}, {
"id": 6,
"parentId": 0,
"name": "内容管理",
"sort": 11.0,
"routeName": "cms",
"icon": "cms",
"isShow": 2,
"createTime": "2020-06-25 21:09:05",
"updateTime": "2020-06-25 21:09:07",
"isDeleted": 1
}, {
"id": 7,
"parentId": 6,
"name": "栏目管理",
"sort": 1.0,
"routeName": "sys:category:index",
"isShow": 2,
"createTime": "2020-06-25 21:09:36",
"updateTime": "2020-06-25 21:09:39",
"isDeleted": 1
}, {
"id": 8,
"parentId": 6,
"name": "模型管理",
"sort": 2.0,
"routeName": "sys:model:index",
"isShow": 2,
"createTime": "2020-06-25 21:10:23",
"updateTime": "2020-06-25 21:10:25",
"isDeleted": 1
}, {
"id": 9,
"parentId": 6,
"name": "文章管理",
"sort": 3.0,
"routeName": "sys:article:index",
"isShow": 2,
"createTime": "2020-06-25 21:10:50",
"updateTime": "2020-06-25 21:10:53",
"isDeleted": 1
}, {
"id": 10,
"parentId": 0,
"name": "订单管理",
"sort": 12.0,
"routeName": "oms",
"isShow": 2,
"createTime": "2020-06-25 21:11:29",
"updateTime": "2020-06-25 21:11:31",
"isDeleted": 1
}, {
"id": 11,
"parentId": 10,
"name": "订单列表",
"sort": 1.0,
"routeName": "oms:order:index",
"isShow": 2,
"createTime": "2020-06-25 21:11:55",
"updateTime": "2020-06-25 21:11:57",
"isDeleted": 1
}, {
"id": 12,
"parentId": 10,
"name": "订单设置",
"sort": 2.0,
"routeName": "oms:orderSetting:index",
"isShow": 2,
"createTime": "2020-06-25 21:12:15",
"updateTime": "2020-06-25 21:12:19",
"isDeleted": 1
}, {
"id": 13,
"parentId": 0,
"name": "商品管理",
"sort": 13.0,
"routeName": "pms",
"icon": "pms",
"isShow": 2,
"createTime": "2020-06-25 21:14:02",
"updateTime": "2020-06-25 21:14:05",
"isDeleted": 1
}, {
"id": 14,
"parentId": 13,
"name": "商品分类",
"sort": 1.0,
"routeName": "pms:productCategory:index",
"isShow": 2,
"createTime": "2020-06-25 21:16:05",
"updateTime": "2020-06-25 21:16:07",
"isDeleted": 1
}, {
"id": 15,
"parentId": 13,
"name": "商品列表",
"sort": 2.0,
"routeName": "pms:product:index",
"isShow": 2,
"createTime": "2020-06-25 21:16:36",
"updateTime": "2020-06-25 21:16:39",
"isDeleted": 1
}, {
"id": 16,
"parentId": 13,
"name": "品牌管理",
"sort": 3.0,
"routeName": "pms:brand:index",
"isShow": 2,
"createTime": "2020-06-25 21:16:57",
"updateTime": "2020-06-25 21:17:01",
"isDeleted": 1
}]
}
}
复制代码
暂时定几个经常使用的参数,后续可能还会有追加
参数名 | 类型 | 默认值 | 说明 |
---|---|---|---|
url | String | undefined | 接口地址 |
isEdit | Boolean | false | 是否编辑模式 |
value | String, Number,Array | undefined | 绑定的值 |
multiple | Boolean | false | 是否多选(预留) |
size | String | medium | 组件大小medium/small/mini |
placeholder | String | 请选择 | 占位符 |
dialogTitle | String | 请选择 | 弹窗标题 |
dialogWidth | String | 30% | 弹窗宽度 |
defaultExpandAll | Boolean | false | 是否默认展开全部节点 |
├── src
├── components/m
├── SelectTree
└── index.vue
├── utils
└── util.js
├── views
├── dashboard
└── index.vue
└── main.js
复制代码
src/components/m/Select/index.vue
选择树组件
<template>
<div class="m-select-tree">
<el-input readonly :size="size" :placeholder="placeholder" v-model="mValue">
<el-button slot="append" icon="el-icon-search" @click="openDialog"></el-button>
</el-input>
<el-dialog :title="dialogTitle" :visible.sync="isOpenDialog" :width="dialogWidth" append-to-body @close="handleCancel">
<el-tree
:props="defaultProps"
:data="treeData"
node-key="id"
highlight-current
:default-expand-all="defaultExpandAll"
@current-change="handleCurrentChange"
ref="tree">
</el-tree>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="handleSubmit">确 定</el-button>
<el-button @click="handleCancel">取 消</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import request from '@/utils/request'
export default {
name: 'MSelectTree',
props: {
url: { // 接口地址
type: String,
default: undefined
},
isEdit: { // 是否编辑模式
type: Boolean,
default: false
},
// 绑定的值
value: {
type: [String, Number, Array],
default: undefined
},
multiple: { // 是否多选
type: Boolean,
default: false
},
size: { // medium/small/mini
type: String,
default: 'medium'
},
placeholder: { // 占位符
type: String,
default: '请选择'
},
dialogTitle: { // 弹窗标题
type: String,
default: '请选择'
},
dialogWidth: { // 弹窗宽度
type: String,
default: '30%'
},
defaultExpandAll: { // 是否默认展开全部节点
type: Boolean,
default: false
}
},
data() {
return {
mValue: '根节点', // 显示的文本值
isOpenDialog: false, // 是否打开弹窗
treeData: [], // 树型结构
defaultProps: { // elementui树型组件默认属性配置
children: 'children',
label: 'name'
}
}
},
watch: {
value(n, o) { // 监听父组件值变更,子组件也要变更
if (o === undefined || o === 0) {
this.refreshView()
}
}
},
created() {
if (this.isEdit) {
this.requestData()
}
},
methods: {
requestData() {
if (this.treeData.length) {
this.$nextTick(() => {
// dom更新完成再设置当前选中项
this.refreshView()
})
return
}
if (this.url) {
request({
url: this.url,
method: 'post',
data: {
pageNum: 1,
pageSize: 10000
}
}).then(res => {
if (res.code === 0) {
this.treeData = [
{
id: 0,
name: '根节点',
children: []
},
// 这里使用工具方法将id/parentId数据结构转成children结构
...this.$util.getTree(res.data.rows)
]
this.$nextTick(() => {
// dom更新完成再设置当前选中项
this.refreshView()
})
}
})
}
},
openDialog() { // 打开弹出框
this.isOpenDialog = true
this.requestData()
},
handleSubmit() {
this.isOpenDialog = false
},
handleCancel() {
this.isOpenDialog = false
},
// 处理当前选中节点变化时触发的事件
handleCurrentChange(data) {
// 修改显示
this.mValue = data.name
// 子组件值变化要经过父组件
this.$emit('input', data.id)
},
// 刷新页面元素
refreshView() {
if (this.$refs.tree) {
if (this.value === undefined) {
this.$refs.tree.setCurrentKey(0)
} else {
this.$refs.tree.setCurrentKey(this.value)
}
}
if (this.isEdit) {
var nodes = this.treeData.filter(item => {
return item.id === this.value
})
if (nodes.length) {
this.mValue = nodes[0].name
}
}
}
}
}
</script>
复制代码
src/utils/util.js
工具类,树型结构处理。好久以前写的了,使用的仍是递归,还没进行优化。
/** * 根据key复制对象 * @param {}} src * @param {*} dest */
export const copy = function(src, dest) {
const res = {}
Object.keys(dest).forEach(key => {
res[key] = src[key]
})
return res
}
/** * 获取菜单树 * @param {} nodes id/parentId格式数据 */
export const getTree = (nodes) => {
var root = []
for (var i = 0; i < nodes.length; i++) {
if (Number(nodes[i]['parentId']) <= 0) {
root.push(nodes[i])
}
}
return buildTree(nodes, root)
}
/** * 构建菜单树 * @param {*} nodes id/parentId格式数据 * @param {*} root 树节点 */
export const buildTree = (nodes, root) => {
for (var i = 0; i < root.length; i++) {
root[i].title = root[i].name
var children = []
for (var k = 0; k < nodes.length; k++) {
if (nodes[k]['parentId'] === root[i]['id']) {
children.push(nodes[k])
}
}
if (children.length !== 0) {
root[i]['children'] = children
buildTree(nodes, children)
}
}
return root
}
/** * 先序遍历树 * @param {*} tree 标准树结构 * @param {*} level 层级 */
export const preorder = (tree, level) => {
var array = []
for (var i = 0; i < tree.length; i++) {
tree[i].level = level
if (level === 1) {
// tree[i].expand = true
}
if (tree[i]['children'] != null) {
tree[i].leaf = false
array.push(tree[i])
array = array.concat(preorder(tree[i]['children'], level + 1))
} else {
tree[i].leaf = true
array.push(tree[i])
}
tree[i]['children'] = []
}
return array
}
/** * 树型结构先序遍历转列表 * @param {*} datas 标准树结构数据 */
export const tranDataTreeToTable = (datas) => {
return preorder(getTree(datas), 1)
}
export const getNode = (datas, id) => {
const res = datas.filter(item => {
return item.id === id
})
if (res.length) {
return res[0]
} else {
return 0
}
}
/** * 获取全部父级 * @param {} datas * @param {*} id */
export const getParents = (datas, id) => {
const res = []
const node = getNode(datas, id)
if (node) {
res.push(node)
}
for (let i = 0, len = datas.length; i < len; i++) {
const item = datas[i]
if (item.id === node.parentId) {
res.push(item)
res.push(...getParents(datas, item.id))
break
}
}
return res
}
/** * 获取全部子元素 * @param {*} datas * @param {*} id * @param {*} containParent 是否包含父id */
export const getChildren = (datas, id, containParent) => {
const res = []
if (containParent === undefined) {
containParent = true
}
const node = getNode(datas, id)
if (node) {
if (containParent) {
res.push(node)
}
} else {
return res
}
for (let i = 0, len = datas.length; i < len; i++) {
const item = datas[i]
if (item.parentId === id) {
res.push(item)
res.push(...getChildren(datas, item.id, false))
}
}
return res
}
复制代码
src/main.js
主入口全局注册自定义组件,这里也用了require.context,代码片断,这里简单的对驼峰进行了-转换
// 处理自定义组件全局注册
const files = require.context('./components/m', true, /\.vue$/)
files.keys().forEach((routerPath) => {
const componentName = routerPath.replace(/^\.\/(.*)\/index\.\w+$/, '$1')
const value = files(routerPath)
Vue.component('m' + componentName.replace(/([A-Z])/g, '-$1').toLowerCase(), value.default)
}, {})
复制代码
src/views/dashboard/index.vue
这里提供了使用样例:
选择单个
<m-select-tree dialog-title="请选择父菜单" v-model="parentId" url="sys/menu/list" value-key="id" label-key="name"></m-select-tree>
复制代码
选择单个-修改模式
<m-select-tree dialog-title="请选择父菜单" v-model="form.parentId" is-edit url="sys/menu/list" value-key="id" label-key="name"></m-select-tree>
复制代码
js片断
export default {
name: 'Dashboard',
data() {
return {
form: {
parentId: undefined
},
parentId: undefined
}
},
created() {
// 模拟修改异步更新
setTimeout(() => {
this.$set(this.form, 'parentId', 1)
}, 2000)
}
}
复制代码
本文的选择树组件使用了elementui的三个组件(Input/Dialog/Tree)进行组装,目前只作了单选的,如后续场景须要再考虑支持多选。