概述
项目开发中,偶尔会用到穿梭框,具体什么场景,可以根据需求而定,但是这个组件不怎么常用,我觉得这个组件蛮有意思的,记录下实现这个组件的过程。
最终效果
实现原理
没用过这个组件的,建议去组件库试一下这个组件的用法,组件分为:左侧列表,中间两个按钮,右侧列表,因此可以划分为三个小组件,通过使用可以得出一个很重要的信息:
。
实现过程
目录结构
基本用法
<template>
<div class="app">
<div class="flex">
<div class="item" :style="{ height: 300 + 'px' }">
<my-transfer
:data="transferData"
@on-change="handleTransferChange"
:check-data="valueList"
@left-check-change="leftCheckChange"
@right-check-change="rightCheckChange"
filterable
></my-transfer>
</div>
</div>
</div>
</template>
<script>
import { MyTransfer } from "./components/MyTransfer";
export default {
components: { MyTransfer },
data() {
return {
transferData: [
{
key: 0,
label: "备选项0",
disabled: false,
},
{
key: 1,
label: "备选项1",
disabled: false,
},
{
key: 2,
label: "备选项2",
disabled: false,
},
{
key: 3,
label: "备选项3",
disabled: true,
},
{
key: 4,
label: "备选项4",
disabled: true,
},
{
key: 5,
label: "备选项5",
disabled: false,
},
{
key: 6,
label: "备选项6",
disabled: true,
},
{
key: 7,
label: "备选项7",
disabled: true,
},
{
key: 8,
label: "备选项8",
disabled: false,
},
{
key: 9,
label: "备选项9",
disabled: false,
},
{
key: 10,
label: "备选项10",
disabled: false,
},
{
key: 11,
label: "备选项11",
disabled: false,
},
{
key: 12,
label: "备选项12",
disabled: false,
},
{
key: 13,
label: "备选项13",
disabled: false,
},
{
key: 14,
label: "备选项14",
disabled: false,
},
{
key: 15,
label: "备选项15",
disabled: false,
},
],
//初始右侧选中数据key集合
valueList: [1, 2, 5, 6],
};
},
// 穿梭框变化
handleTransferChange(list) {
console.log("change", list);
},
// 左边选中变化
leftCheckChange(checkData) {
console.log("left", checkData);
},
// 右边选中变化
rightCheckChange(checkData) {
console.log("right", checkData);
},
},
};
</script>
<style lang="less">
.app {
padding: 20px;
.flex {
display: flex;
.item {
margin: 0 20px;
}
}
}
</style>
MyTransfer.vue暴露给用户使用
<template>
<div class="my-transform">
<!-- 左侧 -->
<div class="left-origin-list">
<!-- 存在搜索 -->
<my-transfer-list
:listData="searchData"
@select-change="(payload) => handleSelectChange(payload, 'right')"
ref="leftSearchList"
@search="handleSearch"
:filterable="filterable"
v-if="isSearch"
></my-transfer-list>
<!-- 常规 -->
<my-transfer-list
v-else
:listData="leftListData"
@select-change="(payload) => handleSelectChange(payload, 'right')"
ref="leftList"
@search="handleSearch"
:filterable="filterable"
></my-transfer-list>
</div>
<!-- 中间 -->
<div class="center-operate-btn">
<center-operate-btn-group
:left-disabled="leftDisabled"
:right-disabled="rightDisabled"
@btn-click="handleBtnClick"
></center-operate-btn-group>
</div>
<!-- 右侧 -->
<div class="right-filter-list">
<my-transfer-list
:listData="rightListDta"
@select-change="(payload) => handleSelectChange(payload, 'left')"
ref="rightList"
></my-transfer-list>
</div>
</div>
</template>
<script>
import MyTransferList from "./MyTransferList.vue";
import CenterOperateBtnGroup from "./CenterOperateBtnGroup.vue";
export default {
components: {
MyTransferList,
CenterOperateBtnGroup,
},
props: {
// 穿梭框数据
data: {
type: Array,
default() {
return [];
},
},
// 选中数据
checkData: {
type: Array,
default() {
return [];
},
},
// 是否可搜索
filterable: {
type: Boolean,
default: false,
},
},
data() {
return {
// 原始数据
originList: this.data,
// 右侧数据
rightListDta: [],
// 向右禁用
leftDisabled: true,
// 向左禁用
rightDisabled: true,
// 左边选中数据
leftCheckData: [],
// 右边选中数据
rightCheckData: [],
// 搜索关键词
searchValue: "",
// 搜索数据
searchData: [],
};
},
mounted() {
// 初始化右侧选中数据
this.filterRightListData();
},
computed: {
// 左侧列表数据
leftListData: {
get() {
return this.originList.filter((orginItem) => {
return !this.rightListDta.find(
(rightListItem) => rightListItem.key == orginItem.key
);
});
},
},
// 是否搜索
isSearch() {
return this.searchValue.trim().length > 0;
},
},
methods: {
// 初始化右侧数据
filterRightListData() {
this.rightListDta = this.originList.filter((orginItem) => {
return this.checkData.find(
(checkDataItemKey) =>
checkDataItemKey == orginItem.key && !orginItem.disabled
);
});
},
// 选中项变化(根据选中项长度决定是否禁用按钮)
handleSelectChange({ status, checkedData }, pos) {
if (pos == "left") {
this.leftDisabled = !status;
this.leftCheckData = checkedData;
this.$emit("right-check-change", checkedData);
} else {
this.rightDisabled = !status;
this.rightCheckData = checkedData;
this.$emit("left-check-change", checkedData);
}
},
// 中间两个按钮点击
handleBtnClick(type) {
switch (type) {
case "left":
this.rightListDta = this.rightListDta.filter((rightListItem) => {
return !this.leftCheckData.find(
(leftCheckDataItem) => rightListItem.key == leftCheckDataItem.key
);
});
this.$refs.rightList.clearCheckData();
break;
case "right":
this.rightListDta = [...this.rightListDta, ...this.rightCheckData];
this.$refs.leftList?.clearCheckData();
this.$emit("on-change", this.rightListDta);
// 存在搜索关键词的时候,重新计算左侧筛选数据
if (this.searchValue) {
this.searchData = this.searchData.filter((searchDataItem) => {
return !this.rightCheckData.find(
(checkDtaItem) => searchDataItem.key == checkDtaItem.key
);
});
this.$refs.leftSearchList?.clearCheckData();
}
break;
}
},
// 搜索
handleSearch(value) {
this.searchValue = value;
this.searchData = this.leftListData.filter(
(item) => item.label.indexOf(value) != -1
);
},
},
};
</script>
<style lang="less">
.my-transform {
display: flex;
align-items: center;
}
</style>
MyTransferList.vue 左右侧列表组件
<template>
<div class="my-transfer-list">
<!-- 头部 -->
<div class="head-bar">
<div class="left-check-all">
<check-box
@change="handleSelectAll"
:checked="
checkedData.length == excludeDisbledListDataLength &&
checkedData.length > 0
"
:disabled="listData.every((item) => item.disabled)"
>列表</check-box
>
</div>
<div class="right-num-count">
<span class="num-count-span">{{ checkedData.length }}</span>
<span class="num-count-span">/</span>
<span class="num-count-span">{{ excludeDisbledListDataLength }}</span>
</div>
</div>
<!-- 可搜索 -->
<div class="search-input" v-if="filterable">
<input type="text" placeholder="请输入搜索内容" @input="handleSearch" />
</div>
<!-- 主体列表 -->
<ul class="transfer-list-body" v-if="listData.length">
<li
:class="['transfer-list-item', item.disabled ? 'disabled-item' : '']"
v-for="item in listData"
:key="item.key"
>
<check-box
:disabled="item.disabled"
@change="(checked) => handlleListImteChange(checked, item)"
:checked="
checkedData.find((data) => data == item) && !item.disabled
? true
: false
"
>{{ item.label }}</check-box
>
</li>
</ul>
<!-- 空状态 -->
<div class="transfer-empty" v-else>暂无数据</div>
</div>
</template>
<script>
import CheckBox from "./CheckBox.vue";
export default {
components: { CheckBox },
props: {
// 列表数据
listData: {
type: Array,
default() {
return [];
},
},
// 是否可搜索
filterable: {
type: Boolean,
default: false,
},
},
data() {
return {
// 选中数据
checkedData: [],
};
},
computed: {
// 排除禁用项后的长度
excludeDisbledListDataLength() {
return this.listData.filter((item) => !item.disabled).length;
},
},
methods: {
// 单个itemcheckbox状态变更
handlleListImteChange(checked, data) {
checked
? this.checkedData.push(data)
: (this.checkedData = this.checkedData.filter(
(item) => item.key != data.key
));
// 发布选中项变化事件
this.$emit("select-change", {
status: this.checkedData.length > 0,
checkedData: this.checkedData,
});
},
// 全选或者不全选
handleSelectAll(checked) {
checked
? (this.checkedData = this.listData.filter((item) => !item.disabled))
: (this.checkedData = []);
// 发布选中项变化事件
this.$emit("select-change", {
status: this.checkedData.length > 0,
checkedData: this.checkedData,
});
},
// 清空checkdata当点击移走按钮时
clearCheckData() {
this.checkedData = [];
// 告知外部选中项变化
this.$emit("select-change", {
status: this.checkedData.length > 0,
checkedData: this.checkedData,
});
},
// 执行搜索
handleSearch(e) {
this.$emit("search", e.target.value);
},
},
};
</script>
<style lang="less">
.my-transfer-list {
border: 1px solid #ebeef5;
border-radius: 4px;
overflow: hidden;
background: #fff;
vertical-align: middle;
width: 200px;
max-height: 100%;
box-sizing: border-box;
position: relative;
color: #afb0b4;
.head-bar {
display: flex;
justify-content: space-between;
height: 40px;
line-height: 40px;
background: #f5f7fa;
border-bottom: 1px solid #ebeef5;
box-sizing: border-box;
color: #000;
padding: 0 15px;
.num-count-span {
font-size: 12px;
color: #909399;
}
}
.search-input {
padding: 0 15px;
margin: 15px 0;
input {
height: 32px;
width: 100%;
font-size: 12px;
display: inline-block;
box-sizing: border-box;
border-radius: 16px;
padding-right: 10px;
padding-left: 30px;
border: 1px solid #dcdfe6;
&:focus {
outline: none;
border-color: #409eff;
}
}
}
.transfer-list-body,
.transfer-empty {
height: 246px;
overflow: auto;
}
.transfer-empty {
text-align: center;
line-height: 35px;
}
.transfer-list-item {
padding: 0 15px;
color: #606266;
line-height: 30px;
}
.transfer-list-item.disabled-item {
cursor: not-allowed;
}
}
</style>
CheckBox.vue多选框组件
<template>
<label class="check-box-label">
<input
type="checkbox"
:disabled="disabled"
:checked="checked"
@change="$emit('change', $event.target.checked)"
/>
<slot></slot>
</label>
</template>
<script>
export default {
props: {
disabled: {
type: Boolean,
default: false,
},
checked: {
type: Boolean,
default: false,
},
},
};
</script>
<style lang="less">
.check-box-label {
input {
margin-right: 5px;
}
}
</style>
CenterOperateBtnGroup.vue中间两个操作按钮组件
<template>
<div class="operate-group">
<button
class="btn btn-left"
:disabled="leftDisabled"
@click="handBtnClick('left')"
>
< 到左边
</button>
<button
class="btn btn-right"
:disabled="rightDisabled"
@click="handBtnClick('right')"
>
到右边 >
</button>
</div>
</template>
<script>
export default {
props: {
leftDisabled: {
type: Boolean,
default: false,
},
rightDisabled: {
type: Boolean,
default: false,
},
},
methods: {
handBtnClick(type) {
this.$emit("btn-click", type);
},
},
};
</script>
<style lang="less">
.operate-group {
padding: 0 30px;
.btn {
display: inline-block;
line-height: 1;
cursor: pointer;
border: 1px solid #409eff;
color: #fff;
background-color: #409eff;
text-align: center;
box-sizing: border-box;
outline: none;
margin: 0;
transition: 0.1s;
font-weight: 500;
padding: 12px 20px;
font-size: 14px;
border-radius: 4px;
}
.btn[disabled] {
color: #fff;
background-color: #a0cfff;
border-color: #a0cfff;
cursor: not-allowed;
}
.btn-right {
margin-left: 20px;
}
}
</style>
index.js按需导出组件
import MyTransfer from "./MyTransfer.vue";
export { MyTransfer };
总结
上面搜索只做了左侧的,右侧的思路和左侧思路是一样的,需要注意写这个组件的时候,把数据隔离清楚就好了。