某次项目要使用穿梭框进行数据选择,项目使用的element-ui框架,框架中的穿梭框是这样子的:

VUE使用ElementUI的table表格重构穿梭框 vue穿梭框组件_node.js

包括图片等信息,也要加上很多样式等等,我尝试这去改造,一会后觉得还是自己动手去写一个靠谱。几经鼓捣效果如下:

VUE使用ElementUI的table表格重构穿梭框 vue穿梭框组件_javascript_02

 

VUE使用ElementUI的table表格重构穿梭框 vue穿梭框组件_插槽_03

 

VUE使用ElementUI的table表格重构穿梭框 vue穿梭框组件_数据_04

 基本上实现了一个穿梭框。以上是展示内容,不包含实际使用。具体可以自定义实现其中的渲染格式。比如

上干货

<template>
  <div class="shuttle" :style="{'height':height}">
    <div class="shuttle_header" ref="header">
      <!-- 这个插槽是给筛选条件集成的,数据怎么来的由使用者处理 -->
      <slot name="header"></slot>
    </div>
    <div class="shuttle_body" :style="{'height':bodyHeight}">
      <div class="shuttle_body_left" ref="leftBody" @scroll="leftBodyScroll">
        <div class="shuttle_body_left_item" v-for="(item,index) in handleData" :key="index">
          <!-- 数据源列表,将渲染格式插入这个插槽 -->
          <slot name="source" v-bind:item="item"></slot>
          <input
            type="checkbox"
            name="vehicle"
            class="checkbox"
            v-model="item.$$shuttleSelect"
            @change="checkbox(item,'$$shuttleSelect')"
          />
        </div>
      </div>
      <div class="shuttle_body_center">
        <button type="button" class="_btn _btn_bule" @click="toRight()">{{ buttonText[0] }}</button>
        <button type="button" class="_btn _btn_bule" @click="toLeft()">{{ buttonText[1] }}</button>
      </div>
      <div class="shuttle_body_right" ref="rightBody">
        <div class="shuttle_body_right_item" v-for="(item,index) in choiceData" :key="index">
          <!-- 目标渲染插槽 -->
          <slot name="target" v-bind:item="item"></slot>
          <input
            type="checkbox"
            name="vehicle"
            class="checkbox"
            v-model="item.$$choiceSelect"
            @change="checkbox(item,'$$choiceSelect')"
          />
        </div>
      </div>
    </div>
  </div>
</template>

<script>
/***
 * 自定义穿梭框
 * author Maldway
 *
 *暂未实现全选
 */
