Element-ui提供的穿梭框只支持列表,根据实际需求自己写了一个左边是树结构,右边是列表结构的穿梭框,(如果需要两边都是树结构的话,需要把右侧的逻辑参考左侧改一改)拖拽使用了
vuedraggable
插件文章来源:https://www.toymoban.com/news/detail-511354.html
效果图
文章来源地址https://www.toymoban.com/news/detail-511354.html
组件代码
<template>
<div class="transfer-tree">
<div class="transfer-panel">
<div class="transfer-panel-header">
<el-checkbox
v-model="leftAllChecked"
:disabled="!(leftDataList && leftDataList.length)"
:indeterminate="isIndeterminateLeft"
@change="handleCheckAllChangeLeft">{{ leftTitle }}</el-checkbox>
</div>
<div class="transfer-panel-body">
<el-tree
ref="leftTree"
show-checkbox
check-on-click-node
default-expand-all
:node-key="defaultProps.key"
:data="leftDataList"
:props="defaultProps"
@check="handleCheckLeft">
</el-tree>
</div>
</div>
<div class="transfer-buttons">
<el-button
class="mb8"
size="mini"
icon="el-icon-arrow-left"
:disabled="!(rightCheckedList && rightCheckedList.length)"
@click="handleLeftChange"></el-button>
<el-button
type="primary"
size="mini"
icon="el-icon-arrow-right"
:disabled="!(leftCheckedList && leftCheckedList.length)"
@click="handleRightChange"></el-button>
</div>
<div class="transfer-panel">
<div class="transfer-panel-header">
<el-checkbox
v-model="rightAllChecked"
:disabled="!(rightDataList && rightDataList.length)"
:indeterminate="isIndeterminateRight"
@change="handleCheckAllChangeRight">{{ rightTitle }}</el-checkbox>
<!-- 右侧数据量/限制最大可保存数据量 -->
<span class="transfer-panel-ratio">{{ rightDataList.length }}/{{ maxLimitCount }}</span>
</div>
<div class="transfer-panel-body">
<el-checkbox-group
v-if="rightDataList && rightDataList.length"
v-model="rightCheckedKeyList"
@change="handleCheckRight">
<draggable
v-model="rightDataList"
chosenClass="chosen"
forceFallback="true"
animation="200"
@start="drag = true"
@end="drag = false"
@update="handleOrder">
<transition-group>
<el-checkbox
v-for="(item, index) in rightDataList"
:key="`right_${item[defaultProps.key]}_${index}`"
:label="item[defaultProps.key]"
>{{ item[defaultProps.label]}}
<img
src="@/assets/drag_icon.svg"
alt="拖拽排序"
width="40"
height="15" /></el-checkbox>
</transition-group>
</draggable>
</el-checkbox-group>
<el-empty description="暂无数据" v-else></el-empty>
</div>
</div>
</div>
</template>
<script>
import { number } from 'echarts';
import draggable from 'vuedraggable';
export default {
name: '',
components: {
draggable,
},
props: {
// tree的默认结构
defaultProps: {
type: Object,
required: true,
default: () => ({
children: 'children',
label: 'label',
key: 'key',
parentKey: 'parent', // 这个属性不是 tree组件需要的,是子节点数据中记录父节点标识的属性
}),
},
// left 原始数据
leftOriginalList: {
type: Array,
default: () => [],
},
// right 原始数据
rightOriginalList: {
type: Array,
default: () => [],
},
// 最大可保存数据量
maxLimitCount: {
type: Number,
default: 0,
},
// left 标题
leftTitle: {
type: String,
default: '可选项',
},
// right 标题
rightTitle: {
type: String,
default: '已选项',
},
},
data() {
return {
leftAllChecked: false, // left 全选checkbox
leftDataList: [], // left 所有数据
leftCheckedList: [], // left 选中的数据
isIndeterminateLeft: false,
rightAllChecked: false, // right 全选checkbox
rightDataList: [], // right 所有数据
rightCheckedList: [], // right 选中的数据 =>rightCheckedKeyList对应的 对象数组
rightCheckedKeyList: [], // right 选中的 key list => 绑定在 el-checkbox-group上的 list
isIndeterminateRight: false,
drag: false,
};
},
// 初始化
watch: {
leftOriginalList: {
immediate: true,
deep: true,
handler(newVal) {
this.leftDataList = JSON.parse(JSON.stringify(newVal));
this.leftCheckedList = [];
this.leftAllChecked = false;
this.isIndeterminateLeft = false;
},
},
rightOriginalList: {
immediate: true,
deep: true,
handler(newVal) {
this.rightDataList = JSON.parse(JSON.stringify(newVal));
this.rightCheckedList = [];
this.rightCheckedKeyList = [];
this.rightAllChecked = false;
this.isIndeterminateRight = false;
},
},
},
computed: {
// left 所有子节点数据的数量
leftDataTotal() {
let count = 0;
this.leftDataList.forEach((v) => {
if (v[this.defaultProps.children]) {
count += v[this.defaultProps.children].length;
}
});
return count;
},
},
methods: {
// 选择——left
handleCheckLeft(val, { checkedNodes }) {
// 包含了父节点
const checkedCount = checkedNodes.length;
const totalNodeCount = this.leftDataTotal + this.leftDataList.length;
this.leftAllChecked = checkedCount === totalNodeCount;
this.isIndeterminateLeft = checkedCount > 0 && checkedCount < totalNodeCount;
// 手动剔除父节点
this.leftCheckedList = checkedNodes.filter((v) => (!v[this.defaultProps.children]));
},
// 选择——right
handleCheckRight(val) {
const checkedCount = val.length;
this.rightAllChecked = checkedCount === this.rightDataList.length;
this.isIndeterminateRight = checkedCount > 0 && checkedCount < this.rightDataList.length;
// 手动组织对象数组
this.rightCheckedList = this.rightDataList.filter((v) => (val.includes(v[this.defaultProps.key])));
},
// 全选——left
handleCheckAllChangeLeft(val) {
this.isIndeterminateLeft = false;
const checkedNodes = [];
if (val) {
this.leftDataList.forEach((v) => {
checkedNodes.push(v);
if (v[this.defaultProps.children]) {
v[this.defaultProps.children].forEach((child) => { checkedNodes.push(child); });
}
});
}
// 手动赋checkedlist值
this.leftCheckedList = checkedNodes.filter((v) => (!v[this.defaultProps.children]));
this.$refs.leftTree.setCheckedNodes(checkedNodes);
},
// 全选——right
handleCheckAllChangeRight(val) {
this.isIndeterminateRight = false;
this.rightCheckedKeyList = val ? this.rightDataList.map((v) => (v[this.defaultProps.key])) : [];
// 手动赋checkedlist值
this.rightCheckedList = val ? this.rightDataList.map((v) => (v)) : [];
},
// 传递 right => left
handleLeftChange() {
// left +
const leftDataMap = {};
this.leftDataList.forEach((v) => {
leftDataMap[v[this.defaultProps.key]] = v[this.defaultProps.children] || [];
});
this.rightCheckedList.forEach((v) => {
leftDataMap[v[this.defaultProps.parentKey]].push(v);
});
// right -
this.rightDataList = this.rightDataList.filter((v) => !(this.rightCheckedKeyList.includes(v[this.defaultProps.key])));
// 清空选中数组
this.rightCheckedList = [];
this.rightCheckedKeyList = [];
// right 全选 => 直接取消
this.rightAllChecked = false;
this.isIndeterminateRight = false;
// left 全选 => 原先没有选中/半选中=>不动,原先全选=>半选中 => 重新渲染一次 tree组件选中
if (this.leftAllChecked && !this.isIndeterminateLeft) {
this.leftAllChecked = false;
this.isIndeterminateLeft = true;
}
// 先清空再重置,直接重置的话,父节点的状态会有问题
this.$refs.leftTree.setCheckedNodes([]);
this.$nextTick(() => {
this.$refs.leftTree.setCheckedNodes(this.leftCheckedList);
});
// 传递当前数据分布
this.$emit('change', {
left: this.leftDataList,
right: this.rightDataList,
});
},
// 传递 left => right
handleRightChange() {
// right +
this.rightDataList.push(...this.leftCheckedList);
// left -
const { key, children } = this.defaultProps;
const checkedKeys = this.leftCheckedList.map((v) => (v[key]));
this.leftDataList.forEach((v) => {
if (v[children]) {
v[children] = v[children].filter((child) => !checkedKeys.includes(child[key]));
}
});
// 清空选中数组
this.leftCheckedList = [];
// 清空 tree组件选中
this.$refs.leftTree.setCheckedNodes([]);
// left 全选 => 直接取消
this.leftAllChecked = false;
this.isIndeterminateLeft = false;
// right 全选 => 原先没有选中/半选中=>不动,原先全选=>半选中
if (this.rightAllChecked && !this.isIndeterminateRight) {
this.rightAllChecked = false;
this.isIndeterminateRight = true;
}
// 传递当前数据分布
this.$emit('change', {
left: this.leftDataList,
right: this.rightDataList,
});
},
handleOrder() {
// 传递当前数据分布
this.$emit('change', {
left: this.leftDataList,
right: this.rightDataList,
});
},
},
};
</script>
<style lang="scss" scoped>
.transfer-tree {
display: flex;
width: 100%;
.transfer-panel {
width: 100%;
height: 100%;
border-radius: 4px;
border: 1px solid $color-border;
.transfer-panel-header {
display: flex;
justify-content: space-between;
height: 30px;
line-height: 30px;
border-radius: 3px 3px 0px 0px;
padding: 0 12px;
::v-deep .el-checkbox {
.el-checkbox__label {
color: $color-text;
font-size: 14px;
padding-left: 8px;
}
}
.transfer-panel-ratio {
font-size: 12px;
color: $color-text;
}
}
.transfer-panel-body {
height: 200px;
padding: 12px 12px 0 12px;
border-top: 1px solid $color-border;
overflow: auto;
.transfer-panel-filter {
float: right;
width: 170px;
.el-checkbox__label {
color: $color-text;
font-size: 12px;
padding-left: 8px;
}
.el-input__inner {
height: 26px;
border: none;
}
}
::v-deep .el-tree {
color: $color-text;
margin-bottom: 4px;
.el-tree-node__content {
height: 22px;
margin-bottom: 8px;
.el-tree-node__label {
font-size: 12px;
}
}
.el-tree-node__children {
.el-tree-node__content {
padding-left: 12px!important;
}
}
.el-tree-node__expand-icon {
margin-left: -6px;
}
}
::v-deep .el-checkbox-group {
margin-bottom: 4px;
.el-checkbox {
display: block;
line-height: 22px;
color: $color-text;
margin-bottom: 8px;
width: 100%;
.el-checkbox__label {
width: calc(100% - 5px);
position: relative;
font-size: 12px;
padding-left: 8px;
img{
position: absolute;
right: 0;
top: 2px;
}
}
}
}
}
}
.transfer-buttons {
display: flex;
justify-content: center;
flex-flow: column;
margin: 0 12px;
.el-button {
display: flex;
justify-content: center;
align-items: center;
width: 32px;
height: 24px;
padding: 0;
margin-left: 0;
}
}
}
::v-deep .el-empty {
height: 60px;
padding: 0;
.el-empty__image {
display: none;
}
.el-empty__description {
margin: 0;
}
}
</style>
父组件调用
<template>
<div>
<TransferTreeList
:defaultProps="{ children: 'list', label: 'name', key: 'id', parentKey: 'classify' }"
:leftOriginalList="unselectedList"
:rightOriginalList="selectedList"
:maxLimitCount="10"
@change="handelSelectedChange" />
</div>
</template>
<script>
import TransferTreeList from '@/components/TransferTreeList';
export default {
name: '',
components: {
TransferTreeList,
},
data() {
return {
unselectedList: [ // 未被选中的选项
{
id: 'classify1',
name: '分类1',
list: [
{ id: 'kpi1-1', name: '选项1-1', classify: 'classify1' },
{ id: 'kpi1-3', name: '选项1-3', classify: 'classify1' },
],
},
{
id: 'classify2',
name: '分类2',
list: [
{ id: 'kpi2-1', name: '选项2-1', classify: 'classify2' },
{ id: 'kpi2-3', name: '选项2-3', classify: 'classify2' },
],
},
],
selectedList: [ // 被选中的选项(选项内部要有父节点标识)
{ id: 'kpi2-2', name: '选项2-2', classify: 'classify2' },
{ id: 'kpi1-2', name: '选项1-2', classify: 'classify1' },
],
};
},
methods: {
handelSelectedChange(data) {
console.log('最新数据', data)
},
},
};
</script>
到了这里,关于基于Element-ui 封装穿梭框(左侧树 右侧列表,可全选,列表可拖拽)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!