效果图
组件实现 -目录结构
Transfer.vue实现
<template>
<el-card :body-style="{ minHeight: '350px' }">
<el-input
v-show="filterable"
v-model="filterName"
:placeholder="filterPlaceholder"
clearable
@input="handleInput"
>
<i slot="prefix" class="el-input__icon el-icon-search"></i>
</el-input>
<div class="transfer-title">
<el-checkbox
class="transfer-title-check"
:indeterminate="isIndeterminate"
:disabled="!data.length"
v-model="checkAll"
@change="handleCheckAllChange"
>{{ checkAllTitle }}</el-checkbox
>
<span class="check-num">
{{ checkList.length + "/" + checkData.length }}</span
>
</div>
<main>
<el-checkbox-group v-model="checkList">
<div
class="transfer-checkbox-item"
v-for="item in checkData"
:key="item[props.key]"
>
<el-checkbox
class="transfer-checkbox"
:disabled="item[props.disabled]"
:label="item[props.label]"
@change="handleCheckChange"
>
</el-checkbox>
</div>
<div class="empty" v-show="!checkData.length">
{{ beSearched ? "无匹配数据" : "暂无数据" }}
</div>
</el-checkbox-group>
</main>
<footer>
<slot name="footer">
<el-button
class="footer-button"
v-for="item in buttonList"
:key="item.key"
:disabled="!checkList.length"
:type="item.type"
@click="handleConfirm(item.key)"
>{{ item.title }}</el-button
>
</slot>
</footer>
</el-card>
</template>
<script>
export default {
name: "transfer-view",
props: {
data: {
type: Array,
default: () => [],
},
// 可自定义传入的数据结构
props: {
type: Object,
default: () => {
return { key: "label_name", label: "label_name", disabled: "disabled" };
},
},
filterPlaceholder: {
type: String,
default: "请输入搜索名称",
},
checkAllTitle: {
type: String,
default: "列表名称",
},
// 实际对应 列表类型 每个列表一个特有的类型与按钮关联
buttonType: {
type: String,
default: "origin",
},
// 按钮Map对象 自定义插槽按钮可不传
buttonMap: {
type: Object,
default: () => {},
},
// 是否可搜索
filterable: {
type: Boolean,
default: true,
},
// 定义自己的搜索逻辑 同el
filterMethod: {
type: Function,
},
},
data() {
return {
filterName: "",
checkList: [],
checkAll: false,
isIndeterminate: false,
beSearched: false,
searchList: [],
};
},
computed: {
buttonList() {
return this.buttonMap[this.buttonType];
},
checkData() {
return this.beSearched ? this.searchList : this.data;
},
},
methods: {
/**
* 搜索框搜索
*/
handleInput(value) {
if (value === "") {
this.beSearched = false;
return;
}
this.beSearched = true;
let label = this.props.label;
let data = this.data;
this.searchList = data.filter((item) => {
if (typeof this.filterMethod === "function") {
return this.filterMethod(this.filterName, item);
}
return item[label].includes(value);
});
},
/**
* 全选
*/
handleCheckAllChange(flag) {
if (flag) {
let label = this.props.label;
this.checkList = this.data.map((i) => i[label]);
this.checkAll = true;
} else {
this.checkList = [];
this.checkAll = false;
}
this.isIndeterminate = false;
},
/**
* 单选
*/
handleCheckChange(flag) {
if (this.checkList.length === this.data.length && flag) {
this.isIndeterminate = false;
this.checkAll = true;
} else {
this.checkAll = false;
}
if (this.data.length > 1) {
this.isIndeterminate = true;
}
},
/**
* 按钮点击
*/
handleConfirm(key) {
let checkList = this.checkList;
let label = this.props.label;
let data = this.data.filter((item) => checkList.includes(item[label]));
let newData = this.data.filter(
(item) => !checkList.includes(item[label])
);
this.$emit("confirm", { key, data });
this.$emit("update:data", newData);
if (this.filterable) {
this.resetSearchData();
}
this.resetData();
},
/**
* 重置为原始状态
*/
resetData() {
this.checkAll = false;
this.isIndeterminate = false;
this.checkList = [];
},
/**
* 重置搜索状态
*/
resetSearchData() {
if (this.searchList.length) {
let label = this.props.label;
let checkList = this.checkList;
let searchList = this.searchList;
this.searchList = searchList.filter(
(i) => !checkList.includes(i[label])
);
}
},
},
};
</script>
<style lang="scss" scoped>
.transfer-title {
background: #f2f6fc;
width: 100%;
height: 50px;
padding: 16px;
box-sizing: border-box;
margin-top: 12px;
display: flex;
justify-content: space-between;
align-items: center;
.transfer-title-check {
display: flex;
}
.check-num {
color: #909399;
font-size: 12px;
}
}
main {
height: 250px;
overflow-y: auto;
position: relative;
border-bottom: 1px solid #e4e7ed;
.transfer-checkbox-item {
.transfer-checkbox {
width: 100%;
box-sizing: border-box;
padding: 12px;
display: flex;
justify-content: flex-start;
align-items: center;
border-top: 1px solid #e4e7ed;
}
&:nth-child(1) {
.transfer-checkbox {
border: none;
}
}
}
.empty {
width: 100%;
height: 20px;
text-align: center;
color: #ccc;
font-size: 13px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: 2;
}
}
footer {
.footer-button {
margin-top: 12px;
}
}
</style>
config.js
export const buttonDefaultMap = {
origin: [
{ key: "must", type: "primary", title: "设为列表1" },
{ key: "choose", type: "success", title: "设为列表2" },
],
must: [
{ key: "origin", type: "warning", title: "恢复初始" },
{ key: "choose", type: "success", title: "设为列表2" },
],
choose: [
{ key: "origin", type: "warning", title: "恢复初始" },
{ key: "must", type: "success", title: "设为列表1" },
],
};
export const buttonMap = {
origin: [
{ key: "must", type: "primary", title: "设为列表1" },
{ key: "choose", type: "success", title: "设为列表2" },
{ key: "replace", type: "info", title: "设为列表3" },
],
must: [
{ key: "origin", type: "warning", title: "恢复初始" },
{ key: "choose", type: "success", title: "设为列表2" },
{ key: "replace", type: "info", title: "设为列表3" },
],
choose: [
{ key: "origin", type: "warning", title: "恢复初始" },
{ key: "must", type: "success", title: "设为列表1" },
{ key: "replace", type: "info", title: "设为列表3" },
],
replace: [
{ key: "origin", type: "warning", title: "恢复初始" },
{ key: "must", type: "success", title: "设为列表1" },
{ key: "choose", type: "info", title: "设为列表2" },
],
};
feeTemplate.vue
<template>
<div>
<el-row :gutter="20" type="flex" justify="space-around">
<el-col>
<Transfer
ref="originRef"
:data.sync="originTable"
:buttonMap="buttonList"
@confirm="handleConfirm"
:filterMethod="filterMethod"
/>
</el-col>
<el-col>
<Transfer
ref="mustRef"
:data.sync="mustTable"
:buttonMap="buttonList"
checkAllTitle="列表1"
buttonType="must"
@confirm="handleConfirm"
/>
</el-col>
<el-col>
<Transfer
ref="chooseRef"
:data.sync="chooseTable"
:buttonMap="buttonList"
checkAllTitle="列表2"
buttonType="choose"
@confirm="handleConfirm"
/>
</el-col>
<el-col>
<Transfer
ref="replaceRef"
:data.sync="replaceTable"
:buttonMap="buttonList"
checkAllTitle="列表3"
buttonType="replace"
@confirm="handleConfirm"
/>
</el-col>
</el-row>
</div>
</template>
<script>
import Transfer from "@/components/Transfer";
import { buttonMap } from "./config.js";
export default {
components: {
Transfer,
},
props: {
openType: {
type: String,
default: "",
},
},
data() {
return {
originTable: [
{
label_name: "备选1",
},
{
label_name: "备选2",
},
{
label_name: "备选3",
},
{
label_name: "备选4",
},
{
label_name: "备选5",
},
{
label_name: "备选6",
},
{
label_name: "备选7",
},
{
label_name: "备选8",
},
],
mustTable: [],
chooseTable: [],
replaceTable: [],
buttonList: buttonMap,
//buttonList:buttonMap,
filterMethod(query,item){
return item.label_name.includes(query);
}
};
},
methods: {
/**
* 按钮点击
*/
handleConfirm({ key, data }) {
switch (key) {
case "origin":
this.originTable = [...this.originTable, ...data];
break;
case "must":
this.mustTable = [...this.mustTable, ...data];
break;
case "choose":
this.chooseTable = [...this.chooseTable, ...data];
break;
case "replace":
this.replaceTable = [...this.replaceTable, ...data];
break;
}
},
},
};
</script>
<style scoped></style>
- 高度自定义,主要看配置 config.js 对应的操作 支持多个穿梭框 ,部分api同elementui ,降低心智负担
vue3也是同样的原理 ,只不过 部分api改下 就行