任何業務系統均可能會涉及到對樹型類數據的管理,如菜單管理、組織機構管理等。而在對樹型類數據進行管理的時候通常都須要選擇父節點,雖然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)進行組裝,目前只作了單選的,如後續場景須要再考慮支持多選。