穿梭框
该组件是个穿梭框,因为使用了 滚动加载,所以支持数据量较大的情况。
基于 Vue 、element-ui 的 【Checkbox 多选框】& 【Input 输入框】& 【InfiniteScroll 无限滚动】
代码分析
<template>
<div class="transfer-wrapper">
<div class="flex-dir-column padd-all-15">
<el-input v-model="query" :placeholder="placeholder" prefix-icon="el-icon-search" class="mar-b-15" clearable></el-input>
<el-checkbox v-if="allCheck" v-model="checkAll" @change="handleCheckAllChange">全选</el-checkbox>
<div class="over-auto flex-auto" v-infinite-scroll="load_l_d" infinite-scroll-distance="10">
<el-checkbox-group v-model="r_d" class="flex-dir-column">
<el-checkbox @change="checkHandle($event, item.key)" v-for="(item, index) in l_d_s" :key="index" :label="item.key" class="checkbox">{{item.label}}</el-checkbox>
</el-checkbox-group>
</div>
</div>
<div class="fgx"></div>
<div class="flex-dir-column padd-all-15 flex-auto">
<div class="empty-tips" v-if="!r_d.length">{{emptyText}}</div>
<div v-else class="select-tips">已选择 <span class="color-e6a441">{{r_d.length}}</span> 个{{checkAddTips}}</div>
<div class="over-auto flex-auto" v-infinite-scroll="load_r_d" infinite-scroll-distance="10">
<div class="dis-flex select-item flex-cross-center" v-for="(val, key) in r_d_s" :key="key">
<div class="flex-auto">{{labelByKey[val]}}</div>
<i class="el-icon-error pointer" @click="del(val)"></i>
</div>
</div>
</div>
<!-- 这里隐藏渲染一个 l_d_set 主要是为了触发 computed 中的 l_d_set 执行 -->
<span style="display: none;">{{l_d_set}}</span>
</div>
</template>
<style scoped>
.transfer-wrapper {
border: 1px solid #dddddd;
border-radius: 2px 0 2px 2px 2px;
display: flex;
width: 500px;
height: 300px;
}
.fgx {
width: 1px;
background-color: #dddddd;
}
.checkbox {
line-height: 30px;
margin-left: 0 !important;
}
.select-item {
line-height: 20px;
padding: 7px 10px 7px 0;
}
.empty-tips {
font-size: 12px;
color: #91a1a9;
}
.select-tips {
font-size: 12px;
color: #91a1a9;
line-height: 34px;
}
.color-e6a441 {
color: #e6a441;
}
.el-form-item.is-error .transfer-wrapper {
border-color: #f56c6c;
}
.el-form-item.is-error .transfer-wrapper >>> .el-input__inner {
border-color: #dcdfe6;
}
.flex-dir-column {
display: flex;
flex-direction: column;
}
.padd-all-15 {
padding: 15px;
}
.mar-b-15 {
margin-bottom: 15px;
}
.over-auto {
overflow: auto;
}
.flex-auto {
flex: 1 1 auto;
}
.dis-flex {
display: flex;
}
.flex-cross-center {
align-items: center !important;
}
.pointer {
cursor: pointer;
}
</style>
<script>
export default {
name: 'transfer',
props: {
value: {
type: Array,
default: function () {
return []
}
},
allCheck: {
type: Boolean
},
placeholder: {
type: String,
default: '请输入'
},
data: {
required: true,
type: Array,
default: function () {
return []
}
},
emptyText: {
type: String,
default: '请选择'
},
checkAddTips: {
required: false,
type: String,
default: ''
}
},
watch: {
query(n) {
clearTimeout(this.timer)
this.timer = setTimeout(() => {
this.l_d = [...this.l_d_set].filter(
v => v.label.indexOf(n) != -1
)
}, 300)
},
l_d() {
this.l_d_s = []
this.load_l_d()
},
r_d() {
this.r_d_s = []
this.load_r_d()
}
},
computed: {
checkAll: {
get: function () {
if (!this.l_d.length) {
return false
} else if (this.r_d.length >= this.l_d.length) {
let ret = true
this.l_d.forEach(v => {
if (!this.r_d.includes(v.key)) ret = false
})
return ret
} else {
return false
}
},
set: function (val) {}
},
labelByKey() {
return [...this.l_d_set].reduce((a, b) => {
a[b.key] = b.label
return a
}, {})
},
l_d_set() {
this.l_d = [...this.data]
return new Set(this.data)
},
r_d: {
get() {
return [...this.value]
},
set(n) {
this.$emit('input', n)
this.$nextTick(() => {
this.$emit('change', n)
// 以下代码可以触发 el-form-item 的change 校验
try {
this.$parent.validate('change')
} catch (error) {}
})
}
}
},
data() {
return {
timer: null,
query: '',
l_d: [],
l_d_s: [],
r_d_s: [],
step: 10 //这里可以设置每次加载项的个数
}
},
methods: {
handleCheckAllChange(selected) {
const opearIds = this.l_d.map(v => v.key)
if (selected) {
this.r_d = [...new Set([...this.r_d, ...opearIds])]
} else {
this.r_d_s = this.r_d_s.filter(v => !opearIds.includes(v))
this.r_d = this.r_d.filter(v => !opearIds.includes(v))
}
},
del(key) {
this.r_d = this.r_d.filter(v => v !== key)
this.r_d_s = this.r_d_s.filter(v => v !== key)
},
load_l_d() {
const start = this.l_d_s.length
const end = Math.min(start + this.step, this.l_d.length)
;[...this.l_d].slice(start, end).forEach(e => {
this.l_d_s.push(e)
})
},
load_r_d() {
const start = this.r_d_s.length
const end = Math.min(start + this.step, this.r_d.length)
;[...this.r_d].slice(start, end).forEach(e => {
this.r_d_s.push(e)
})
},
checkHandle(isAdd, key) {
if (!isAdd) this.del(key)
}
}
}
</script>
组件使用(基础组件可进行全局注册,直接使用)
<ym-transfer :data="animalIdList" v-model="animalIds" allCheck checkAddTips="小动物"></ym-transfer>
<script>
export default {
data(){
return {
animalIds: ['1','2','3','4'],
animalIdList: [
{ label: '小猪', key: '1' },
{ label: '小鸭', key: '2' },
{ label: '小鱼', key: '3' },
{ label: '小鸡', key: '4' },
{ label: '小马', key: '5' },
{ label: '小狗', key: '6' },
{ label: '蚂蚁', key: '7' },
{ label: '小兔', key: '8' },
{ label: '大象', key: '9' },
{ label: '鳄鱼', key: '10' },
{ label: '鸽子', key: '11' },
{ label: '天鹅', key: '12' },
{ label: '蛤蟆', key: '13' },
{ label: '海马', key: '14' },
{ label: '企鹅', key: '15' },
{ label: '小驴', key: '16' }
],
}
}
}
</script>
属性
参数 | 说明 | 类型 | require | 默认值 |
---|---|---|---|---|
v-model | 绑定值(当绑定值为空数组时,默认是全部选中) | Array | true | [] |
allCheck | 是否展示全选按钮 | Boolean | false | false |
data | 需要展示的列表,每项需要符合 { label: ‘小猪’, key: ‘1’ }形式 | Array | true | [] |
placeholder | 自定义 搜索框 placeholder | String | false | 请输入 |
emptyText | 自定义 右侧 没有选择时的展示文字 | String | false | 请选择 |
checkAddTips | 自定义 右侧 选择时的展示个数描述(如小动物) | String | false | 无 |
组件使用注意事项
- v-mode 绑定时,[1] 和 [‘1’] 是不一样的,需要和 data 中的 key 保持一致
- 组件基于 element 用到的组件有:【Checkbox 多选框】& 【Input 输入框】& 【InfiniteScroll 无限滚动】,使用前请确保以上组件已注册
- 初始加载 step 不能过小,当元素不能撑满高度时,就不会出现滚动条,进而不能触发滚动事件 加载后续项
关于大数据量的思考
- 当数据量较大时,页面会出现卡顿。主要原因:页面渲染占用时间过长,其实数据的存储没什么问题。
- 本组件采取 滚动加载的形式,来解决大数据量的展示问题,亲测两千左右的数据不会有问题。