<template>
<view :class="{ 'next-tree': !toEject, 'next-tree-eject': toEject }" v-if="showTree">
<view v-if="toEject && needMask" :class="[{ show: showTree }, 'next-tree-mask']" @tap="_cancel(1)"></view>
<view :class="[{ show: showTree }, 'next-tree-cnt']">
<view class="next-tree-bar" v-if="needBtn && componentType != 'customer'">
<view class="next-tree-bar-cancel" :style="{ color: cancelColor }" hover-class="hover-c" @tap="_cancel"
>取消</view
>
<view class="next-tree-bar-title" :style="{ color: titleColor }">{{ title }}</view>
<view class="next-tree-bar-confirm" :style="{ color: confirmColor }" hover-class="hover-c" @tap="_confirmBtn"
>确定</view
>
</view>
<view class="next-tree-bar-box" v-if="componentType == 'customer'">
<view class="next-tree-bar-customer" :style="{ color: titleColor }">{{ title || '客户所属行业' }}</view>
<uni-icons class="next-tree-bar-customer-icon" type="close-outline" size="13" color="#999999" @tap="_cancel" />
</view>
<slot></slot>
<view
class="next-tree-view"
:class="{
customer: componentType == 'customer'
}"
:style="nextTreeStyleCp"
>
<scroll-view class="next-tree-view-sc" :scroll-y="true" :style="nextTreeScrollStyleCp">
<slot name="empty"></slot>
<block v-for="(item, index) in treeList" :key="index">
<view
class="next-tree-item"
:style="[
{
marginLeft:
componentType != 'customer' ? item.rank * marginLeftNum + 'px' : item.rank == 0 ? '8px' : '0px',
paddingLeft: componentType != 'customer' ? '0px' : (item.rank - 1) * marginLeftNum + 20 + 'px',
zIndex: item.rank * -1 + 50
}
]"
:class="{
border: showBorder(item.show, item.lastRank),
show: item.show,
last: item.lastRank,
showchild: item.showChild,
open: item.open,
first: !item.parentIdNub
}"
>
<view
class="next-tree-label"
:class="{
checked: item.checked && componentType != 'customer',
disabled: item.disabled
}"
>
<uni-icons
v-if="!item.lastRank && componentType != 'customer'"
class="next-tree-icon"
:size="iconSize"
:type="item.lastRank ? lastIcon : item.showChild ? currentIcon : defaultIcon"
:color="treeIconColor"
@tap.stop="_treeItemTap(item, index)"
/>
<uni-icons
v-if="item.lastRank && zipList"
class="next-tree-icon"
:size="zipListIconSize"
:type="imgTypeFn(item.type)"
@tap.stop="_treeItemTap(item, index)"
/>
<uni-icons
v-if="item.parentIdNub && componentType == 'customer'"
class="next-tree-icon-zhexian"
:size="8"
color="#CCCCCC"
:type="'zhexian'"
@tap.stop="_treeItemSelect(item, index)"
/>
<view
class="next-tree-icon-not-selected"
v-if="!item.checked && !item.indeterminate && componentType == 'customer'"
@tap.stop="_treeItemSelect(item, index)"
>
</view>
<uni-icons
v-if="componentType == 'customer' && item.checked"
class="next-tree-icon"
:size="iconSize"
:type="'all-selected'"
:color="'#0F56D5'"
@tap.stop="_treeItemSelect(item, index)"
/>
<uni-icons
v-if="componentType == 'customer' && item.indeterminate"
class="next-tree-icon"
:color="'#0F56D5'"
:size="iconSize"
:type="'indeterminate'"
@tap.stop="_treeItemSelect(item, index)"
/>
<!-- {{indeterminate}} -->
<!-- <view v-if="!item.lastRank" class="next-tree-icon">{{
item.lastRank ? '' : item.showChild ? 'X' : '>'
}}</view> -->
<view v-if="!zipList" @tap.stop="_treeItemSelect(item, index)"> {{ item.name }}</view>
<view v-if="zipList" class="zipList_box" @tap.stop="_treeItemSelect(item, index)">
<view class="zipList_box_one">{{ item.name }}</view>
<view v-if="item.lastRank" class="zipList_box_two">文件大小:{{ getSize(item.size) }}</view>
</view>
</view>
<view class="next-tree-check" @tap.stop="_treeItemTap(item, index)">
<view class="next-tree-check-yes" v-if="item.checked && !zipList" :class="{ radio: !multiple }">
<!-- <text class="icon-text">✔</text> -->
<uni-icons v-if="componentType != 'customer'" :type="checkIcon" color="#0f56d5" />
</view>
<view class="next-tree-check-yes" v-if="item.lastRank && zipList" :class="{ radio: !multiple }">
<uni-icons type="forward" color="#CCCCCC" size="12" />
</view>
<view class="next-tree-check-no" v-else :class="{ radio: !multiple }"></view>
</view>
<uni-icons
v-if="!item.lastRank && componentType == 'customer'"
class="next-tree-icon"
:size="iconSize"
:type="item.open ? 'up' : 'down'"
color="#333"
@tap.stop="_treeItemSelect(item, index)"
/>
</view>
</block>
</scroll-view>
<view class="next-tree-view-bottom" v-if="componentType == 'customer'">
<view class="next-tree-view-bottom-cancel" hover-class="hover-c" @tap="_cancel">取消</view>
<view class="next-tree-view-bottom-confirm" hover-class="hover-c" @tap="_confirmBtn">确定</view>
</view>
</view>
</view>
<view v-if="!toEject && needMask" :class="[{ show: showTree }, 'next-tree-mask-eject']" @tap="_cancel(1)"></view>
</view>
</template>
<script>
/**
* zy-tree 树组件
* @description 树组件
* @property {Array} treeData 组件数据{needShowChild: true,当前项与父级展开,当前项子级不展开
needOpen: true,当前项子级与父级展开,checked: true默认选中的值,irrevocable: true不能取消选中,选中的值父级自动打开}
* @property {String} valueKey 选中值键名
* @property {String} labelKey 展示值键名
* @property {String} childrenKey 子类数据键名
* @property {String} title 标题
* @property {Boolean} multiple 是否可以多选
* @property {Boolean} selectParent = 是否可以选父级
* @property {Boolean} foldAll = 折叠时关闭所有已经打开的子集,再次打开时需要一级一级打开
* @property {String} confirmColor = 确定按钮颜色
* @property {String} cancelColor = 取消按钮颜色
* @property {String} titleColor = 标题颜色
* @property {String} currentIcon = 展开时候的icon
* @property {String} checkIcon = 选中时候的icon
* @property {String} defaultIcon = 折叠时候的icon
* @property {String} lastIcon = 没有子集的icon
* @property {Boolean} border = 是否有分割线
* @property {Boolean} needBtn = 是否有操作按钮
* @property {Boolean} toEject = 是否是从底部弹出
* @property {Boolean} needMask = 是否是从底部弹出
* @property {String} boxHeight = 是否是从底部弹出时组件可选区域的高度默认112px
* @property {Boolean} clickMask = 点击遮罩层关闭组件
* @property {String} treeIconColor = 父级icon颜色
* @property {String} componentType = 组件类型('customer')
* @event {Function} _cancel弹窗关闭触发的方法
* @event {Function} _confirmBtn数据选中或有按钮时点击‘确定’触发的方法
* @event {Function} 可通过ref调用组件内方法_show,进行组件的展示
* @event {Function} 可通过ref调用组件内方法_hide,进行组件的关闭但不触发cancel事件
* @event {Function} 可通过ref调用组件内_reTreeList()对数据进行重置
*/
export default {
name: 'ZyTree',
props: {
treeData: {
type: Array,
default: function () {
return []
}
},
valueKey: {
type: String,
default: 'id'
},
labelKey: {
type: String,
default: 'label'
},
childrenKey: {
type: String,
default: 'children'
},
treeIconColor: {
type: String,
default: '#0f56d5'
},
title: {
type: String,
default: ''
},
multiple: {
// 是否可以多选
type: Boolean,
default: true
},
selectParent: {
//是否可以选父级
type: Boolean,
default: true
},
foldAll: {
//折叠时关闭所有已经打开的子集,再次打开时需要一级一级打开
type: Boolean,
default: false
},
confirmColor: {
// 确定按钮颜色
type: String,
default: '#0f56d5'
},
cancelColor: {
// 取消按钮颜色
type: String,
default: '#0f56d5'
},
titleColor: {
// 标题颜色
type: String,
default: ''
},
currentIcon: {
// 展开时候的icon
type: String,
default: 'folder'
},
checkIcon: {
// 选中时候的icon
type: String,
default: 'checkmarkempty'
},
defaultIcon: {
// 折叠时候的icon
type: String,
default: 'folder'
},
lastIcon: {
// 没有子集的icon
type: String,
default: ''
},
border: {
// 是否有分割线
type: Boolean,
default: true
},
faNoBorder: {
// 是否有分割线
type: Boolean,
default: false
},
needBtn: {
// 是否有操作按钮
type: Boolean,
default: false
},
toEject: {
// 是否是从底部弹出
type: Boolean,
default: false
},
boxHeight: {
// 是否是从底部弹出时组件可选区域的高度默认100%
type: String,
default: '500px'
},
clickMask: {
// 点击遮罩层关闭组件
type: Boolean,
default: true
},
openAll: {
// 全部打开
type: Boolean,
default: false
},
irrevocableAll: {
// 全部不能取消选中
type: Boolean,
default: false
},
nextTreeStyle: {
// 样式
type: Object,
default: () => ({})
},
nextTreeScrollStyle: {
// 样式
type: Object,
default: () => ({})
},
defaultValue: {
// 默认选中的值
type: Array,
default: function () {
return []
}
},
nextTreeMinHeight: {
type: String,
default: '500px'
},
needClickFn: {
// 选中子节点是否需要触发confirm
type: Boolean,
default: false
},
otherValueKey: {
// 额外选中的值
type: Array,
default: function () {
return []
}
},
needMask: {
// 是否需要遮罩层关闭组件
type: Boolean,
default: true
},
allWayShow: {
// 是否一直展示
type: Boolean,
default: false
},
iconSize: {
// icon大小默认16
type: String,
default: '16'
},
zipList: {
// 是否是文件展示列表
type: Boolean,
default: false
},
zipListIconSize: {
// 文件列表icon大小默认34
type: String,
default: '34'
},
marginLeftNum: {
// 每行左边缩进的距离
type: Number,
default: 25
},
componentType: {
// 组件类型('customer')customer~是客户编辑时编辑的样式,正常为''
type: String,
default: ''
}
},
watch: {
defaultValue: {
deep: true,
handler() {
this._defaultSelect() // 只更新已选中的节点,而不是重建整棵树
}
}
},
data() {
return {
showTree: false,
treeList: [],
selectIndex: -1,
childNums: [],
rt: [],
rtName: [],
otherValue: [],
cklickType: {
type: '',
value: ''
}
}
},
computed: {
nextTreeStyleCp() {
return {
minHeight: this.nextTreeStyle.minHeight ?? this.nextTreeMinHeight,
height: this.nextTreeStyle.height ?? this.boxHeight,
...this.nextTreeStyle
}
},
nextTreeScrollStyleCp() {
return {
minHeight: this.nextTreeStyle.minHeight ?? this.nextTreeMinHeight,
...this.nextTreeStyle
}
}
},
methods: {
imgTypeFn(type) {
let imgType = 'oth'
const typeMap = {
img: ['png', 'jpg', 'jpeg', 'gif', 'bmp'],
exel: ['xls', 'xlsx', 'xlsm'],
word: ['doc', 'docx'],
pdf: ['pdf'],
ppt: ['ppt', 'pptx'],
txt: ['txt']
}
for (let key in typeMap) {
if (typeMap[key].includes(type)) {
imgType = key
}
}
return imgType
},
getSize(val) {
let sizeInKB = val / 1024
return sizeInKB.toFixed(2) + 'KB'
},
showBorder(show, item) {
if (this.faNoBorder && !item) {
return false
} else {
return show && this.border
}
},
_show() {
this.showTree = true
this.$emit('showTree', true)
this._confirm()
},
_hide() {
this.$emit('showTree', false)
this.showTree = false
},
_cancel(val) {
if (val == 1 && !this.clickMask) return
this._hide()
this.$emit('cancel', '')
},
_confirm(val) {
console.log('🚀 ~ _confirm ~ val:', val)
this.rt = []
this.rtName = []
this.otherValue = []
// 处理所选数据
this.treeList.forEach(item => {
// if (item.id == 100 && item.checked) {
if (item.checked) {
return this.confirmEach(this.treeData)
}
// if (item.checked && item.parentId && item.parentId != '100') {
if (item.checked && item.parentId) {
this.rt.push(item.id)
this.rtName.push(item.name)
this.otherValueKey.forEach(key => {
if (!this.otherValue[key]) {
this.otherValue[key] = [] // 初始化数组
}
this.otherValue[key].push(item[key]) // 将对应字段值加入数组
})
}
})
if (val == 1 && !this.allWayShow) {
this._cancel()
}
this.$emit('confirm', { id: this.rt, name: this.rtName, otherValue: this.otherValue }, { ...this.cklickType })
},
_confirmBtn() {
this.rt = []
this.rtName = []
this.otherValue = []
// 处理所选数据
this.treeList.forEach(item => {
// if (item.id == 100 && item.checked) {
// if (item.checked) {
// return this.confirmEach(this.treeData)
// }
// if (item.checked && item.parentId && item.parentId != '100') {
if (item.checked && item.parentId) {
this.rt.push(item.id)
this.rtName.push(item.name)
this.otherValueKey.forEach(key => {
if (!this.otherValue[key]) {
this.otherValue[key] = [] // 初始化数组
}
this.otherValue[key].push(item[key]) // 将对应字段值加入数组
})
}
})
this.$emit('_confirmBtn', { id: this.rt, name: this.rtName, otherValue: this.otherValue })
},
confirmEach(list) {
list.forEach(item => {
if (item.isDy) {
this.rtName.push(item.name)
this.rt.push(item.id)
this.otherValueKey.forEach(key => {
if (!this.otherValue[key]) {
this.otherValue[key] = [] // 初始化数组
}
this.otherValue[key].push(item[key]) // 将对应字段值加入数组
})
}
if (item.children && item.children.length > 0) this.confirmEach(item.children)
})
},
//扁平化树结构
_renderTreeList(list = [], rank = 0, parentId = [], parents = []) {
list.forEach(item => {
this.treeList.push({
...item,
id: item[this.valueKey],
name: item[this.labelKey],
source: item,
parentIdNub: item.parentId,
parentId, // 父级id数组
parents, // 父级id数组
rank, // 层级
showChild: false, //子级是否显示
needShowChild: this.openAll ? this.openAll : item.needShowChild ? item.needShowChild : false, //子级是否显示
open: false, //是否打开
needOpen: this.openAll ? this.openAll : item.needOpen ? item.needOpen : false, //是否打开
show: rank === 0, // 自身是否显示
hideArr: [],
orChecked: this.defaultValue.some(obj => obj == item[this.valueKey]) ? true : item.checked ? true : false,
checked: this.defaultValue.some(obj => obj == item[this.valueKey]) ? true : item.checked ? true : false,
disabled: item.disabled ? item.disabled : false,
isParent: item[this.childrenKey] ? true : false,
irrevocable: this.irrevocableAll ? this.irrevocableAll : item.irrevocable,
indeterminate: false
})
if (Array.isArray(item[this.childrenKey]) && item[this.childrenKey].length > 0) {
let parentid = [...parentId],
parentArr = [...parents]
// childrenid = []
delete parentArr.children
parentid.push(item[this.valueKey])
parentArr.push({
[this.valueKey]: item[this.valueKey],
[this.labelKey]: item[this.labelKey]
})
this._renderTreeList(item[this.childrenKey], rank + 1, parentid, parentArr)
} else {
this.treeList[this.treeList.length - 1].lastRank = true
}
})
},
_defaultSelect() {
this.treeList.forEach(v => {
const isChecked = this.defaultValue.some(obj => obj === v.id)
if (isChecked !== v.checked) {
v.checked = isChecked
}
})
// 同步父子级选中状态
this.treeList.forEach(v => {
if (v.checked) {
this._handleCheckedNode(v)
}
if (v.needOpen) {
this._handleOpenNode(v)
}
if (v.needShowChild) {
this._handleShowChildNode(v)
}
})
},
_handleCheckedNode(v) {
this.treeList.forEach(v2 => {
if (v.parentId.toString().indexOf(v2.parentId.toString()) >= 0) {
v2.show = true
if (v.parentId.includes(v2.id)) {
v2.showChild = true
v2.open = true
}
}
})
},
_handleOpenNode(v) {
v.showChild = true
v.open = true
v.show = true
this.treeList.forEach(v2 => {
if (v2.parentId.toString().indexOf(v.id.toString()) >= 0) {
v2.show = true
if (v2.parentId.includes(v.id)) {
v2.showChild = true
v2.open = true
}
}
if (v.parentId.includes(v2.id)) {
v2.showChild = true
v2.open = true
}
})
},
_handleShowChildNode(v) {
this.treeList.forEach(v2 => {
if (v.parentId.toString().indexOf(v2.parentId.toString()) >= 0) {
v2.show = true
if (v.parentId.includes(v2.id)) {
v2.showChild = true
v.showChild = true
}
}
if (v.id === v2.id) {
v.showChild = false
v.open = false
}
})
},
// 点击
_treeItemTap(item, index) {
if (this.zipList && item.lastRank == true) {
//点击最后一级时触发事件
this.treeList[index].checked = !this.treeList[index].checked
this._fixMultiple(index)
if (this.needClickFn) {
this._confirm()
}
this.$emit('lastRank', item, this.treeList[index].checked)
return
}
this.cklickType = {
type: '',
value: ''
}
if (item.disabled) return
if (item.irrevocable && this.treeList[index].checked) return
if (item.lastRank == true) {
//点击最后一级时触发事件
this.treeList[index].checked = !this.treeList[index].checked
this._fixMultiple(index)
if (this.needClickFn) {
this._confirm()
}
//子级选择/选满/取消选择, 父级往上同步状态
if (this.componentType == 'customer') {
console.log(1)
this.setAncestorsCustomer(item.parentId, this.treeList[index].checked, item.id)
}
this.$emit('lastRank', item, this.treeList[index].checked)
return
}
let list = this.treeList
let id = item.id
if (item.showChild && !item.open) {
item.showChild = false
item.open = false
} else {
item.showChild = !item.showChild
}
item.open = item.showChild ? true : !item.open
list.forEach(childItem => {
if (item.showChild === false) {
this.cklickType = {
type: 'click',
value: 'close'
}
//隐藏所有子级
if (!childItem.parentId.includes(id)) {
return
}
if (!this.foldAll) {
if (childItem.lastRank !== true && !childItem.open) {
childItem.showChild = false
}
// 为隐藏的内容添加一个标记
if (childItem.show) {
childItem.hideArr[item.rank] = id
}
} else {
if (childItem.lastRank !== true) {
childItem.showChild = false
}
}
childItem.show = false
} else {
this.cklickType = {
type: 'click',
value: 'open'
}
// 打开子集
if (childItem.parentId[childItem.parentId.length - 1] === id) {
childItem.show = true
}
// 打开被隐藏的子集
if (childItem.parentId.includes(id) && !this.foldAll) {
if (childItem.hideArr[item.rank] === id) {
childItem.show = true
if (childItem.open && childItem.showChild) {
childItem.showChild = true
} else {
childItem.showChild = false
}
childItem.hideArr[item.rank] = null
}
}
}
})
if (this.selectParent) {
this.$emit('faClick', item, this.cklickType.value)
}
if (!this.selectParent) return
if (!this.needBtn || this.needClickFn) {
this._confirm()
}
},
_treeItemSelect(item, index) {
this.cklickType = {
type: '',
value: ''
}
if (this.selectParent) {
this.$emit('faClick', item, !this.treeList[index].checked)
}
if (item.lastRank == true) {
this.$emit('lastRank', item, !this.treeList[index].checked)
}
if (item.disabled) return
if (!this.selectParent && !item.lastRank) return this._treeItemTap(item, index)
if (item.irrevocable && this.treeList[index].checked) return
this.treeList[index].checked = !this.treeList[index].checked
// 选父级, 子级自动全选
// this.syncChecked(this.treeList, item.id, this.treeList[index].checked)
this.syncChecked(item.id, this.treeList[index].checked)
if (item.rank > 0) {
item.parentId.forEach(pid => {
const parent = this.treeList.filter(i => i.id === pid)
const childNum = parent.length > 0 ? parent[0].childNum : 0
if (this.childNums[pid] === undefined) {
this.childNums[pid] = 1
} else if (this.childNums[pid] < childNum) {
this.childNums[pid]++
}
})
//子级选择/选满/取消选择, 父级往上同步状态
if (this.componentType == 'customer') {
console.log(1)
this.setAncestorsCustomer(item.parentId, this.treeList[index].checked, item.id)
} else {
console.log(2)
this.setAncestors(item.parentId, this.treeList[index].checked)
}
}
this._fixMultiple(index)
if (!this.needBtn) {
if (this.multiple) {
this._confirm()
} else {
if (this.componentType == 'customer') {
this._confirm()
} else {
this._confirm(1)
}
}
}
if (this.needClickFn) {
this._confirm()
}
},
syncChecked(parentId, checked) {
this.treeList.forEach(item => {
if (item.disabled || item.irrevocable) return
// 判断当前节点是否在 parent 路径下
if (Array.isArray(item.parentId) && item.parentId.includes(parentId)) {
this.$set(item, 'checked', checked)
}
})
},
setAncestors(pids, checked) {
this.treeList.forEach((item, index) => {
if (pids.includes(item.id)) {
if (checked && this.childNums[item.id] !== undefined && item.childNum === this.childNums[item.id]) {
// 子级全部选中, 父级才选中
this.treeList[index].checked = true
} else {
this.treeList[index].checked = false
}
this.setAncestors(item.parentId, checked)
this.cklickType = {
type: 'checked',
value: checked
}
}
})
},
setAncestorsCustomer(pids, checked, id) {
if (this.multiple) {
console.log('setAncestorsCustomer---multiple')
} else {
for (let index = 0; index < this.treeList.length; index++) {
if (!checked) {
this.treeList[index].checked = false
this.treeList[index].indeterminate = false
} else {
if (pids.includes(this.treeList[index].id)) {
this.treeList[index].checked = false
this.treeList[index].indeterminate = true
} else {
this.treeList[index].indeterminate = false
}
}
}
}
// 记录操作类型
this.cklickType = {
type: 'checked',
value: checked
}
},
// _treeItemSelect(item, index) {
// this.treeList[index].checked = !this.treeList[index].checked
// this._fixMultiple(index)
// },
// 处理单选多选
_fixMultiple(index) {
if (!this.multiple) {
// 如果是单选
this.treeList.forEach((v, i) => {
if (i != index) {
this.treeList[i].checked = false
} else {
if (this.treeList[i].checked) {
this.treeList[i].checked = true
} else {
this.treeList[i].checked = false
}
}
})
if (this.componentType == 'customer') {
this._confirm()
} else {
this._confirm(1)
}
}
},
// 重置数据
_reTreeList() {
this.treeList.forEach((v, i) => {
this.treeList[i].checked = v.orChecked
})
},
_initTree(treeData = this.treeData) {
this.treeList = []
this._renderTreeList(treeData)
this.$nextTick(() => {
this._defaultSelect(treeData)
})
}
},
mounted() {
this._initTree()
this.$watch(
() => this.treeData,
list => {
this._initTree(list)
},
{ deep: true, immediate: true }
)
this.$watch(
() => [this.multiple, this.selectParent],
() => {
if (this.treeData.length) {
this._reTreeList()
}
},
{ deep: true, immediate: true }
)
}
}
</script>
<style lang="scss">
@import './style.scss';
</style>