父组件使用
<template>
<div>
{{ array }} 更多属性详见wgyTreeSelect组件
<wgyTreeSelect
v-model="array"
:list="list"
:multiple="true"
:disabled-ids="[111,113,2]"
/>
</div>
</template>
<script>
/*
注意: 默认是这种结构
id: 'id', // ID
label: 'name', // 显示名称
children: 'children', // 子级字段名
path: 'path', // 路径
content: 'content', // 描述
pid: 'pid', // 父id
如果不是这种结构传入obj定义
*/
export default {
data() {
return {
array: [],
//数据源
list: [
{
id: '1', // ID
name: '上海市', // 显示名称
children: [
{
id: '2', // ID
name: '嘉定区', // 显示名称
children: [
{
id: '3', // ID
name: '江桥镇11111111111111111111111111111', // 显示名称
content: '嘉定', // 描述
pid: '2', // 父id
},
{
id: '4', // ID
name: '安亭镇', // 显示名称
content: '安亭', // 描述
pid: '2', // 父id
},
], // 子级字段名
content: '嘉定', // 描述
pid: '1', // 父id
},
{
id: '2000000', // ID
name: '嘉定2区', // 显示名称
children: [
{
id: '3876543', // ID
name: '江桥2镇11111111111111111111111111111', // 显示名称
content: '嘉定', // 描述
pid: '2000000', // 父id
},
], // 子级字段名
content: '嘉定', // 描述
pid: '1', // 父id
},
], // 子级字段名
content: '上海魔都', // 描述
pid: '0', // 父id
},
{
id: '11', // ID
name: '北京市', // 显示名称
children: [
{
id: '12', // ID
name: '朝阳区', // 显示名称
children: [
{
id: '13', // ID
name: '三里屯', // 显示名称
content: '三里屯', // 描述
pid: '12', // 父id
},
{
id: '111', // ID
name: '四里屯--------------', // 显示名称
pid: '12', // 父id
},
{
id: '113', // ID
name: '五里屯--------------', // 显示名称
pid: '12', // 父id
},
{
id: '123', // ID
name: '六里屯--------------', // 显示名称
pid: '12', // 父id
},
{
id: '11111', // ID
name: '七里屯--------------', // 显示名称
pid: '12', // 父id
},
{
id: '11311', // ID
name: '八里屯--------------', // 显示名称
pid: '12', // 父id
},
{
id: '12312', // ID
name: '九里屯--------------', // 显示名称
pid: '12', // 父id
},
{
id: '11113331', // ID
name: '十里屯--------------', // 显示名称
pid: '12', // 父id
},
{
id: '11344411', // ID
name: '十一里屯--------------', // 显示名称
pid: '12', // 父id
},
{
id: '12555312', // ID
name: '十二里屯--------------', // 显示名称
pid: '12', // 父id
},
{
id: '14', // ID
name: '左家庄--------------', // 显示名称
content: '左家庄', // 描述
pid: '12', // 父id
},
], // 子级字段名
content: '朝阳', // 描述
pid: '11', // 父id
},
], // 子级字段名
content: '北京', // 描述
pid: '0', // 父id
},
],
};
},
}
</script>
子组件
<template>
<div>
<el-popover
v-model="isShowSelect"
placement="bottom-start"
:width="popoverWidth"
:close-on-click-modal="false"
trigger="manual"
@hide="popoverHide"
>
<el-tree
ref="tree"
v-bind="$attrs"
class="common-tree"
:width="width"
:data="treeData"
:props="obj"
:show-checkbox="multiple"
:node-key="obj.id"
:check-strictly="checkStrictly"
:default-expanded-keys="defaultKeys"
:expand-on-click-node="multiple&&expandClickNode"
:check-on-click-node="checkClickNode"
:highlight-current="true"
@check-change="nodeClick"
@node-click="nodeClick"
/>
<el-select
slot="reference"
ref="select"
v-bind="$attrs"
v-model="returnDataKeys"
:size="size"
:width="width"
:multiple="multiple"
:clearable="clearable"
:collapse-tags="collapseTags"
class="tree-select"
@click.native="selectClick"
@remove-tag="removeTag"
@clear="clear"
@mouseenter.native="showCloseIcon = true"
@mouseleave.native="showCloseIcon = false"
>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
<!-- 这里是删除整个下拉框的内容,多选的时候,是没有这个功能的,所有自己弄了一个icon -->
<template
slot="prefix"
>
<i
v-if="multiple && clearable && returnDataKeys.length && showCloseIcon"
class="el-icon-close"
@click.stop="clearSelectedNodes"
></i>
<i></i>
</template>
</el-select>
</el-popover>
</div>
</template>
<script>
export default {
name: 'TreeSelect',
props: {
// 绑定的值
value: {
tyep: Object,
},
// 树结构数据
list: {
type: Array,
default: () => ([]),
},
obj: {
type: Object,
required: false,
default: () => ({
id: 'id', // ID
label: 'name', // 显示名称
children: 'children', // 子级字段名
path: 'path', // 路径
content: 'content', // 描述
pid: 'pid', // 父id
}),
},
// 配置是否可多选
multiple: {
type: Boolean,
default: false,
},
// 配置是否可清空选择,只对单选时生效, 多选时自定义清空按钮
clearable: {
type: Boolean,
default: true,
},
// 配置多选时是否将选中值按文字的形式展示
collapseTags: {
type: Boolean,
default: false,
},
// 显示复选框情况下,是否严格遵循父子不互相关联
checkStrictly: {
type: Boolean,
default: false,
},
// 多选时设置点击节点是否可以选中,false是只有点击多选框才选中,点击元素不选中
checkClickNode: {
type: Boolean,
default: false,
},
// 多选时:点击节点展开还是点三角标,true是点击节点展开,false是点击三角形展开
expandClickNode: {
type: Boolean,
default: false,
},
size: {
type: String,
default: 'small',
},
width: {
type: String,
default: '200px',
},
// 父节点全选时, 是否只展示子节点
onlyLeaf: {
type: Boolean,
default: false,
},
// 父节点全选时,是否只展示父节点
mergeTag: {
type: Boolean,
default: false,
},
// 外部点击是否收起树形, 默认收起
closeOnOutsideClick: {
type: Boolean,
default: true,
},
// 禁止选中的数据
disabledIds: {
type: Array,
default: () => ([]),
},
},
// 上面是父组件可传入参数
data() {
return {
defaultKeys: [], // 默认展开的节点
defaultKey: '',
first: false, //
popoverWidth: '0px', // 下拉框大小
isShowSelect: false, // 是否显示树状选择器
options: [], // select option选项
returnDatas: [], // 返回给父组件数组对象
returnDataKeys: [], // 返回父组件数组主键值
showCloseIcon: false, // 清空icon
};
},
computed: {
treeData() {
// 判断传入的数据是不是树形结构
const isTreeStructure = JSON.stringify(this.list).indexOf(this.obj.children) !== -1;
// 是树形结构,就返回传入的数据, 不是的话格式化成树形结构
return isTreeStructure ? this.list : this.toTreeStructure(this.list);
},
},
watch: {
// 是否显示树状选择器
isShowSelect() {
// 隐藏select自带的下拉框
this.$refs.select.blur();
},
// 监听tree数据
treeData() {
this.$nextTick(() => {
this.init();
});
},
// 监听value从新赋值
value: {
handler(val) {
this.$nextTick(() => {
if (this.multiple) {
this.defaultKeys = val;
} else {
this.defaultKey = val;
}
this.init();
});
},
immediate: true,
},
// 监听选中的值
returnDataKeys: {
handler(val) {
if (this.first || val) {
this.first = true;
this.$emit('input', val);
} else {
this.first = true;
}
},
},
// 监听禁用数组
disabledIds: {
handler(val) {
console.log(val);
this.addDisabledProperty(this.list, val);
},
},
},
mounted() {
if (this.closeOnOutsideClick) {
document.addEventListener('click', this.handleClickOutside);
}
},
beforeDestroy() {
if (this.closeOnOutsideClick) {
document.removeEventListener('click', this.handleClickOutside);
}
},
methods: {
// 传入数据类型不是树形结构,转成树形结构
toTreeStructure(list) {
console.log(list);
const map = {}; let node; const roots = []; let
i;
for (i = 0; i < list.length; i += 1) {
map[list[i].id] = i; // 初始化map
list[i].children = []; // 初始化children
}
for (i = 0; i < list.length; i += 1) {
node = list[i];
if (node.pid !== '0') {
// 如果有父级
list[map[node.pid]].children.push(node);
} else {
// 如果没有父级,则为根节点
roots.push(node);
}
}
return roots;
},
// 点击其他元素, 树隐藏
handleClickOutside(event) {
if (!this.$refs.select?.$el?.contains(event.target)) {
this.isShowSelect = false;
}
},
init() {
// 如果是多选,
if (this.multiple) {
// 且默认展开的节点大于0
if (Array.isArray(this.defaultKeys) && this.defaultKeys.length > 0) {
// 检测this.defaultKeys[0]是否是一个对象。
if (Object.prototype.toString.call(this.defaultKeys[0]).indexOf('Object') !== -1) { // 对象
this.setDatas(this.defaultKeys);
// 检测this.defaultKeys[0]是否是一个数字或者字符串。
} else if (Object.prototype.toString.call(this.defaultKeys[0]).indexOf('Number') !== -1
|| Object.prototype.toString.call(this.defaultKeys[0]).indexOf('String') !== -1) {
this.setKeys(this.defaultKeys);
} else {
console.log('多选:传入参数类型不匹配');
}
}
} else {
// 单选
if (Object.prototype.toString.call(this.defaultKey).indexOf('Number') !== -1
|| Object.prototype.toString.call(this.defaultKey).indexOf('String') !== -1
|| Object.prototype.toString.call(this.defaultKey).indexOf('Object') !== -1) {
this.setKey(this.defaultKey);
} else {
console.log('单选:传入参数类型不匹配');
}
}
},
// 下拉框select点击[入口]
selectClick() {
this.isShowSelect = !this.isShowSelect;
},
// 节点被点击
nodeClick(a, node) {
// 单选
if (!this.multiple) {
this.isShowSelect = false;
this.setKey(node.key);
// 多选
} else {
// 所有被选中的节点的 key 所组成的数组数据
const checkedKeys = this.$refs.tree.getCheckedKeys();
let selectedNodes = checkedKeys.map((item) => {
// 所有被选中的节点对应的node
const { data, label, key } = this.$refs.tree.getNode(item);
return { label, value: key, data };
});
// 如果onlyLeaf为true,mergeTag为false只保留叶子节点
if (this.onlyLeaf && !this.mergeTag) {
selectedNodes = selectedNodes.filter((n) => !n.data.children);
}
// 如果mergeTag为true,onlyLeaf为false只保留父节点
if (this.mergeTag && !this.onlyLeaf) {
selectedNodes = selectedNodes.filter((n) => {
// 判断当前节点是否有父节点
if (n.data.pid) {
// 判断当前节点的父节点是否也在selectedNodes中
const parentInSelectedNodes = selectedNodes.some((upN) => upN.data.id === n.data.pid);
// 如果父节点也在selectedNodes中,就将当前节点从selectedNodes中移除
return !parentInSelectedNodes;
}
return true;
});
}
// 设置option选项
this.options = selectedNodes;
this.returnDataKeys = selectedNodes.map((item) => item.value);
this.returnDatas = selectedNodes.map((n) => n.data);
}
},
// 单选:清空选中
clear() {
this.$refs.tree.setCurrentKey(null);// 清除树选中key
this.returnDatas = null;
this.returnDataKeys = '';
this.popoverHide();
this.isShowSelect = false;
},
// 单选:设置、初始化值 key
setKey(thisKey) {
if (thisKey) {
// 设置当前选中的值, 获取当前值对应的节点, 设置当前节点
this.$refs.tree.setCurrentKey(thisKey);
const node = this.$refs.tree.getNode(thisKey);
this.setData(node.data);
}
},
// 单选:设置、初始化对象
setData(data) {
this.options = [];
this.options.push({ label: data[this.obj.label], value: data[this.obj.id] });
this.returnDatas = data;
this.returnDataKeys = data[this.obj.id];
},
// 多选:设置、初始化值 keys
setKeys(checkedKeys) {
// 给树状选择器设置选中的节点。 给select赋值
this.$refs.tree.setCheckedKeys(checkedKeys);
this.returnDataKeys = checkedKeys;
const selectedNodes = checkedKeys.map((item) => {
// 所有被选中的节点对应的node
const node = this.$refs.tree.getNode(item);
return { label: node.label, value: node.key, data: node.data };
});
this.returnDatas = selectedNodes.map((node) => node.data);
this.popoverHide();
},
// 多选:设置、初始化对象
setDatas(data) {
// 获取选中节点的主键值
const checkedKeys = data.map((item) => item[this.obj.id]);
// 设置树状选择器的选中节点
this.$refs.tree.setCheckedKeys(checkedKeys);
// 设置返回给父组件的数组对象
this.returnDatas = data;
this.returnDataKeys = checkedKeys;
this.popoverHide();
},
// 多选模式下,当删除选中的节点时,将该节点及其子节点设置为未选中状态,并更新选中的节点。 ok
removeTag(val) {
// 获取删除的节点
const node = this.$refs.tree.getNode(val);
// 判断获取的节点是否为叶子节点
const isLeafNode = node.childNodes.length === 0;
// 是叶子节点,那么只需要将该节点设置为未选中状态
// 不是叶子节点,那么需要将该节点及其所有子节点设置为未选中状态
const nodesToUncheck = isLeafNode ? [node] : this.treeToList(node);
// 将每个节点设置为未选中状态。
nodesToUncheck.forEach((n) => this.$refs.tree.setChecked(n, false));
// 更新选中的节点。
this.nodeClick();
this.popoverHide();
},
// 下拉框关闭执行
popoverHide() {
this.$emit('getValue', this.returnDataKeys, this.returnDatas);
},
// 多选,清空所有被选中的节点
clearSelectedNodes() {
this.$refs.tree.setCheckedKeys([]);
},
// 树形转为集合
treeToList(tree) {
let queen = [];
const out = [];
queen = queen.concat(tree);
while (queen.length) {
const first = queen.shift();
if (first.childNodes) {
queen = queen.concat(first.childNodes);
}
out.push(first);
}
return out;
},
// 禁用某个节点
addDisabledProperty(data, ids) {
data.forEach((item) => {
if (ids.includes(+item.id)) {
item.disabled = true;
}
if (item.children) {
this.addDisabledProperty(item.children, ids);
}
});
},
},
};
</script>
<style scoped lang="scss">
::v-deep .el-input__prefix{
position: absolute;
height: 100%;
right: 5px;
.el-icon-close {
position: absolute;
right: 6px;
top: 11px;
z-index: 1;
border-radius:50%;
color: #fff;
background-color: #B0B3B8;
}
}
.mask{
height: 100%;
position: fixed;
top: 0;
left: 0;
opacity: 0;
z-index: 11;
}
.common-tree{
overflow: auto;
}
.tree-select{
position: relative;
z-index: 111;
}
.ok{
float: right;
}
.el-row{
padding-top: 0px !important;
}
</style>