树形选择器 基于iview
实现: 多级树形结构,可搜索,可清空
传入data结构:
data: [
{
title: '早餐',
children: [
{
title: '北方',
children: [
{
title: '豆浆',
children: [
{title: '豆浆+咸豆腐脑'}
]
},
{
title: '油条',
children: [
{title: '油条+胡辣汤'}
]
}
]
},
{
title: '南方',
children: [
{
title: '豆浆',
children: [
{title: '豆浆+甜豆腐脑'}
]
},
{title: '肠粉'},
{title: '虾饺'}
]
}
]
},
{
title: '午餐',
children: [
{
title: '北方',
children: [
{title: '面条'},
{title: '饺子'}
]
},
{
title: '南方',
children: [
{title: '米饭'},
{title: '馄饨'}
]
}
]
}
],
代码:
<template>
<div>
<span class="tech-sel-wrapper">
<div
ref="selectBox"
:class="['select-box', {active: showClass.isActive}]"
tabindex="-1"
@blur="blur"
@focus="focus"
@mouseenter="hover"
@mouseleave="() => {this.showClear = false}"
:style="{width: `${width}px`}">
<div class="tag-box">
<template v-for="(item, index) in selectNodes">
<!-- Tag标签
fade:是否在出现和消失时使用渐变的动画,动画时长可能会引起占位的闪烁
closable:标签是否可以关闭
name:当前标签的名称,使用 v-for,并支持关闭时,会比较有用
on-close:关闭时触发
-->
<Tag
:fade="false"
closable
:key="index"
@on-close="closeTag"
:name="JSON.stringify(item)">
{{item.title}}
</Tag>
</template>
</div>
<div v-if="clearable" class="clear-btn" v-show="showClear" @click="clear">
<Icon type="md-close-circle" size="20"></Icon>
</div>
<div :class="['content-box', {'fade-in': showClass.fadeIn}, {'fade-out': showClass.fadeIn}]" ref="contentBox">
<div class="input-box" v-if="searchable" ref="inputBox">
<div style="width: 40%;display: inline-block">
<!-- Input输入框
icon:输入框尾部图标,仅在 text 类型下有效;type 默认 text
on-blur:输入框失去焦点时触发
on-keyup:原生的 keyup 事件,按钮被松开
-->
<Input
@on-keyup="keyUp"
@on-blur="inputBlur"
v-model="keyWord"
size="small"
icon="ios-search">
</Input>
</div>
</div>
<div class="tree-box" ref="treeBox">
<Tree
:data="searching ? searchData : originData"
multiple
show-checkbox
@on-check-change="setSelectNodes"
></Tree>
</div>
</div>
</div>
</span>
</div>
</template>
<script>
const contains = (parentNode, childNode) => {
// Node.contains()方法返回一个Boolean值,
// 该值指示节点是否是给定节点的后代,即节点本身、其直接子节点 ( childNodes) 之一、子节点的直接子节点之一,等等。
if (parentNode && childNode) return parentNode.contains(childNode)
return false
}
export default {
props: {
data: { // 树形结构的数据
type: Array,
default: null,
required: true
},
searchable: { // 是否搜索,设置该属性为true时,可以根据子节点的title进行搜索
type: Boolean,
default: false,
required: false
},
clearable: { // 是否清除已选项
type: Boolean,
default: false,
required: false
},
width: { // 选择框的宽度
type: [String, Number],
default: 300,
required: false
},
value: { // v-model绑定的数据
type: Array,
default: null,
required: true
},
pkey: { // 设置每一个节点的唯一标识
type: String,
default: 'title',
required: false
}
},
data () {
return {
searching: false, // 为false是originData,为true是searchData
searchData: [], // 搜索的数据
originData: [], // this.data加上value属性的数据
selectNodes: [], // 选中的数据
keyWord: null, // 搜索的关键词
showClear: false, // 清除按钮是否显示
showClass: {
'isActive': false,
'fadeIn': false,
'fadeOut': false
}
}
},
methods: {
focus() { // 获得焦点
this.showClass.isActive = true; // active 边框加颜色,把一个或多个下拉阴影添加到框上
this.showClass.fadeIn = true; // 不透明度,从0.0(完全透明)到1.0(完全不透明)
},
blur({relatedTarget}) { // 失去焦点
// relatedTarget --- 选中的节点 --- <input type="checkbox" class="ivu-checkbox-input">
// this.$refs.inputBox --- 搜索框 --- <div data-v-a7dae8d6 class="input-box">...</div>
// this.$refs.treeBox --- 树形结构 --- <div data-v-a7dae8d6 class="tree-box">...</div>
switch (true) {
case contains(this.$refs.inputBox, relatedTarget):
break;
case contains(this.$refs.treeBox, relatedTarget):
this.$refs.selectBox.focus();
break;
default:
this.showClass.isActive = false;
this.showClass.fadeIn = false;
this.showClass.fadeOut = true;
}
},
/** hover() 方法规定当鼠标指针悬停在被选元素上时要运行的两个函数。
方法触发 mouseenter 和 mouseleave 事件。
注意: 如果只指定一个函数,则 mouseenter 和 mouseleave 都执行它
**/
hover() { // 鼠标经过
if (this.selectNodes.length > 0) this.showClear = true
},
// input输入框失去焦点
inputBlur({relatedTarget}) {
// relatedTarget --- 选中的节点 --- <input type="checkbox" class="ivu-checkbox-input">
// this.$refs.selectBox --- 整个div --- <div data-v-a7dae8d6 tabindex="-1" class="select-box" style="width: 300px;">...</div>
// this.$refs.treeBox --- 树形结构 --- <div data-v-a7dae8d6 class="tree-box">...</div>
switch (true) {
case relatedTarget === this.$refs.selectBox:
this.$refs.selectBox.focus();
break;
case contains(this.$refs.treeBox, relatedTarget):
this.$refs.selectBox.focus();
break;
default:
this.showClass.isActive = false;
this.showClass.fadeIn = false;
this.showClass.fadeOut = true;
}
},
// 清除已选项
clear() {
this.selectNodes = [];
this.originData = this.resetTree(
(node) => {
delete node.checked
delete node.indeterminate
}
)
this.setSelectNodes();
},
setSelectNodes() {
let nodes = []; // 选中的数据
this.traverseTree(
{children: this.originData},
(node) => {
if (!node.children && node.checked === true) nodes.push(node)
}
)
this.$emit('input', this.selectNodes = nodes);
},
// input输入框按钮被松开
keyUp() {
if (this.keyWord && this.keyWord.length > 0) { // 有搜索关键词时用searchData
this.searchData = [];
this.traverseTree(
{children: this.originData},
(node) => {
if (!node.children && node.title.includes(this.keyWord)) this.searchData.push(node)
}
)
this.searching = true;
} else {
// 无搜索关键词时用originData,尽可能还原originData,如果node有children则删除node的checked与indeterminate属性
// 在按搜索词选中后再删除搜索词,this.originData选中项的上级刚开始并没有checked与indeterminate属性,在经过this.resetTree才有????
this.originData = this.resetTree(
(node) => {
if (node.children && node.children.length > 0) {
delete node.checked
delete node.indeterminate
}
}
)
this.searching = false;
}
},
// 删除tag标签
closeTag(event, value) {
// value: {"title":"豆浆","value":"早餐/北方/豆浆","nodeKey":2,"checked":true,"indeterminate":false}
let curKey = JSON.parse(value)[this.pkey]; // 豆浆
this.originData = this.resetTree(
(node) => {
if (node.children && node.children.length > 0) {
delete node.checked
delete node.indeterminate
} else if (node[this.pkey] === curKey) node.checked = false
}
)
this.setSelectNodes();
},
traverseTree(node, callBack, parentNode) {
// 确认callBack有值,并执行callBack函数
callBack && callBack(node, parentNode)
if (node.children && node.children.length > 0) {
for (let index in node.children) {
this.traverseTree(node.children[index], callBack, node);
}
}
},
resetTree(callBack) {
let cloneNode = JSON.parse(JSON.stringify(this.originData.length > 0 ? this.originData : this.data));
this.traverseTree(
{children: cloneNode},
callBack
)
return cloneNode;
}
},
created () {
// this.value = [{checked: true, indeterminate: false, nodeKey: 0, title: "豆浆", value: "早餐/北方/豆浆"},
// {checked: true, indeterminate: false, nodeKey: 3, title: "油条", value: "早餐/北方/油条"}]
// this.pkey = 'title'
// keys 记录value里面所有pkey值 ["豆浆", "油条"]
let keys = this.value.map((val) => {
if (val[this.pkey]) return val[this.pkey]
})
this.originData = this.resetTree(
(node, parentNode) => {
if (parentNode && parentNode[this.pkey]) {
node.value = `${parentNode.value}/${node[this.pkey]}`;
} else {
node.value = node[this.pkey];
}
// includes() 方法用来判断一个数组keys是否包含一个指定的值node[this.pkey],如果是返回 true,否则false
if (!node.children && keys.includes(node[this.pkey])) node.checked = true
}
)
this.setSelectNodes();
}
}
</script>
<style scoped lang="scss">
.tech-sel-wrapper {
display: inline-block;
text-align: left;
.select-box {
position: relative;
min-height: 40px;
border-radius: 5px;
border: 1px solid #CCC;
transition: .3s;
.clear-btn {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%); // 垂直居中
transition: .5s;
cursor: pointer;
&:hover {
color: #f07649;
}
}
.tag-box {
width: 100%;
padding: 5px;
max-height: 300px;
overflow-y: auto;
}
&:hover {
border-color: #57a3f3;
}
&:focus, &.active {
border-color: #57a3f3;
// 把一个或多个下拉阴影添加到框上
// box-shadow: h-shadow水平 v-shadow垂直 blur模糊距离 spread阴影大小 color阴影颜色 inset从外层的阴影(开始时)改变阴影内侧阴影;
box-shadow: 0 0 0 2px rgba(45, 140, 240, .2);
outline: none;
}
.content-box {
height: 0;
background-color: #FFF;
position: absolute;
z-index: 100;
left: 0;
top: 100%;
margin-top: 5px;
padding-left: 10px;
box-shadow: rgba(0, 0, 0, 0.15) 0 2px 8px 0;
width: 100%;
max-height: 400px;
overflow-y: auto;
&.fade-out {
animation: fade-out .5s forwards;
}
&.fade-in {
animation: fade-in .3s forwards;
}
.input-box {
padding-top: 5px;
padding-right: 10px;
text-align: right;
}
}
}
}
// 不透明度,从0.0(完全透明)到1.0(完全不透明)
@keyframes fade-in {
0% {
height: auto;
opacity: 0;
}
100% {
height: auto;
opacity: 1;
}
}
// 不透明度,从1.0(完全不透明)到 0.0(完全透明),高度从自动到0
@keyframes fade-out {
0% {
height: auto;
opacity: 1;
}
50% {
opacity: 0;
}
100% {
height: 0;
}
}
</style>