export default {
  name: 'VueShuttle',
  data() {
    return {
      bodyHeight: 0,
      handleData: [],
      choiceData: [],
      leftSelectAll: false,
      rightSelectAll: false
    }
  },
  components: {
    //组件
  },
  props: {
    //参数
    /**
     * 渲染数据源
     */
    data: {
      type: Array,
      required: true
    },
    /**
     * 组件高度
     */
    height: {
      type: String,
      required: true
    },
    /***
     * 按钮文字
     */
    buttonText: {
      type: Array,
      default: function () {
        return ['向右移动', '向左移动']
      }
    },
    /***
     * 初始状态下左侧勾选的数据
     */
    leftDefaultChecked: {
      type: Array,
      default: function () {
        return []
      }
    },
    /***
     * 初始状态下左侧勾选的数据的key值
     */
    leftDefaultCheckedKey: {
      type: String
    }
  },
  computed: {
    //计算属性
  },
  watch: {
    //监听
    data: {
      handler(newObj, oldObj) {
        this.handleData = this.transformation()
      },
      immediate: true,
      deep: true
    }
  },
  created: function () {
    //可访问实例,dom还未渲染
  },
  mounted: function () {
    //dom已经挂载
    this.$nextTick(() => {
      /**
       * 动态设置高度
       */
      let offsetHeight = this.$refs.header.offsetHeight
      let height = this.height
      this.bodyHeight = `calc(${height} - ${offsetHeight}px)`
      /**
       * 初始化左侧勾选数据,并将勾选的数据移动到右边
       */
      if (
        this.leftDefaultChecked &&
        this.leftDefaultChecked.length > 0 &&
        this.leftDefaultCheckedKey &&
        this.leftDefaultCheckedKey != ''
      ) {
        for (let i in this.handleData) {
          let item = this.handleData[i]
          let is = this.leftDefaultChecked.includes(
            item[this.leftDefaultCheckedKey]
          )
          if (typeof item['$$shuttleSelect'] != 'undefined') {
            if (is) {
              item['$$shuttleSelect'] = true
            } else {
              item['$$shuttleSelect'] = false
            }
          }
        }
        // 移动到右边
        this.toRight()
      } else {
        console.warn('No default value')
      }
    })
  },
  methods: {
    /**
     * 左边滚动事件
     */
    leftBodyScroll() {
      //获取距离顶部的距离
      let leftBodyScrollTop = this.$refs.leftBody.scrollTop
      leftBodyScrollTop = parseFloat(leftBodyScrollTop.toFixed())
      // 获取可视区的高度
      let leftBodyClientHeight = this.$refs.leftBody.clientHeight
      // 获取滚动条的总高度
      let leftBodyScrollHeight = this.$refs.leftBody.scrollHeight
      if (leftBodyScrollTop + leftBodyClientHeight >= leftBodyScrollHeight) {
        // 把距离顶部的距离加上可视区域的高度 等于或者大于滚动条的总高度就是到达底部
        this.$emit('toLeftBottom')
      }
    },
    reset(newArray) {
      this.choiceData = []
      if (
        newArray &&
        newArray.length > 0 &&
        this.leftDefaultCheckedKey &&
        this.leftDefaultCheckedKey != ''
      ) {
        for (let i in this.handleData) {
          let item = this.handleData[i]
          let is = newArray.includes(item[this.leftDefaultCheckedKey])
          if (typeof item['$$shuttleSelect'] != 'undefined') {
            if (is) {
              item['$$shuttleSelect'] = true
            } else {
              item['$$shuttleSelect'] = false
            }
          }
        }
        // 移动到右边
        this.toRight()
      } else {
        for (let i in this.handleData) {
          let item = this.handleData[i]
          if (typeof item['$$shuttleSelect'] != 'undefined') {
            item['$$shuttleSelect'] = false
          }
        }
      }
    },
    obtain(attribute) {
      /***
       * 将指定属性名返回对应数组。
       * 获得选择数组对象中某个属性的数组。
       */
      let keyList = []
      for (let key in this.choiceData) {
        let item = this.choiceData[key]
        if (item['$$shuttleSelect']) {
          keyList.push(item[attribute + ''])
        }
      }
      return keyList
    },
    transformation() {
      //转变数据,添加自定义属性
      let $data = [...this.data]
      for (let key in $data) {
        let item = $data[key]
        if (typeof item !== 'object') {
          throw new Error('not Object')
        }
        //是否存在侵入的属性
        if (!item['$$shuttleSelect'] && !item['$$choiceSelect']) {
          this.$set(item, '$$shuttleSelect', false)
          this.$set(item, '$$choiceSelect', false)
        }
      }
      return $data
    },
    checkbox(item, key) {
      //选中
      item[key] = !item[key]
      if (item[key]) {
        item[key] = false
      } else {
        item[key] = true
      }
      /**
       * 取消全选
       */
      if (this.leftSelectAll && !item[key]) {
        this.leftSelectAll = false
      }
      if (this.rightSelectAll && !item[key]) {
        this.rightSelectAll = false
      }
      /**
       * 全选
       */
      if (!this.leftSelectAll && item[key]) {
        let is = this.handleData.every(function (item, index, array) {
          return item['$$shuttleSelect'] == true
        })
        this.leftSelectAll = is
      }
      if (!this.rightSelectAll && item[key]) {
        // 如果其中每一项都是true 的话就选中
        let is = this.choiceData.every(function (item, index, array) {
          return item['$$choiceSelect'] == true
        })
        this.rightSelectAll = is
      }
    },
    toRight() {
      //移动到右边
      this.choiceData = []
      for (let key in this.handleData) {
        let item = this.handleData[key]
        if (item['$$shuttleSelect']) {
          this.choiceData.push(item)
          item['$$choiceSelect'] = false
        }
      }
    },
    toLeft() {
      //移动到左边
      for (let i = 0; i < this.choiceData.length; ) {
        let item = this.choiceData[i]
        if (item['$$choiceSelect']) {
          item['$$shuttleSelect'] = false
          item['$$choiceSelect'] = false
          this.$delete(this.choiceData, i)
        } else {
          i++
        }
      }
    },
    isAllLeft() {
      if (this.leftSelectAll) {
        this.selectAllLeft(true)
      } else {
        this.selectAllLeft(false)
      }
    },
    isAllRight() {
      if (this.rightSelectAll) {
        this.selectAllRight(true)
      } else {
        this.selectAllRight(false)
      }
    },
    selectAllLeft(isSelect) {
      /**
       * 全选/取消 左边
       */
      for (let i in this.handleData) {
        let item = this.handleData[i]
        item['$$shuttleSelect'] = isSelect
      }
    },
    selectAllRight(isSelect) {
      /**
       * 全选/取消 右边
       */
      for (let i in this.choiceData) {
        let item = this.choiceData[i]
        item['$$choiceSelect'] = isSelect
      }
    }
  }
}
</script>

