打造一款適合自己的快速開發框架-前端篇之選擇樹元件設計與實現
阿新 • • 發佈:2020-06-29
前言
任何業務系統都可能會涉及到對樹型類資料的管理,如選單管理、組織機構管理等。而在對樹型類資料進行管理的時候一般都需要選擇父節點,雖然elementui也有樹型元件,但是如果直接使用,要完成該功能,需要編寫的程式碼量也還是不少,所以我們要想更方便的時候,就得需要在其基礎上進行進一步的封裝。
選擇樹元件設計
資料結構
數型元件一般都需要一定規範的資料結構。
如下效果:
├── 節點1
├── 節點11
└── 節點111
└── 節點12
├── 節點2
├── 節點21
└── 節點22
複製程式碼
其標準的資料結構:
[
{
"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"
}
]
}
]
複製程式碼
資料庫的儲存一般結構為:
[
{ "id": 1,"parentId": 0,"name": "節點1" },{ "id": 11,"parentId": 1,"name": "節點11" },{ "id": 111,"parentId": 11,"name": "節點111" },{ "id" : 12,"name": "節點12" },{ "id": 2,"name": "節點2" },{ "id": 21,"parentId": 2,"name": "節點21" },{ "id": 22,"name": "節點22" }
]
複製程式碼
資料結構處理
elementui的樹型元件是不支援id/parentId模式的,需要組裝成children模式,所以直接使用資料庫的列表資料是不能直接展示成樹狀結構的。這就需要對原始的資料進行轉換,常見的轉換方式有兩種,其實就是由哪一端處理。
- 後端按照前端需要的資料結構返回
- 後端只返回原始資料,由前端自行轉換成標準的樹型結構
本文為了方便,採用的是前端轉換的方式,其實不管是哪一端,都可以寫成通用的方法,只是java這邊寫成通用方法沒有js方便,所以本框架選擇在前端進行該轉換動作。
介面說明
介面還是通用的查詢介面,區別在於入參需要把pageSize調大一點,以選單為例
請求地址
{{api_base_url}}/sys/menu/list
資料型別
application/json
請求示例:
{
"pageNum": 1,"pageSize": 10000
}
複製程式碼
響應示例:
{
"code": 0,"msg": "查詢選單成功","data": {
"pageNum": 1,"pageSize": 10000,"recordCount": 16,"totalPage": 1,"rows": [{
"id": 1,"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,"name": "選單管理","sort": 1.0,"routeName": "sys:menu:index","createTime": "2020-06-25 21:06:34","updateTime": "2020-06-25 21:06:36",{
"id": 3,"name": "使用者管理","sort": 2.0,"routeName": "sys:user:index","createTime": "2020-06-25 21:07:05","updateTime": "2020-06-25 21:07:09",{
"id": 4,"name": "角色管理","sort": 3.0,"routeName": "sys:role:index","createTime": "2020-06-25 21:07:37","updateTime": "2020-06-25 21:07:41",{
"id": 5,"name": "字典管理","sort": 4.0,"routeName": "sys:dict:index","createTime": "2020-06-25 21:08:08","updateTime": "2020-06-25 21:08:11",{
"id": 6,"name": "內容管理","sort": 11.0,"routeName": "cms","icon": "cms","createTime": "2020-06-25 21:09:05","updateTime": "2020-06-25 21:09:07",{
"id": 7,"parentId": 6,"name": "欄目管理","routeName": "sys:category:index","createTime": "2020-06-25 21:09:36","updateTime": "2020-06-25 21:09:39",{
"id": 8,"name": "模型管理","routeName": "sys:model:index","createTime": "2020-06-25 21:10:23","updateTime": "2020-06-25 21:10:25",{
"id": 9,"name": "文章管理","routeName": "sys:article:index","createTime": "2020-06-25 21:10:50","updateTime": "2020-06-25 21:10:53",{
"id": 10,"name": "訂單管理","sort": 12.0,"routeName": "oms","createTime": "2020-06-25 21:11:29","updateTime": "2020-06-25 21:11:31",{
"id": 11,"parentId": 10,"name": "訂單列表","routeName": "oms:order:index","createTime": "2020-06-25 21:11:55","updateTime": "2020-06-25 21:11:57",{
"id": 12,"name": "訂單設定","routeName": "oms:orderSetting:index","createTime": "2020-06-25 21:12:15","updateTime": "2020-06-25 21:12:19",{
"id": 13,"name": "商品管理","sort": 13.0,"routeName": "pms","icon": "pms","createTime": "2020-06-25 21:14:02","updateTime": "2020-06-25 21:14:05",{
"id": 14,"parentId": 13,"name": "商品分類","routeName": "pms:productCategory:index","createTime": "2020-06-25 21:16:05","updateTime": "2020-06-25 21:16:07",{
"id": 15,"name": "商品列表","routeName": "pms:product:index","createTime": "2020-06-25 21:16:36","updateTime": "2020-06-25 21:16:39",{
"id": 16,"name": "品牌管理","routeName": "pms:brand:index","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,Array],multiple: { // 是否多選
type: Boolean,size: { // medium/small/mini
type: String,default: 'medium'
},placeholder: { // 佔位符
type: String,default: '請選擇'
},dialogTitle: { // 彈窗標題
type: String,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)進行組裝,目前只做了單選的,如後續場景需要再考慮支援多選。
專案原始碼地址
- 後端
- 前端