目录
- 本文内容
- 最终效果图
- 组件html布局
- 穿梭框左侧内容
- 穿梭框右侧内容
- 穿梭框中间向左、向右按钮
- 把排序好的穿梭数据传给父组件
- 整体代码
- 小结
本文内容
需求是实现类似 el-transfer的组件,右侧框内容可以拖动排序;
手写div样式 + vuedraggable组件实现。
最终效果图
组件html布局
新建一个组件文件 CustormTransfer.vue,穿梭框 html 分为左中右三部分,使用flex布局使其横向布局,此时代码如下
<template> | |
<div class="custom-transfer-cls"> | |
<div class="left-side"></div> | |
<div class="btn-cls"></div> | |
<div class="right-side"></div> | |
</div> | |
</template> | |
<script> | |
export default { | |
name: 'CustomTransferName', | |
components: {}, | |
props: {}, | |
data () { | |
return { } | |
}, | |
computed: { }, | |
created () {}, | |
mounted () { }, | |
methods: {} | |
} | |
</script> | |
<style lang="less" scoped> | |
.custom-transfer-cls { | |
display: flex; | |
justify-content: space-between; | |
min-height:px; | |
.left-side, | |
.right-side {} | |
.btn-cls { } | |
} | |
</style> |
此时页面上看不到组件内容。
穿梭框左侧内容
左侧内容是个列表,列表的每一项是多选框checkbox加文字标题,列表最上面是标题;所以.left-side的代码如下:
<div class="left-side"> | |
<!-- 标题 --> | |
<h>{{ titles[0] }}</h4> | |
<!-- 列表 --> | |
<div v-for="left in leftData" :key="left.key" class="item-cls"> | |
<el-checkbox :checked="left.checked" @change="leftCheckChange(left)" /> | |
<span :title="left.label">{{ left.label }}</span> | |
</div> | |
<!-- 数据为空时显示 --> | |
<div v-if="leftData.length ===" class="empty-text">{{ emptypText }}</div> | |
</div> |
解析:
- 列表标题使用h4标签,titles是组件使用者传入props的标题数组的第一项;
- 列表数据 leftData是组件使用者传入的数据处理之后的,因为我们默认el-checkbox不勾选,所以在生命周期mounted时,checked设为false;
- el-checkbox触发change事件时,执行函数leftCheckChange(left),去改变leftData数组对应项的checked设为取反;
- 当leftData数据为空时,显示数据为空的文本,此文本组件使用者可通过 属性 emptypText 传入,默认'数据为空';
- 列表的每一项的样式在 .item-cls 定义,内容过长时显示省略号,在 title 属性中显示全部内容;
- 列表整体内容多时,显示滚动条,滚动条样式重写;
以上内容加上样式、函数后如下:
<template> | |
<div class="custom-transfer-cls"> | |
<div class="left-side"></div> | |
<div class="btn-cls"></div> | |
<div class="right-side"></div> | |
</div> | |
</template> | |
<script> | |
export default { | |
name: 'CustomTransferName', | |
components: {}, | |
props: { | |
allData: { | |
type: Array, | |
default: () => { | |
// 对象数组需要有label、key两个属性 | |
return [] | |
} | |
}, | |
emptypText: { | |
type: String, | |
default: '数据为空' | |
}, | |
titles: { | |
type: Array, | |
default: () => { | |
return ['列表', '列表 2'] | |
} | |
} | |
}, | |
data () { | |
return { | |
leftData: [] | |
} | |
}, | |
computed: { }, | |
created () {}, | |
mounted () { | |
// 初始化列表的数据 | |
this.leftData = this.allData.map(a => { | |
a.checked = false | |
return a | |
}) | |
}, | |
methods: { | |
// 左边checkbox的change事件 | |
leftCheckChange (check) { | |
this.leftData = this.leftData.map(l => { | |
if (l.key === check.key) { | |
l.checked = !l.checked | |
} | |
return l | |
}) | |
} | |
} | |
} | |
</script> | |
<style lang="less" scoped> | |
.custom-transfer-cls { | |
display: flex; | |
justify-content: space-between; | |
min-height:px; | |
.left-side { | |
height:px; | |
overflow-y: scroll; | |
background-color: white; | |
width:px; | |
border:px solid #eee; | |
border-radius:px; | |
h { | |
/* 列表标题在列表滚动时吸附在顶部 */ | |
position: sticky; | |
top:px; | |
z-index:; | |
background: white; | |
text-align: center; | |
font-weight:; | |
margin-bottom:px; | |
} | |
/* 数据为空的样式 */ | |
.empty-text { | |
text-align: center; | |
color: #ccc; | |
} | |
/* 列表每项的样式,文字很长时显示省略号 */ | |
.item-cls { | |
margin-left:px; | |
margin-right:px; | |
overflow: hidden; | |
white-space: nowrap; | |
text-overflow: ellipsis; | |
} | |
/* 列表的滚动条样式重写 */ | |
&::-webkit-scrollbar { | |
width:px; | |
} | |
&::-webkit-scrollbar-thumb { | |
background: #ccc; | |
} | |
&::-webkit-scrollbar-track { | |
background: #ededed; | |
} | |
} | |
.btn-cls { } | |
} | |
</style> |
穿梭框右侧内容
右侧的列表需要具有可拖动排序的功能,我使用的使 vuedraggable组件,所以首先需要先安装npm install vuedraggable -S, 再引入 import draggable from 'vuedraggable',使用时配合 <transition-group>增加过渡效果;代码如下:
<div class="right-side"> | |
<h>{{ titles[1] }}</h4> | |
<draggable v-model="rightData"> | |
<transition-group> | |
<div v-for="(right, index) in rightData" :key="right.key" class="item-cls"> | |
<el-checkbox :checked="right.checked" @change="rightCheckChange(right)" /> | |
<span>{{ index + + '.' }}</span> | |
<span :title="right.label">{{ right.label }}</span> | |
</div> | |
</transition-group> | |
</draggable> | |
<div v-if="rightData.length ===" class="empty-text">{{ emptypText }}</div> | |
</div> |
解析:
- 右侧的列表样式和左侧一样;
- 只是多了一个<draggable></draggable>组件的使用
此时整体的代码如下:
<template> | |
<div class="custom-transfer-cls"> | |
<!-- 左侧列表 --> | |
<div class="left-side"> | |
<h>{{ titles[0] }}</h4> | |
<div v-for="left in leftData" :key="left.key" class="item-cls"> | |
<el-checkbox :checked="left.checked" @change="leftCheckChange(left)" /> | |
<span :title="left.label">{{ left.label }}</span> | |
</div> | |
<div v-if="leftData.length ===" class="empty-text">{{ emptypText }}</div> | |
</div> | |
<!-- 向左、向右操作按钮 --> | |
<div class="btn-cls"></div> | |
<!-- 右侧列表 --> | |
<div class="right-side"> | |
<h>{{ titles[1] }}</h4> | |
<draggable v-model="rightData"> | |
<transition-group> | |
<div v-for="(right, index) in rightData" :key="right.key" class="item-cls"> | |
<el-checkbox :checked="right.checked" @change="rightCheckChange(right)" /> | |
<span>{{ index + + '.' }}</span> | |
<span :title="right.label">{{ right.label }}</span> | |
</div> | |
</transition-group> | |
</draggable> | |
<div v-if="rightData.length ===" class="empty-text">{{ emptypText }}</div> | |
</div> | |
</div> | |
</template> | |
<script> | |
import draggable from 'vuedraggable' | |
export default { | |
name: 'CustomTransferName', | |
components: { | |
draggable | |
}, | |
props: { | |
allData: { | |
type: Array, | |
default: () => { | |
// 对象数组需要有label、key两个属性 | |
return [] | |
} | |
}, | |
checkedData: { | |
type: Array, | |
default: () => { | |
// 对象数组需要有label、key两个属性 | |
return [] | |
} | |
}, | |
emptypText: { | |
type: String, | |
default: '数据为空' | |
}, | |
titles: { | |
type: Array, | |
default: () => { | |
return ['标题', '标题2'] | |
} | |
} | |
}, | |
data () { | |
return { | |
leftData: [], | |
rightData: [] | |
} | |
}, | |
computed: {}, | |
created () {}, | |
mounted () { | |
// 初始化左侧列表的数据 | |
this.leftData = this.allData.map(a => { | |
a.checked = false | |
return a | |
}) | |
// 初始化右侧列表的数据 | |
this.rightData = this.checkedData.map(a => { | |
a.checked = false | |
return a | |
}) | |
}, | |
methods: { | |
// 左边选中 | |
leftCheckChange (check) { | |
this.leftData = this.leftData.map(l => { | |
if (l.key === check.key) { | |
l.checked = !l.checked | |
} | |
return l | |
}) | |
}, | |
// 右边选中 | |
rightCheckChange (check) { | |
this.rightData = this.rightData.map(l => { | |
if (l.key === check.key) { | |
l.checked = !l.checked | |
} | |
return l | |
}) | |
} | |
} | |
} | |
</script> | |
<style lang="less" scoped> | |
.custom-transfer-cls { | |
display: flex; | |
justify-content: space-between; | |
min-height:px; | |
.left-side, | |
.right-side { | |
height:px; | |
overflow-y: scroll; | |
background-color: white; | |
width:px; | |
border:px solid #eee; | |
border-radius:px; | |
h { | |
/* 列表标题在列表滚动时吸附在顶部 */ | |
position: sticky; | |
top:px; | |
z-index:; | |
background: white; | |
text-align: center; | |
font-weight:; | |
margin-bottom:px; | |
} | |
/* 数据为空的样式 */ | |
.empty-text { | |
text-align: center; | |
color: #ccc; | |
} | |
/* 列表每项的样式,文字很长时显示省略号 */ | |
.item-cls { | |
margin-left:px; | |
margin-right:px; | |
overflow: hidden; | |
white-space: nowrap; | |
text-overflow: ellipsis; | |
} | |
/* 列表的滚动条样式重写 */ | |
&::-webkit-scrollbar { | |
width:px; | |
} | |
&::-webkit-scrollbar-thumb { | |
background: #ccc; | |
} | |
&::-webkit-scrollbar-track { | |
background: #ededed; | |
} | |
} | |
} | |
</style> |
穿梭框中间向左、向右按钮
穿梭框的向左、向右按钮,使用<el-button icon="el-icon-arrow-right"></el-button>实现,代码如下:
<div class="btn-cls"> | |
<el-button | |
:disabled="toRightDisable" | |
plain | |
type="default" | |
size="small" | |
icon="el-icon-arrow-right" | |
@click="toRight" | |
/> | |
<el-button | |
:disabled="toLeftDisable" | |
class="right-btn" | |
plain | |
type="default" | |
size="small" | |
icon="el-icon-arrow-left" | |
@click="toLeft" | |
/> | |
</div> |
解析:
- 按钮的禁用disabled逻辑,在computed中定义toRightDisable、toLeftDisable;
- 按钮的点击事件 toRight、toLeft,是对左右两侧列表数组的运算;
此部分的代码如下:
<template> | |
<div class="custom-transfer-cls"> | |
<div class="left-side"></div> | |
<!-- 向左、向右按钮开始 --> | |
<div class="btn-cls"> | |
<el-button | |
:disabled="toRightDisable" | |
plain | |
type="default" | |
size="small" | |
icon="el-icon-arrow-right" | |
@click="toRight" | |
/> | |
<el-button | |
:disabled="toLeftDisable" | |
class="right-btn" | |
plain | |
type="default" | |
size="small" | |
icon="el-icon-arrow-left" | |
@click="toLeft" | |
/> | |
</div> | |
<!-- 向左、向右按钮结束 --> | |
<div class="right-side"></div> | |
</div> | |
</template> | |
<script> | |
export default { | |
name: 'CustomTransferName', | |
components: { }, | |
props: {}, | |
data () { | |
return { | |
leftData: [], | |
rightData: [] | |
} | |
}, | |
computed: { | |
// 向左穿梭按钮的disabled逻辑 | |
toLeftDisable () { | |
return !this.rightData.some(r => r.checked) | |
}, | |
// 向右穿梭按钮的disabled逻辑 | |
toRightDisable () { | |
return !this.leftData.some(r => r.checked) | |
} | |
}, | |
created () {}, | |
mounted () { }, | |
methods: { | |
// 数据向右穿梭 | |
toRight () { | |
// 左减去,右加上 | |
const leftUnchecked = this.leftData.filter(l => !l.checked) | |
const leftChecked = this.leftData.filter(l => l.checked) | |
this.leftData = leftUnchecked | |
this.rightData = [].concat(this.rightData, leftChecked).map(r => { | |
r.checked = false | |
return r | |
}) | |
}, | |
// 数据向左穿梭 | |
toLeft () { | |
// 右减去,左加上 | |
const rightUnchecked = this.rightData.filter(l => !l.checked) | |
const rightChecked = this.rightData.filter(l => l.checked) | |
this.rightData = rightUnchecked | |
this.leftData = [].concat(this.leftData, rightChecked).map(r => { | |
r.checked = false | |
return r | |
}) | |
} | |
} | |
} | |
</script> | |
<style lang="less" scoped> | |
.custom-transfer-cls { | |
display: flex; | |
justify-content: space-between; | |
min-height:px; | |
.btn-cls { | |
display: flex; | |
flex-direction: column; | |
justify-content: center; | |
align-items: center; | |
.right-btn { | |
margin-left:; | |
margin-top:px; | |
} | |
} | |
} | |
</style> |
把排序好的穿梭数据传给父组件
即把rightData: []数据通过$emit()传递出去,父组件监听dragedData事件之后获取; 定义函数 transferData(),在拖动完成时的@end事件调用,在向左向右更新了右侧列表数据之后调用;
代码如下:
methods: { | |
// 传递数据 | |
transferData () { | |
this.$emit('dragedData', this.rightData) | |
} | |
} |
整体代码
<template> | |
<div class="custom-transfer-cls"> | |
<!-- 左侧列表 --> | |
<div class="left-side"> | |
<h>{{ titles[0] }}</h4> | |
<div v-for="left in leftData" :key="left.key" class="item-cls"> | |
<el-checkbox :checked="left.checked" @change="leftCheckChange(left)" /> | |
<span :title="left.label">{{ left.label }}</span> | |
</div> | |
<div v-if="leftData.length ===" class="empty-text">{{ emptypText }}</div> | |
</div> | |
<!-- 向左、向右按钮开始 --> | |
<div class="btn-cls"> | |
<el-button | |
:disabled="toRightDisable" | |
plain | |
type="default" | |
size="small" | |
icon="h-icon-angle_right" | |
@click="toRight" | |
/> | |
<el-button | |
:disabled="toLeftDisable" | |
class="right-btn" | |
plain | |
type="default" | |
size="small" | |
icon="h-icon-angle_left" | |
@click="toLeft" | |
/> | |
</div> | |
<!-- 右侧列表 --> | |
<div class="right-side"> | |
<h>{{ titles[1] }}</h4> | |
<draggable v-model="rightData" @end="transferData"> | |
<transition-group> | |
<div v-for="(right, index) in rightData" :key="right.key" class="item-cls"> | |
<el-checkbox :checked="right.checked" @change="rightCheckChange(right)" /> | |
<span>{{ index + + '.' }}</span> | |
<span :title="right.label">{{ right.label }}</span> | |
</div> | |
</transition-group> | |
</draggable> | |
<div v-if="rightData.length ===" class="empty-text">{{ emptypText }}</div> | |
</div> | |
</div> | |
</template> | |
<script> | |
// 可拖动组件 | |
import draggable from 'vuedraggable' | |
export default { | |
name: 'CustomTransferName', | |
components: { | |
draggable | |
}, | |
props: { | |
allData: { | |
type: Array, | |
default: () => { | |
// 对象数组需要有label、key两个属性 | |
return [] | |
} | |
}, | |
checkedData: { | |
type: Array, | |
default: () => { | |
// 对象数组需要有label、key两个属性 | |
return [] | |
} | |
}, | |
emptypText: { | |
type: String, | |
default: '数据为空' | |
}, | |
titles: { | |
type: Array, | |
default: () => { | |
return ['标题', '标题2'] | |
} | |
} | |
}, | |
data () { | |
return { | |
leftData: [], | |
rightData: [] | |
} | |
}, | |
computed: { | |
// 向左穿梭按钮的disabled逻辑 | |
toLeftDisable () { | |
return !this.rightData.some(r => r.checked) | |
}, | |
// 向右穿梭按钮的disabled逻辑 | |
toRightDisable () { | |
return !this.leftData.some(r => r.checked) | |
} | |
}, | |
created () {}, | |
mounted () { | |
// 初始化左侧列表的数据 | |
this.leftData = this.allData.map(a => { | |
a.checked = false | |
return a | |
}) | |
// 初始化右侧列表的数据 | |
this.rightData = this.checkedData.map(a => { | |
a.checked = false | |
return a | |
}) | |
}, | |
methods: { | |
// 传递数据 | |
transferData () { | |
this.$emit('dragedData', this.rightData) | |
}, | |
// 左边选中 | |
leftCheckChange (check) { | |
this.leftData = this.leftData.map(l => { | |
if (l.key === check.key) { | |
l.checked = !l.checked | |
} | |
return l | |
}) | |
}, | |
// 右边选中 | |
rightCheckChange (check) { | |
this.rightData = this.rightData.map(l => { | |
if (l.key === check.key) { | |
l.checked = !l.checked | |
} | |
return l | |
}) | |
}, | |
// 数据向右穿梭 | |
toRight () { | |
// 左减去,右加上 | |
const leftUnchecked = this.leftData.filter(l => !l.checked) | |
const leftChecked = this.leftData.filter(l => l.checked) | |
this.leftData = leftUnchecked | |
this.rightData = [].concat(this.rightData, leftChecked).map(r => { | |
r.checked = false | |
return r | |
}) | |
// 传递数据 | |
this.transferData() | |
}, | |
// 数据向左穿梭 | |
toLeft () { | |
// 右减去,左加上 | |
const rightUnchecked = this.rightData.filter(l => !l.checked) | |
const rightChecked = this.rightData.filter(l => l.checked) | |
this.rightData = rightUnchecked | |
this.leftData = [].concat(this.leftData, rightChecked).map(r => { | |
r.checked = false | |
return r | |
}) | |
// 传递数据 | |
this.transferData() | |
} | |
} | |
} | |
</script> | |
<style lang="less" scoped> | |
.custom-transfer-cls { | |
display: flex; | |
justify-content: space-between; | |
min-height:px; | |
.left-side, | |
.right-side { | |
height:px; | |
overflow-y: scroll; | |
background-color: white; | |
width:px; | |
border:px solid #eee; | |
border-radius:px; | |
/* 标题样式 */ | |
h { | |
position: sticky; | |
top:px; | |
z-index:; | |
background: white; | |
text-align: center; | |
font-weight:; | |
margin-bottom:px; | |
} | |
/* 数据为空时的样式 */ | |
.empty-text { | |
text-align: center; | |
color: #ccc; | |
} | |
/* 列表每一项样式 */ | |
.item-cls { | |
margin-left:px; | |
margin-right:px; | |
overflow: hidden; | |
white-space: nowrap; | |
text-overflow: ellipsis; | |
} | |
/* 列表滚动条样式 */ | |
&::-webkit-scrollbar { | |
width:px; | |
} | |
&::-webkit-scrollbar-thumb { | |
background: #ccc; | |
} | |
&::-webkit-scrollbar-track { | |
background: #ededed; | |
} | |
} | |
/* 按钮样式 */ | |
.btn-cls { | |
display: flex; | |
flex-direction: column; | |
justify-content: center; | |
align-items: center; | |
.right-btn { | |
margin-left:; | |
margin-top:px; | |
} | |
} | |
} | |
</style> |