<style lang="scss" scoped>
.shuttle {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: flex-start;
  align-content: flex-start;
  position: relative;

  width: 100%;
  height: auto;
  padding: 10px;

  &_header {
    width: 100%;
    flex-basis: 100%;
    height: auto;
    position: relative;
    box-sizing: border-box;
    margin-bottom: 10px;
  }
  &_body {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    align-items: center;
    align-content: center;
    flex-basis: 100%;
    width: 100%;
    box-sizing: border-box;

    &_left,
    &_center,
    &_right {
      border-radius: 5px;
      height: 100%;
      overflow-y: auto;
      overflow-x: hidden;
      background: #fff;
      display: inline-block;
      vertical-align: middle;
      box-sizing: border-box;
    }
    &_left {
      width: calc(50% - 120px);
      border: 1px solid #ebeef5;
      display: flex;
      flex-wrap: wrap;
      justify-content: flex-start;
      align-items: flex-start;
      align-content: flex-start;
      &_item {
        position: relative;
        border: 1px solid #ebeef5;
        margin: 10px;
        border-radius: 5px;
        .checkbox {
          position: absolute;
          top: 5px;
          left: 5px;
          width: 15px;
          height: 15px;
        }
      }
    }
    &_center {
      display: flex;
      flex-wrap: wrap;
      justify-content: center;
      align-items: center;
      align-content: center;
      width: 220px;
      ._btn {
        display: inline-block;
        line-height: 1;
        white-space: nowrap;
        cursor: pointer;
        background: #ffffff;
        border: 1px solid #dcdfe6;
        border-color: #dcdfe6;
        color: #606266;
        text-align: center;
        box-sizing: border-box;
        outline: none;
        margin: 0;
        transition: 0.1s;
        font-weight: 400;
        padding: 12px 20px;
        font-size: 14px;
        border-radius: 4px;

        &_bule {
          width: 100px;
          background: #46a6ff;
          border-color: #46a6ff;
          color: #ffffff;
          margin: 10px;
        }
      }
    }
    &_right {
      width: calc(50% - 120px);
      border: 1px solid #ebeef5;
      display: flex;
      flex-wrap: wrap;
      justify-content: flex-start;
      align-items: flex-start;
      align-content: flex-start;
      &_item {
        position: relative;
        border: 1px solid #ebeef5;
        margin: 10px;
        border-radius: 5px;
        .checkbox {
          position: absolute;
          top: 5px;
          left: 5px;
          width: 15px;
          height: 15px;
        }
      }
    }
  }
}
</style>

插件说明

属性名

类型

说明

data

Array

渲染数据源

height

String

组件高度;'100px'

buttonText

Array

['向右移动', '向左移动']

leftDefaultChecked

Array

初始状态下左侧勾选的数据

leftDefaultCheckedKey

String

初始状态下左侧勾选的数据的key值

事件名

参数

说明

toLeftBottom

none

左边选择框滑动到底事件,用于分页

方法名

参数

说明

obtain

none

将指定属性名返回对应数组。获得选择数组对象中某个属性的数组。例如获取选中项id组成的数组。

