vue下自定义权限tree组件

1,072 阅读4分钟
最近项目中遇到了不一样的需求,是element UI中不能满足的。主要是项目中的权限太多,想要让展示btn的部分行内显示;菜单的部分一行显示;直接改elemantUI效果不太理想。主要是追求细节的我,在细节上耿耿于怀,痛下决心决定自己写一个权限树的组件
项目需求随时变,自定义组件才万变!!

最终成果展示

image-20200730112537299

考虑到组件引用,我尝试了一下,在组件中应用自己,先考虑一个组件,来表示各种情况,分为:行内显示和一行显示,需要用的属性有:树结构数据(data),每项节点的数据(item),是否扩展子节点(expand),并定义一个组件的值(value);
首先固定数据结构:
1.采用对象数组的结构,方便遍历;
2.为了增强每一个点的的唯一标示符id的可读性,采用字符串,子id中带上父节点的id信息:
     device
        device.list
           device.list.delete
3.增加属性type,区分时属性展示页面内的按钮--'btn',还是菜单的路由---'menu',
4.name是国际化使用,role是对应的权限名

最终,确定数据结构,如下:

export const Authorities: IAuthorityData[] = [{
    id: 'device',
    name: 'authority.device.label',
    role: 'MENU_DEVICE',
    type: 'menu',
    children: [
      {
        id: 'device.list',
        name: 'authority.device.list.label',
        role: 'MENU_DEVICE_LIST',
        type: 'menu',
        children: [
          {
            id: 'device.list.delete',
            name: 'authority.device.list.delete.label',
            role: 'BTN_DEVICE_DELETE',
            type: 'btn',
            children: []
          }, {
            id: 'device.list.modify',
            name: 'authority.device.list.modify.label',
            role: 'BTN_DEVICE_MODIFY',
            type: 'btn',
            children: []
          }
        ]
    }]
}]

先定义一个auth-tree-item.vue

<template>
  <!-- 行内显示内容 -->
  <span
    v-if="showInlineBlock"
    style="padding-left: 25px;"
    class="inline-block"
  >
    <span
      style="margin-right: 20px;"
    />
    <el-checkbox
      v-model="checked"
      :label="item.name"
      style="margin-right: 10px;width:90px;"
      @change="checkChange($event)"
    />
  </span>

  <div
    v-else
    style="padding-left: 15px"
  >
    <!-- 每行显示内容 -->
    <div>
      <!-- 可下拉显示图标  -->
      <i
        v-if="hasChild"
        :class="[showChild?'el-icon-caret-bottom':'el-icon-caret-right']"
        style="margin-left: 7px;margin-right: 10px;"
        @click="toggleChild"
      />
      <span
        v-else
        style="margin-right: 30px;"
      />
      <el-checkbox
        v-model="checked"
        :label="item.name"
        style="margin-right: 10px;"
        @change="checkChange($event)"
      />
    </div>
    <div
      v-if="hasChild"
      v-show="showChild"
      style="padding-left: 20px"
    >
      <auth-tree-item
        v-for="child in item.children"
        :key="child.id"
        v-model="$store.state.common.checkKeys"
        :data="data"
        :item="child"
        :father-type="type"
        :expand="expand"
      />
    </div>
  </div>
</template>
<script src='./auth-tree-item.ts'></script>

先定义一个auth-tree-item.ts

