需求分析
- 实现类似
el-select下拉框与收缩功能 - 展开结构为树形组件
- 支持数据双向绑定,数据静态搜索
思路
使用el-select与el-tree二次封装完成
基础实现
子组件
<template>
<el-select
ref="selectTreeRef"
v-model="selectedValue">
<el-option
v-for="d in option"
:key="d.id"
:value="d.id"
:label="d.label"
style="display: none"
>
</el-option>
<el-tree
ref="treeRef"
:data="treeData"
node-key="id"
highlight-current
:expand-on-click-node="false"
@current-change="handleCurrentChange"
>
</el-tree>
</el-select>
</template>
<sctipt>
// 数据
const data = [
{ id: 1, label: '一级 1' },
{ id: 2, label: '一级 2' },
{ id: 3, label: '二级 1-1', pid: 1 },
{ id: 4, label: '二级 1-1', pid: 3 },
]
// 列表转树形结构
function listToTree(list, idField = 'id', parentId = 'pid') {
const map = {}; // 用于存储所有节点的映射
const tree = []; // 最终的树形结构数组
// 首先,将列表中的每个项放入映射中,以便稍后快速查找
list.forEach(item => {
map[item[idField]] = { ...item, children: [] };
});
// 遍历列表,为每个节点找到其父节点,并将其添加到父节点的 children 数组中
list.forEach(item => {
const parent = map[item[parentId]];
if (parent) {
// 如果找到了父节点,将当前节点添加到父节点的 children 数组中
parent.children.push(map[item[idField]]);
} else {
// 如果没有父节点(即它是根节点),则将其添加到树形结构数组中
tree.push(map[item[idField]]);
}
});
return tree;
}
export default {
model: {
prop: 'value',
event: 'change'
},
props: {
value: [String, Number],
},
data() {
return {
// 下拉选项
option: data,
// 树形结构数据
treeData: listToTree(data),
// 选中值
selectedValue: ''
}
},
watch: {
value: {
handler(val) {
this.selectedValue = val;
},
immediate: true
}
},
methods: {
// 当前选中节点变化时触发的事件
handleCurrentChange(data) {
this.selectedValue = data.id;
// 使 input 失去焦点,并隐藏下拉框
this.$refs.selectTreeRef.blur()
this.updateValue();
},
// 更新value
updateValue() {
this.$nextTick(() => {
this.$emit('change', this.selectedValue);
})
}
}
}
</script>
父组件使用
<template>
<!-- 如果没有全局注册组件,此处需要引入组件 -->
<tree-select v-model="value"></tree-select>
</template>
<sctipt>
export default {
data() {
return {
value: ''
}
}
}
</sctipt>
注意:
el-option设置样式style="display: none",如果不设置,上半部分是下拉,下半部分才是el-tree
回显自动展开,并且标亮当前选中项
- 自动展开使用
default-expanded-keys - 标亮当前选中项是需要设置
highlight-current,同时监听下拉框展开visible-change设置当前选中项
<template>
<el-select
ref="selectTreeRef"
v-model="selectedValue"
@visible-change="handleVisibleChange">
...
</el-option>
<el-tree
...
ref="treeRef"
highlight-current
:default-expanded-keys="[selectedValue]"
>
</el-tree>
</el-select>
</template>
<sctipt>
export default {
methods: {
// 显示/隐藏下拉框时触发的事件
handleVisibleChange(val) {
if(val) {
this.setCurrentNode()
}
},
// 设置当前选中节点
setCurrentNode() {
this.$nextTick(() => {
// 设置当前选中节点
this.$refs.treeRef.setCurrentKey(this.selectedValue || null)
})
},
}
}
</sctipt>
可筛选
- 使用
el-select的自定义搜索 el-tree设置filter-node-method- 在函数
filter-method中调用el-tree实例的filter方法
<template>
<el-select
ref="selectTreeRef"
v-model="selectedValue"
filterable
:filter-method="filterMethod">
...
</el-option>
<el-tree
...
ref="treeRef"
:filter-node-method="filterNode"
>
</el-tree>
</el-select>
</template>
<sctipt>
export default {
methods: {
// 搜索时触发的事件
filterMethod(query) {
this.$refs.treeRef.filter(query);
},
// 过滤节点时触发的事件
filterNode(value, data) {
if(!value) return true;
return data.label.indexOf(value) !== -1;
}
}
}
</sctipt>
完整版
<template>
<el-select
ref="selectTreeRef"
v-model="selectedValue"
filterable
:filter-method="filterMethod"
@visible-change="handleVisibleChange">
<el-option
v-for="d in option"
:key="d.id"
:value="d.id"
:label="d.label"
style="display: none"
>
</el-option>
<el-tree
ref="treeRef"
:data="treeData"
node-key="id"
highlight-current
:default-expanded-keys="[selectedValue]"
:expand-on-click-node="false"
@current-change="handleCurrentChange"
:filter-node-method="filterNode"
>
</el-tree>
</el-select>
</template>
<sctipt>
// 数据
const data = [
{ id: 1, label: '一级 1' },
{ id: 2, label: '一级 2' },
{ id: 3, label: '二级 1-1', pid: 1 },
{ id: 4, label: '二级 1-1', pid: 3 },
]
// 列表转树形结构
function listToTree(list, idField = 'id', parentId = 'pid') {
const map = {}; // 用于存储所有节点的映射
const tree = []; // 最终的树形结构数组
// 首先,将列表中的每个项放入映射中,以便稍后快速查找
list.forEach(item => {
map[item[idField]] = { ...item, children: [] };
});
// 遍历列表,为每个节点找到其父节点,并将其添加到父节点的 children 数组中
list.forEach(item => {
const parent = map[item[parentId]];
if (parent) {
// 如果找到了父节点,将当前节点添加到父节点的 children 数组中
parent.children.push(map[item[idField]]);
} else {
// 如果没有父节点(即它是根节点),则将其添加到树形结构数组中
tree.push(map[item[idField]]);
}
});
return tree;
}
export default {
model: {
prop: 'value',
event: 'change'
},
props: {
value: [String, Number],
},
data() {
return {
// 下拉选项
option: data,
// 树形结构数据
treeData: listToTree(data),
// 选中值
selectedValue: ''
}
},
watch: {
value: {
handler(val) {
this.selectedValue = val;
},
immediate: true
}
},
methods: {
// 当前选中节点变化时触发的事件
handleCurrentChange(data) {
this.selectedValue = data.id;
// 使 input 失去焦点,并隐藏下拉框
this.$refs.selectTreeRef.blur()
this.updateValue();
},
// 更新value
updateValue() {
this.$nextTick(() => {
this.$emit('change', this.selectedValue);
})
},
// 显示/隐藏下拉框时触发的事件
handleVisibleChange(val) {
if(val) {
this.setCurrentNode()
}else {
this.$refs.treeRef.filter(null);
}
},
// 设置当前选中节点
setCurrentNode() {
this.$nextTick(() => {
// 设置当前选中节点
this.$refs.treeRef.setCurrentKey(this.selectedValue || null)
})
},
// 搜索时触发的事件
filterMethod(query) {
this.$refs.treeRef.filter(query);
},
// 过滤节点时触发的事件
filterNode(value, data) {
if(!value) return true;
return data.label.indexOf(value) !== -1;
},
// 获取上级数据
queryAllSuperiorData() {
let {
treeData,
selectedValue
} = this;
let valueKey = 'id';
function findAncestors(nodes, targetId, ancestors = []) {
for (let node of nodes) {
if (node[valueKey] === targetId) {
return ancestors;
}
if (node.children && node.children.length > 0) {
ancestors.push(node);
const result = findAncestors(node.children, targetId, ancestors.slice());
if (result) return result;
ancestors.pop();
}
}
return null;
}
return findAncestors(treeData, selectedValue);
}
}
}
</script>
案例中使用的下拉数据现在是固定的,有需要可以自己改成props传参,筛选也是默认开启,也可以改成props控制。
本案例使用树形单选下拉,如果需要多选就需要各位大神自己修改。