selectAllLeft

Boolen:isSelect

全选/取消 左边;根据入参isSelect

selectAllRight

Boolen:isSelect

全选/取消 左边;根据入参isSelect

插槽名

数据

说明

header

none

头部搜索等区域

source

item

左边渲染区域

target

item

右边渲染区域

使用示例

实际代码使用如下:

<template>
	<div class="app-container">
		<VueShuttle class="rectangle-shuttle" :data="shuttleOne.data" :height="shuttleOne.height">
			<template v-slot:source="{ item }">
				<div class="rectangle">
					<span class="rectangle_name"> {{ item.name }} </span>
					<span class="rectangle_sex"> {{ item.sex }} </span>
					<span class="rectangle_age"> {{ item.age }} </span>
					<p class="rectangle_hobby">
						<span v-for=" (h,i) in item.hobby" :key="h+i">
							{{ h }}
						</span>
					</p>
				</div>
			</template>
			<template v-slot:target="{ item }">
				<div class="rectangle">
					<span class="rectangle_name"> {{ item.name }} </span>
					<span class="rectangle_sex"> {{ item.sex }} </span>
					<span class="rectangle_age"> {{ item.age }} </span>
					<p class="rectangle_hobby">
						<span v-for=" (h,i) in item.hobby" :key="h+i">
							{{ h }}
						</span>
					</p>
				</div>
			</template>
		</VueShuttle>
	</div>
</template>

<script>
	import VueShuttle from '@/components/VueShuttle/index.vue'
	export default {
		name: '',
		data() {
			const getData = function(individual = 3) {
				const hobbyList = ['篮球', '足球', '排球', '游泳', '爬山', '羽毛球']
				const sexList = ['男', '女']
				const lastnameList = ['张', '王', '李', '刘']
				let peopleList = [];
				for (let i = 0; i < individual; i++) {
					peopleList.push({
						name: lastnameList[randomNum(0, lastnameList.length - 1)] + randomNum(0, lastnameList
							.length - 1),
						headImge: '',
						age: randomNum(18, 60),
						sex: sexList[randomNum(0, sexList.length - 1)],
						hobby: [hobbyList[randomNum(0, hobbyList.length - 1)], hobbyList[randomNum(0, hobbyList
							.length - 1)]]
					})
				}
				return peopleList;
			}
			const randomNum = function(minNum, maxNum) {
				return parseInt(Math.random() * (maxNum - minNum + 1) + minNum, 10);
			}
			return {
				index: 3,
				shuttleOne: {
					data: getData(4),
					height: '360px'
				},
			}
		},
		components: {
			//组件
			VueShuttle
		},
		props: {
			//参数
		},
		computed: {
			//计算属性
		},
		watch: {
			//监听
		},
		created: function() {
			//可访问实例,dom还未渲染
		},
		mounted: function() {
			//dom已经挂载
		},
		methods: {

		}
	}
</script>

<style lang="scss" scoped>
	.app-container {
		.shuttle {
			margin-bottom: 20px;
		}
	}

	.rectangle-shuttle {
		.rectangle {
			display: flex;
			justify-content: flex-start;
			align-items: center;
			align-content: center;
			flex-wrap: wrap;
			padding-top: 20px;
			padding-left: 20px;
			width: 140px;
			height: 140px;

			&_name {
				display: inline-block;
				flex-basis: 100%;
				font-size: 24px;
				font-weight: bold;
			}

			&_sex {
				margin: 2px;
			}

			&_age {
				margin: 2px;
			}

			&_hobby {
				flex-basis: 100%;
			}
		}
	}
</style>

总结:

整体方案如下:

1.将传入的数据源添加选中标识,分为左选中与右选中。

2.选择部分插件渲染,将原数据通过插槽分发出去。

3.获取数据通过res属性持有引用调用方法。

4.预设(未测试)了全选与反选方法。

5.提供额外的插槽以集成搜索等所需部件。

6.提供上拉事件以集成分页功能。

这个插件主要功能就是提供了选择相关逻辑,将选择什么样的内容交由开发者自行处理,适合需要自定义内容的穿梭框或者变种表格等场景。

npm安装

npm -i vue-shuttle