<script lang="ts">
import { Component, Vue, Prop, Watch, Model } from 'vue-property-decorator'
import { IAuthorityData } from '@/api/system/auth/types'
import { CommonModule } from '@/store/modules/common'

  @Component({
    name: 'AuthTreeItem'
  })
  
  export default class extends Vue {
    @Model('change', { type: Array, default: '' }) private value!: string[]
    @Prop({ required: true }) private data!: IAuthorityData[]
    @Prop({ required: true }) private item!: IAuthorityData
    @Prop({ required: true }) private expand!: boolean
    @Prop({ default: '' })private fatherType!: string
    // type
    private type = ''
    // 行内显示
    private showInlineBlock = false
    // 是否有子元素
    private hasChild = false
    // 是否显示子元素
    private showChild = false
    // 元素是否被选中
    private checked = false
    // 暂存选中的框的id 数组
    private initArr: string[] = []
    // 是否需要联动check 父子元素
    private checkChild: boolean = true
    private checkFather: boolean = true
    private uncheckChilren: boolean = true
    private uncheckFather: boolean = true
    private chilrenIds: string[] = []
    private parentIds: string[]=[]
    // 同级元素,包含自己
    private silbingIds: string[] = []
    // 所有子孙元素集
    private currentChildrenIds: string[] = []
    // 所有子元素集
    private currentChildIds: string[] = []
    private currentParentIds: string[]=[]
    // 同级元素,包含自己
    private currentSilbingIds: string[] = []
    private roleIdObject:any = {}
    
    created() {
      this.init()
      this.dataInit()
    }
    
    /**
     * 根据初始化组件
     */
    init() {
      this.hasChild = this.item.children.length > 0
      this.showChild = this.expand
      this.type = this.item.type
      this.$nextTick(() => {
        this.showInlineBlock = this.type === 'btn' && this.item.children.length === 0
      })
      // 初始化相关的子元素,父元素
      this.initRelative(this.item.id)
    }
    
     /**
     * 根据组建的传过来的数据渲染页面
     */
    private dataInit() {
      this.initArr = this.$store.state.common.checkKeys
      let arr: string[] = this.$store.state.common.checkKeys
      let id: string = this.item.id
      this.checked = arr.includes(id)
    }
  }

定义交互相关的函数,选中某一节点时触发复选框的change事件,然后对需要选中的节点进行扩充。

  private checkChange(event:Event) {
      // arr 保存之前的数据,以免选中后之前的数据this.value会消失,需要用新的变量保存
      let arr: string[] = this.initArr
      // 如果选中值为true
      if (this.checked) {
        // 如果check需要联动父子元素
        if (this.checkFather) {
          // 如果check需要联动父元素
          arr = Array.from(new Set([...arr, ...this.parentIds]))
        }
        if (this.checkChild) {
          // 如果check需要联动父元素
          arr = Array.from(new Set([...arr, ...this.chilrenIds]))
        }
        arr.push(this.item.id)
        // 如果选中值为false
      } else {
        arr.splice(arr.indexOf(this.item.id), 1)
        // 如果未选中,需要联动,需要uncheck子元素
        if (this.uncheckChilren) {
          this.chilrenIds.forEach((id:string) => {
            if (arr.includes(id)) {
              arr.splice(arr.indexOf(id), 1)
            }
          })
        }
        // 如果节点属于menu, 且需要联动元素uncheck父元素
        if (this.type === 'menu' && this.uncheckFather) {
          let reverseParentIds = [...this.parentIds].reverse()
          // 遍历所有父元素,uncheck父元素
          reverseParentIds.forEach((parentId:string, i: number) => {
            if (arr.includes(parentId)) {
              // 判断是否去除该父元素
              let level = reverseParentIds.length - 1 - i
              if (!this.keepSelf(arr, parentId, level)) {
                arr.splice(arr.indexOf(parentId), 1)
              }
            }
          })
        }
      }
      this.initArr = arr
    }
    
    private toggleChild() {
      this.showChild = !this.showChild
    }

目前只是数据变化,并没有渲染到页面,人渲染到页面需要执行相关init

/**
     * 选中数据处理--ids
     */
    @Watch('initArr', { deep: true })
    changeIds(newValue:[], oldValue:[]) {
      CommonModule.SET_CHECKED(newValue)
    }
    
     /**
     * 根据组建的传过来的关键数值变化,更新页面
     */
    @Watch('value', { deep: true })
    changeValue(newValue: string[], oldValue: string[]) {
      this.dataInit()
    }

