最近项目中遇到了不一样的需求,是element UI中不能满足的。主要是项目中的权限太多,想要让展示btn的部分行内显示;菜单的部分一行显示;直接改elemantUI效果不太理想。主要是追求细节的我,在细节上耿耿于怀,痛下决心决定自己写一个权限树的组件
项目需求随时变,自定义组件才万变!!
最终成果展示
考虑到组件引用,我尝试了一下,在组件中应用自己,先考虑一个组件,来表示各种情况,分为:行内显示和一行显示,需要用的属性有:树结构数据(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
}
欢迎大佬指导不好的地方,在此感谢!!