手寫可拖動穿梭框組件CustormTransfer vue實現示例
本文內容
需求是實現類似 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的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- Vue實用功能之實現拖拽元素、列表拖拽排序
- Vue 實現穿梭框功能的詳細代碼
- 基於Vue技術實現遞歸組件的方法
- vue如何使用vue slot封裝公共組件
- vue + element動態多表頭與動態插槽