附上相关的方法:

 /**
  * 根据组件的相关的子元素的ids,父元素的ids
  */
 private initRelative(id: string) {
   this.currentParentIds = []
   this.getParents(id)
   // this.currentSilbingIds = []
   // this.getSiblings(this.data, this.currentParentIds[this.currentParentIds.length - 1], this.currentParentIds, 0)
   // 生成需要的子孙id数组
   let findArr = [...this.currentParentIds]
   findArr.push(id)
   this.currentChildrenIds = []
   this.currentChildIds = []
   this.getChild(this.data, id, 0, findArr.length - 1)
   this.parentIds = this.currentParentIds
   this.chilrenIds = this.currentChildrenIds
 }
 
 /**
  * 获取某元素下的子元素
  * @param data<IAuthorityData[]> 遍历的树结构
  * @param id<string> 某元素的id
  * @param level 元素所处的层级
  * @param lastLevel 父元素所在的层级
  */
 private getChild(data:IAuthorityData[], id: string, level: number, lastLevel: number) {
   data.forEach((item: IAuthorityData) => {
     if (id.indexOf(item.id) > -1) {
       item.children.length > 0 && this.getChild(item.children, id, level + 1, lastLevel)
     } else if (item.id.indexOf(id) > -1) {
       level !== lastLevel && this.currentChildrenIds.push(item.id)
       item.children.length > 0 && this.getChild(item.children, id, level + 1, lastLevel)
       level === lastLevel + 1 && this.currentChildIds.push(item.id)
     }
   })
 }

 /**
  * 获取设置父元素数组
  */
 private getParents(id:string) {
   let splitArr = this.item.id.split('.')
   splitArr.forEach((item:string, i:number) => {
     // 最后一个代表自身属性,不参与字符串拼接
     if (i < splitArr.length - 1) {
       let str = splitArr[0]
       for (let j = 0; j < i + 1; j++) {
         // 如果大于0,需要拼接
         if (j > 0) {
           str = str + '.' + splitArr[j]
         }
       }
       this.currentParentIds.push(str)
     }
   })
 }

/**
  * 获取兄弟(包括自己)元素
  */
 private getSiblings(data:IAuthorityData[], id: string, splitArr:string[], level: number) {
   // 如果父元素数组不为[]
   if (splitArr.length > 0) {
     data.forEach((item: IAuthorityData) => {
       // 如果含有同级中的id 且欲匹配id含有遍历项id内容
       if (item.id.indexOf(splitArr[level]) > -1 && id.indexOf(item.id) > -1) {
         if (level === splitArr.length - 1) {
           item.children.forEach((child: IAuthorityData) => {
             this.currentSilbingIds.push(child.id)
           })
         } else if (level < splitArr.length - 1) {
           this.getSiblings(item.children, id, splitArr, level + 1)
         }
       }
     })
     // 如果父元素数组为[],表示为一级菜单
   } else {
     data.forEach((item: IAuthorityData) => {
       this.currentSilbingIds.push(item.id)
     })
   }
 }

最后在需要引用该组件的地方引用:

<auth-tree-item
     v-for="item in authorityTreeData"
     :key="item.id"
     v-model="$store.state.common.checkKeys"
     :data="authorityTreeData"
     :item="item"
     expand="true"
 />
 
 import AuthTreeItem from '../components/auth-tree-item.vue'
 @Component({
   name: 'Role',
   components: {
     AuthTreeItem
   }
 })
 
/**
* 获取权限树
*/
private async getAuthorityTree() {
 // const { data } = await getAuthorityTree()
 const data = Authorities
 this.regularAuthority(data)
 this.authorityTreeData = data
}

/**
* 国际化展示树
*/
private regularAuthority(data: Array<any>) {
 for (let i in data) {
   data[i].name = this.$t(data[i].name) as string
   if (data[i].children.length !== 0) {
     this.regularAuthority(data[i].children)
   }
 }
}  

/*
* 递归遍历权限树的数据,生成由属性role和id生成的键值对对象dictionary({role:id})
* @param treeData<IAuthorityData[]> 权限树数据
* @patam result 存储的键值对对象dictionary
*/
function getArr(treeData: IAuthorityData[], result: {[key:string]: string}): {[key:string]: string} {
treeData.forEach((item: IAuthorityData) => {
 result[item.role] = item.id
 if (item.children && item.children.length > 0) {
   getArr(item.children, result)
 }
})
return result
}
export const roleIdObject: {[key:string]: string} = getArr(Authorities, {})

 // 根据ids转化为role names
private transferToNames(ids: string[]) {
 let nameArr: string[] = []
 ids.forEach((id: string) => {
   for (let key in roleIdObject) {
     if (roleIdObject[key] && id === roleIdObject[key]) {
       nameArr.push(key)
     }
   }
 })
 return nameArr
}

// 根据ids转化为role names
private transferToIds(names: string[]) {
 let idArr: string[] = []
 names.forEach((name: string) => {
   if (name in roleIdObject) {
     idArr.push(roleIdObject[name])
   }
 })
 return idArr
}

欢迎大佬指导不好的地方,在此感谢!!