手写可拖动穿梭框组件CustormTransfer vue实现示例
前端叭叭说 人气:0本文内容
需求是实现类似 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: 120px; .left-side, .right-side {} .btn-cls { } } </style>
此时页面上看不到组件内容。
穿梭框左侧内容
左侧内容是个列表,列表的每一项是多选框checkbox
加文字标题,列表最上面是标题;所以.left-side
的代码如下:
<div class="left-side"> <!-- 标题 --> <h4>{{ 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 === 0" 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 ['列表 1', '列表 2'] } } }, data () { return { leftData: [] } }, computed: { }, created () {}, mounted () { // 初始化列表1的数据 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: 120px; .left-side { height: 240px; overflow-y: scroll; background-color: white; width: 140px; border: 1px solid #eee; border-radius: 4px; h4 { /* 列表标题在列表滚动时吸附在顶部 */ position: sticky; top: 0px; z-index: 9; background: white; text-align: center; font-weight: 400; margin-bottom: 16px; } /* 数据为空的样式 */ .empty-text { text-align: center; color: #ccc; } /* 列表每项的样式,文字很长时显示省略号 */ .item-cls { margin-left: 12px; margin-right: 12px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } /* 列表的滚动条样式重写 */ &::-webkit-scrollbar { width: 1px; } &::-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"> <h4>{{ 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 + 1 + '.' }}</span> <span :title="right.label">{{ right.label }}</span> </div> </transition-group> </draggable> <div v-if="rightData.length === 0" class="empty-text">{{ emptypText }}</div> </div>
解析:
- 右侧的列表样式和左侧一样;
- 只是多了一个
<draggable></draggable>
组件的使用
此时整体的代码如下:
<template> <div class="custom-transfer-cls"> <!-- 左侧列表 --> <div class="left-side"> <h4>{{ 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 === 0" class="empty-text">{{ emptypText }}</div> </div> <!-- 向左、向右操作按钮 --> <div class="btn-cls"></div> <!-- 右侧列表 --> <div class="right-side"> <h4>{{ 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 + 1 + '.' }}</span> <span :title="right.label">{{ right.label }}</span> </div> </transition-group> </draggable> <div v-if="rightData.length === 0" 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 ['标题1', '标题2'] } } }, data () { return { leftData: [], rightData: [] } }, computed: {}, created () {}, mounted () { // 初始化左侧列表1的数据 this.leftData = this.allData.map(a => { a.checked = false return a }) // 初始化右侧列表2的数据 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: 120px; .left-side, .right-side { height: 240px; overflow-y: scroll; background-color: white; width: 140px; border: 1px solid #eee; border-radius: 4px; h4 { /* 列表标题在列表滚动时吸附在顶部 */ position: sticky; top: 0px; z-index: 9; background: white; text-align: center; font-weight: 400; margin-bottom: 16px; } /* 数据为空的样式 */ .empty-text { text-align: center; color: #ccc; } /* 列表每项的样式,文字很长时显示省略号 */ .item-cls { margin-left: 12px; margin-right: 12px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } /* 列表的滚动条样式重写 */ &::-webkit-scrollbar { width: 1px; } &::-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: 120px; .btn-cls { display: flex; flex-direction: column; justify-content: center; align-items: center; .right-btn { margin-left: 0; margin-top: 8px; } } } </style>
把排序好的穿梭数据传给父组件
即把rightData: []
数据通过$emit()
传递出去,父组件监听dragedData
事件之后获取; 定义函数 transferData()
,在拖动完成时的@end
事件调用,在向左向右更新了右侧列表数据之后调用;
代码如下:
methods: { // 传递数据 transferData () { this.$emit('dragedData', this.rightData) } }
整体代码
<template> <div class="custom-transfer-cls"> <!-- 左侧列表 --> <div class="left-side"> <h4>{{ 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 === 0" 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"> <h4>{{ 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 + 1 + '.' }}</span> <span :title="right.label">{{ right.label }}</span> </div> </transition-group> </draggable> <div v-if="rightData.length === 0" 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 ['标题1', '标题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 () { // 初始化左侧列表1的数据 this.leftData = this.allData.map(a => { a.checked = false return a }) // 初始化右侧列表2的数据 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: 120px; .left-side, .right-side { height: 240px; overflow-y: scroll; background-color: white; width: 140px; border: 1px solid #eee; border-radius: 4px; /* 标题样式 */ h4 { position: sticky; top: 0px; z-index: 9; background: white; text-align: center; font-weight: 400; margin-bottom: 16px; } /* 数据为空时的样式 */ .empty-text { text-align: center; color: #ccc; } /* 列表每一项样式 */ .item-cls { margin-left: 12px; margin-right: 12px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } /* 列表滚动条样式 */ &::-webkit-scrollbar { width: 1px; } &::-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: 0; margin-top: 8px; } } } </style>
小结
本文主要写了一个可拖动排序的穿梭框组件,更多关于拖动穿梭框CustormTransfer vue的资料请关注其它相关文章!
加载全部内容