某次项目要使用穿梭框进行数据选择,项目使用的element-ui框架,框架中的穿梭框是这样子的:
包括图片等信息,也要加上很多样式等等,我尝试这去改造,一会后觉得还是自己动手去写一个靠谱。几经鼓捣效果如下:
基本上实现了一个穿梭框。以上是展示内容,不包含实际使用。具体可以自定义实现其中的渲染格式。比如
上干货
<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