1. 程式人生 > 前端設計 >打造一款適合自己的快速開發框架-前端篇之選擇樹元件設計與實現

打造一款適合自己的快速開發框架-前端篇之選擇樹元件設計與實現

前言

任何業務系統都可能會涉及到對樹型類資料的管理,如選單管理、組織機構管理等。而在對樹型類資料進行管理的時候一般都需要選擇父節點,雖然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模式,所以直接使用資料庫的列表資料是不能直接展示成樹狀結構的。這就需要對原始的資料進行轉換,常見的轉換方式有兩種,其實就是由哪一端處理。

  1. 後端按照前端需要的資料結構返回
  2. 後端只返回原始資料,由前端自行轉換成標準的樹型結構

本文為了方便,採用的是前端轉換的方式,其實不管是哪一端,都可以寫成通用的方法,只是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)進行組裝,目前只做了單選的,如後續場景需要再考慮支援多選。

專案原始碼地址

  • 後端

gitee.com/mldong/mldo…

  • 前端

gitee.com/mldong/mldo…

相關文章

打造一款適合自己的快速開發框架-先導篇

打造一款適合自己的快速開發框架-前端腳手架搭建

打造一款適合自己的快速開發框架-前端篇之登入與路由模組化

打造一款適合自己的快速開發框架-前端篇之框架分層及CURD樣例

打造一款適合自己的快速開發框架-前端篇之字典元件設計與實現

打造一款適合自己的快速開發框架-前端篇之下拉元件設計與實現