手寫可拖動穿梭框組件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其它相關文章!

推薦閱讀: