实现效果:一级和二级树状穿梭,可以筛选,展示列表已选数量,总共数量,选中数据保存后递归遍历原始数组,返回已选中数据的id和label
子组件
<template>
<div class="tree-transfer">
<div class="transfer-panel unselect">
<div class="transfer-panel__header">
<el-checkbox
v-model="leftCheckAll"
@change="clickLeftCheckAll"
:disabled="leftAlltNum === 0"
>待选</el-checkbox
>
<div>{{ leftSelectNum }}/{{ leftAlltNum }}</div>
</div>
<div class="transfer-panel__content">
<el-input
size="mini"
placeholder="输入关键字进行搜索"
v-model="leftFilterText"
>
</el-input>
<el-tree
filter
ref="tree"
:data="leftNodeData"
show-checkbox
node-key="id"
default-expand-all
:filter-node-method="filterNode"
@check="nodeClick"
:props="defaultProps"
>
</el-tree>
</div>
</div>
<div class="button">
<el-button
type="primary"
size="mini"
:disabled="rightSelectNum === 0"
icon="el-icon-arrow-left"
@click="removeToLeft"
>
</el-button>
<el-button
type="primary"
:disabled="leftSelectNum === 0"
icon="el-icon-arrow-right"
size="mini"
@click="removeToRight"
>
</el-button>
</div>
<div class="transfer-panel selected">
<div class="transfer-panel__header">
<el-checkbox
ref="selectRadio"
v-model="rightCheckAll"
@change="clickRightCheckAll"
:disabled="rightAlltNum === 0"
>已选</el-checkbox
>
<div>{{ rightSelectNum }}/{{ rightAlltNum }}</div>
</div>
<div class="transfer-panel__content">
<el-input
size="mini"
placeholder="输入关键字进行搜索"
v-model="rightFilterText"
>
</el-input>
<el-tree
ref="selectedTree"
:data="rightNodeData"
show-checkbox
default-expand-all
node-key="id"
:filter-node-method="filterNode"
@check="selectedClick"
:props="defaultProps"
>
</el-tree>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
defaultProps: {
type: Object,
default: () => {
return {
children: "children",
label: "name",
};
},
},
treeData: {
type: Array,
default: () => {
return [];
},
},
defaultKeys: {
type: Array,
default: () => {
return [];
},
},
},
watch: {
leftFilterText(val) {
this.$refs.tree.filter(val);
},
rightFilterText(val) {
this.$refs.selectedTree.filter(val);
},
rightKeys(val) {
this.$emit("changeKeys", val);
},
leftNodeData(val) {
this.leftAlltNum = this.getAllIds(val).length;
},
rightNodeData(val) {
this.rightAlltNum = this.getAllIds(val).length;
},
},
data() {
return {
leftCheckAll: false,
rightCheckAll: false,
nodeData: this.treeData,
leftNodeData: [],
rightNodeData: [],
rightKeys: this.defaultKeys,
rightFilterText: "",
leftFilterText: "",
leftSelectNum: 0,
rightSelectNum: 0,
leftAlltNum: 0,
rightAlltNum: 0,
};
},
mounted() {
this.leftNodeData = this.nodeData;
if (this.rightKeys.length > 0) {
this.rightNodeData = this.rightKeys;
}
},
methods: {
getAllIds(arr) {
let ids = [];
arr.forEach((item) => {
ids.push({ id: item.id, label: item.label });
if (item.children) {
ids = ids.concat(this.getAllIds(item.children));
}
});
return ids;
},
clickLeftCheckAll(v) {
//左侧全选
if (v) {
let keys = this.getChildNodeKeys(
this.leftNodeData,
this.leftFilterText
);
// 左侧已选
this.leftSelectNum = keys.length ? keys.length : 0;
this.$refs.tree.setCheckedKeys(keys);
} else {
this.$refs.tree.setCheckedKeys([]);
}
},
clickRightCheckAll(v) {
//右侧全选
if (v) {
let keys = this.getChildNodeKeys(
this.rightNodeData,
this.rightFilterText
);
// 右侧已选
this.rightSelectNum = keys.length ? keys.length : 0;
this.$refs.selectedTree.setCheckedKeys(keys);
} else {
this.$refs.selectedTree.setCheckedKeys([]);
}
},
//获取某个节点的id值
getChildNodeKeys(node, filterText) {
const keys = this.getAllIds(node).map((item) => {
if (item.label.includes(filterText)) {
return item.id;
}
});
return keys;
},
filterNode(value, data) {
let labelName = this.defaultProps.label;
if (!value) return true;
return data[labelName].indexOf(value) !== -1;
},
nodeClick() {
let keys = this.$refs.tree.getCheckedKeys(false);
this.leftSelectNum = keys.length ? keys.length : 0;
},
selectedClick() {
let keys = this.$refs.selectedTree.getCheckedKeys(false);
this.rightSelectNum = keys.length ? keys.length : 0;
},
//向右移动
removeToRight() {
//获取此时左边树状结构中被选中的节点,使用原始数据结构删除不包含在选中数组中的节点,创建新的数组,渲染在右侧
//获取此时左侧中被选中的节点数组id
this.leftSelectNum = 0;
this.leftCheckAll = false;
this.leftFilterText = "";
let keys = this.$refs.tree.getCheckedKeys(false);
let checkedKeys = [...this.rightKeys, ...keys];
this.rightKeys = checkedKeys;
this.rerenderData(this.rightKeys);
this.$refs.tree.setCheckedKeys([]);
},
//向左移动的时候
removeToLeft() {
//说明有已选择的数据改变,此时判断右边选中的数据重新渲染左边树状的结构
//渲染逻辑:使用原始数据结构删除包含在右边选中数组中的数组
//找到目前被选中的keys,从rightKeys中去掉,在使用rightKeys左右两侧数组
this.rightSelectNum = 0;
this.rightCheckAll = false;
this.rightFilterText = "";
let keys = this.$refs.selectedTree.getCheckedKeys(false);
this.rightKeys = this.rightKeys.filter((v) => {
return !keys.includes(v);
});
let checkedKeys = this.rightKeys;
this.rerenderData(checkedKeys);
this.$refs.selectedTree.setCheckedKeys([]);
},
// 渲染逻辑:右侧列表,删除未选中节点;左侧列表,删除已选中节点
rerenderData(checkedKeys) {
const childrenName = this.defaultProps.children;
const leftNodeData = JSON.parse(JSON.stringify(this.nodeData));
const rightNodeData = JSON.parse(JSON.stringify(this.nodeData));
this.removeUncheckedNodes(rightNodeData, checkedKeys, childrenName);
this.removeCheckedNodes(leftNodeData, checkedKeys, childrenName);
this.rightNodeData = rightNodeData;
this.leftNodeData = leftNodeData;
},
// 右侧移除未选中节点
removeUncheckedNodes(data, checkedKeys, childrenName) {
for (let i = 0; i < data.length; i++) {
if (data[i][childrenName]) {
data[i][childrenName] = data[i][childrenName].filter((child) =>
checkedKeys.includes(child.id)
);
if (
data[i][childrenName].length === 0 &&
!checkedKeys.includes(data[i].id)
) {
data.splice(i, 1);
i--;
}
} else if (!checkedKeys.includes(data[i].id)) {
data.splice(i, 1);
i--;
}
}
},
// 左侧移除选中节点
removeCheckedNodes(data, checkedKeys, childrenName) {
for (let i = 0; i < data.length; i++) {
if (data[i][childrenName]) {
data[i][childrenName] = data[i][childrenName].filter(
(child) => !checkedKeys.includes(child.id)
);
if (
data[i][childrenName].length === 0 &&
checkedKeys.includes(data[i].id)
) {
data.splice(i, 1);
i--;
}
} else if (checkedKeys.includes(data[i].id)) {
data.splice(i, 1);
i--;
}
}
}
},
};
</script>
<style scoped>
.tree-transfer {
display: flex;
justify-content: center;
}
.transfer-panel {
box-sizing: border-box;
width: 200px;
height: 300px;
border: 1px solid #ebeef5;
border-radius: 5px;
}
.transfer-panel__header {
display: flex;
justify-content: space-between;
box-sizing: border-box;
height: 40px;
border-bottom: 1px solid #ebeef5;
background-color: rgb(245, 247, 250);
padding-left: 10px;
line-height: 40px;
padding-right: 20px;
}
.title {
font-size: 16px;
margin-left: 5px;
}
.transfer-panel__content {
height: calc(100% - 40px);
overflow-y: scroll;
}
.button {
width: 120px;
display: flex;
justify-content: center;
align-items: center;
}
</style>
父组件及使用
<template>
<div>
<el-button type="primary" @click="dialogVisible = true">+添加</el-button>
<el-dialog title="提示" :visible.sync="dialogVisible" width="50%">
<Transfer
:treeData="data"
:defaultProps="{ children: 'children', label: 'label' }"
@changeKeys="getKeys($event)"
/>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="confirm">确 定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import Transfer from "./transfer.vue";
export default {
components: { Transfer },
data() {
return {
dialogVisible: false,
data: [
{
id: 1,
label: "一级 1"
},
{
id: 2,
label: "一级 2",
children: [
{
id: 5,
label: "二级 2-1",
},
{
id: 6,
label: "二级 2-2",
},
],
},
{
id: 3,
label: "一级 3",
children: [
{
id: 7,
label: "二级 3-1",
},
{
id: 8,
label: "二级 3-2",
},
],
},
],
};
},
methods: {
getKeys(val) {
let result = [];
for (let id of val) {
let item = this.findItemById(this.data, id);
if (item) {
result.push(item);
}
}
this.chooseOrg = result;
console.log(this.chooseOrg, " this.chooseOrg");
},
findItemById(data, targetId) {
for (let item of data) {
if (item.id === targetId) {
return { id: item.id, label: item.label };
} else if (item.children) {
let result = this.findItemById(item.children, targetId);
if (result) {
return result;
}
}
}
return null;
},
confirm() {
// console.log()
},
},
};
</script>