使用详解
Element Plus 的 el-transfer
组件是一个强大的穿梭框组件,常用于在两个集合之间进行数据转移,如权限分配、数据选择等场景。下面我将详细介绍其用法并提供一个完整示例。
核心特性与用法
基本属性
-
v-model
:绑定右侧列表的值(key数组) -
data
:组件数据源,需包含key
和label
属性 -
props
:配置数据源的字段别名 -
filterable
:是否启用搜索功能 -
titles
:自定义左右两侧标题 -
button-texts
:自定义按钮文本 -
left-default-checked
/right-default-checked
:设置默认选中项
关键事件
-
change
:当右侧列表变化时触发 -
left-check-change
:左侧选中项变化时触发 -
right-check-change
:右侧选中项变化时触发
插槽
-
默认插槽:自定义列表项内容
-
left-footer
/right-footer
:自定义左右列表底部内容
filter-method 用法详解
1. 基本概念
filter-method
是 Element Plus Transfer 组件的一个属性,用于自定义穿梭框的搜索过滤逻辑。它接受一个函数,该函数有两个参数:
-
query
: 用户输入的搜索关键词 -
item
: 当前遍历的数据项
函数应返回布尔值:
-
true
: 保留该项 -
false
: 过滤掉该项
default 插槽用法详解
1. default 插槽基础用法
default 插槽允许完全自定义每个列表项的显示内容:
html
复制
下载
运行
<el-transfer> <template #default="{ option }"> <!-- 自定义内容 --> <div>{{ option.label }}</div> </template> </el-transfer>
使用示例
MaterialApplyDialog.vue
<script setup lang="ts" name="MaterialApplyDialog">
import { ref } from "vue";
// 模态框显示标识
const dialogVisible = ref(false);
interface Option {
key: number;
label: string;
disabled: boolean;
}
const generateData = () => {
const data: Option[] = [];
for (let i = 1; i <= 500; i++) {
data.push({
key: i,
label: `Option${i}`,
disabled: i % 4 === 0
});
}
return data;
};
const data = ref<Option[]>(generateData());
const value = ref([]);
// 打开模态框
const openDialog = async () => {
dialogVisible.value = true;
};
// 隐藏模态框
const closeDialog = () => {
dialogVisible.value = false;
};
// 过滤方法
const filterMethod = (keywords: string, item: Option) => {
// 默认的过滤方法,使用关键字与选项对象的label属性进行匹配
// return item.label.toLowerCase().includes(keywords.toLowerCase());
// 自定义过滤方法,搜索以关键字结尾的选项,区分大小写
return item.label.endsWith(keywords);
};
// 确定
const onConfirmClick = async () => {
// 关闭模态框
closeDialog();
};
defineExpose({
openDialog
});
</script>
<template>
<el-dialog
class="receive-dialog"
title="穿梭框应用示例"
width="1200px"
top="0vh"
style="border-radius: 10px"
v-model="dialogVisible"
:close-on-press-escape="true"
:close-on-click-modal="false"
:show-close="true"
@close="closeDialog">
<template #default>
<el-container class="container">
<el-transfer
class="custom-transfer"
v-model="value"
:data="data"
:titles="[`待选内容`, `已选内容`]"
:button-texts="[`移除`, `添加`]"
:filterable="true"
:filter-placeholder="`请输入关键字搜索以其结尾的内容`"
:filter-method="filterMethod" />
</el-container>
</template>
<template #footer>
<div>
<el-button class="dialog-btn" type="primary" @click="onConfirmClick">确定</el-button>
<el-button class="dialog-btn" @click="closeDialog">取消</el-button>
</div>
</template>
</el-dialog>
</template>
<style scoped lang="scss">
/* 穿梭框容器样式 */
.container {
width: 100%;
height: 80vh;
}
/* 穿梭框样式 - 已通过 Element Plus v2.7.4 测试 */
.custom-transfer {
flex: 1;
display: flex;
justify-content: space-between; /* 首尾元素贴边,中间元素等距 */
/* 面板样式 */
:deep(.el-transfer-panel) {
/**
如果所在容器没有设置justify-content: space-between; 又需要穿梭框占满容器,
需通过浏览器调试工具,获取到穿梭框容器宽度为1168px,按钮区域占用200px,则面板宽度应该设置为 (1168-200)/2 = 484px,
如果所在容器已经设置justify-content: space-between; 又需要穿梭框占满容器,面板宽度可以不用精细计算
*/
width: 450px;
height: 100% !important;
display: flex;
flex-direction: column;
}
/* 面板头部样式 */
:deep(.el-transfer-panel__header) {
background-color: #d3e2f1;
display: flex;
}
/* 面板主体 */
:deep(.el-transfer-panel__body) {
flex: 1;
display: flex;
flex-direction: column;
}
/* 列表区域 - 关键滚动区域 */
:deep(.el-transfer-panel__list) {
flex: 1;
overflow-y: auto;
}
/* 按钮区域样式 */
:deep(.el-transfer__buttons) {
display: flex;
flex-direction: row; /* 水平排列 */
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
padding: 0 10px;
}
/* 按钮样式 */
:deep(.el-transfer__buttons .el-button) {
margin: 0 10px;
width: 70px;
padding: 8px 15px;
/* 蓝色渐变背景 */
// background: linear-gradient(135deg, #409eff 0%, #3375b9 100%);
// border: none;
// color: white;
// box-shadow: 0 2px 5px rgba(64, 158, 255, 0.3);
}
/* 按钮方向指示 */
:deep(.el-transfer__buttons .el-button:first-child::after) {
content: "<=";
}
:deep(.el-transfer__buttons .el-button:last-child::after) {
content: "=>";
}
/* 按钮文字提示 */
:deep(.el-transfer__buttons .el-button span) {
display: none; /* 隐藏按钮文字 */
}
}
.dialog-btn {
width: 100px;
}
</style>
应用效果:
项目实例
MaterialApplyDialog.vue
特点:
1、自定义过滤方法 filter-method
2、数据项属性别名 props
3、插槽 default
4、插槽 left-footer
5、输入框控制数字输入
6、遍历数组元素检查数据合法性,聚焦元素,全选元素内容
7、结合实际情况调整样式
<script setup lang="ts" name="ReagentApplyDialog">
import { branchWarehouseApplyGenerateForReagentService } from "@/api/branchWarehouse";
import { useReagentOptionList } from "@/hooks/useReagentOptionList";
import { formatToNumber } from "@/utils/formatter";
import { Search } from "@element-plus/icons-vue";
import type { TransferInstance } from "element-plus";
import { ElMessage } from "element-plus";
import { debounce } from "lodash-es";
import { nextTick, ref } from "vue";
import type { IReagentOption } from "../types";
// 模态框显示标识
const dialogVisible = ref(false);
// 试剂选项列表,组合式函数 hook
const { reagentOptionList, fetchReagentOptionList, resetReagentOptionData } = useReagentOptionList();
// 搜索内容
const searchVal = ref("");
// 已选试剂id集合
const selectedOptionIds = ref<number[]>([]);
// 穿梭框实例对象
const transferRef = ref<TransferInstance | null>(null);
// 打开模态框
const openDialog = async () => {
initPageData();
await nextTick();
dialogVisible.value = true;
};
// 隐藏模态框
const closeDialog = () => {
dialogVisible.value = false;
initPageData();
};
// 过滤方法
const filterMethod = (keywords: string, item: IReagentOption) => {
// 默认的过滤方法,使用关键字与选项对象的label属性进行匹配
return item.reagentName.toLowerCase().includes(keywords.toLowerCase());
// 自定义过滤方法,搜索以关键字结尾的选项,区分大小写
// return item.reagentName.endsWith(keywords);
};
// 确定
const onConfirmClick = async () => {
// 检查数据合法性
if (!checkValid()) {
return;
}
let selectedOptions = selectedOptionIds.value
.map((id) => reagentOptionList.value.find((option) => option.id === id))
.filter((option): option is IReagentOption => !!option); // 过滤掉 undefined 并缩小类型
// 生成申领单
if (selectedOptions.length > 0) {
await branchWarehouseApplyGenerateForReagentService(selectedOptions);
}
// 关闭模态框
closeDialog();
};
// 检查数据合法性
const checkValid = () => {
if (selectedOptionIds.value.length === 0) {
ElMessage.warning("请选择试剂");
return false;
}
// 循环遍历已选试剂
for (let i = 0; i < selectedOptionIds.value.length; i++) {
let option = reagentOptionList.value.find((option) => option.id === selectedOptionIds.value[i]);
if ((option?.applyAmount ?? 0) === 0 || (option?.applyAmount ?? 0) > option?.amount!) {
if ((option?.applyAmount ?? 0) === 0) ElMessage.warning("请输入申领数量!");
else ElMessage.error("申领数量不能大于库存数量!");
document.getElementById(`input-apply-amount-${option?.id}`)?.focus();
(document.getElementById(`input-apply-amount-${option?.id}`) as HTMLInputElement)?.select();
return false;
}
}
return true;
};
// 搜索,加装防抖器,防抖处理(leading: true,立即执行、maxWait: 3000,3秒内至少执行一次)
const onSearchClick = debounce(
async () => {
// 清空已选试剂id集合
selectedOptionIds.value = [];
// 发送网络请求,获取试剂选项列表
await fetchReagentOptionList(searchVal.value);
},
1000,
{ leading: true, trailing: true, maxWait: 3000 }
);
// 页面初始化
const initPageData = () => {
// 清空左侧过滤框
transferRef.value?.clearQuery("left");
// 清空右侧过滤框
transferRef.value?.clearQuery("right");
// 清空搜索框
searchVal.value = "";
// 清空已选试剂id集合
selectedOptionIds.value = [];
// 重置试剂选项数据
resetReagentOptionData();
};
defineExpose({
openDialog
});
</script>
<template>
<el-dialog
class="receive-dialog"
title="试剂耗材申领"
width="1200px"
top="0vh"
style="border-radius: 10px"
v-model="dialogVisible"
:close-on-press-escape="true"
:close-on-click-modal="false"
:show-close="true"
@close="closeDialog">
<template #default>
<el-container class="container">
<el-transfer
ref="transferRef"
class="custom-transfer"
v-model="selectedOptionIds"
:data="reagentOptionList"
:props="{
key: `id`,
label: `reagentName`
}"
:titles="[`待选试剂`, `已选试剂`]"
:button-texts="[`移除`, `添加`]"
:filterable="reagentOptionList.length > 12"
:filter-placeholder="`请输入关键字搜索试剂`"
:filter-method="filterMethod">
<!-- 自定义列表数据项的内容 -->
<template #default="{ option }">
<div class="transfer-list-option">
<div class="transfer-list-option-left">
<el-tag type="primary">{{ (option as IReagentOption).reagentName }}</el-tag>
<el-tag type="info" v-if="(option as IReagentOption).batchNo">{{
(option as IReagentOption).batchNo
}}</el-tag>
</div>
<div class="transfer-list-option-right">
<el-tag type="warning">{{ (option as IReagentOption).validityDate }}</el-tag>
<el-tag class="transfer-list-option-right-amount" type="success">{{
(option as IReagentOption).amount
}}</el-tag>
<el-input
v-if="selectedOptionIds.includes((option as IReagentOption).id)"
:id="`input-apply-amount-${(option as IReagentOption).id}`"
class="input-apply-amount"
style="width: 85px; text-align: center"
v-model="(option as IReagentOption).applyAmount"
placeholder="输入申领数量"
size="small"
clearable
@input="(option as IReagentOption).applyAmount = Number(formatToNumber($event, 0))" />
</div>
</div>
</template>
<!-- 自定义左侧列表底部的内容 -->
<template #left-footer>
<div class="transfer-left-footer">
<el-input v-model="searchVal" placeholder="请输入试剂名称" clearable @keydown.enter="onSearchClick">
<template #prepend>查找试剂:</template>
<template #append>
<el-button :icon="Search" @click="onSearchClick" />
</template>
</el-input>
</div>
</template>
<!-- 自定义右侧列表底部的内容 -->
<!-- <template #right-footer>
<div>
<span>可以自定义右侧列表底部的内容</span>
</div>
</template> -->
</el-transfer>
</el-container>
</template>
<template #footer>
<div>
<el-button class="dialog-btn" type="primary" @click="onConfirmClick">确定</el-button>
<el-button class="dialog-btn" @click="closeDialog">取消</el-button>
</div>
</template>
</el-dialog>
</template>
<style scoped lang="scss">
/* 穿梭框容器样式 */
.container {
width: 100%;
height: 80vh;
}
/* 穿梭框样式 - 已通过 Element Plus v2.7.4 测试 */
.custom-transfer {
flex: 1;
display: flex;
justify-content: space-between; /* 首尾元素贴边,中间元素等距 */
/* 面板样式 */
:deep(.el-transfer-panel) {
/**
如果所在容器没有设置justify-content: space-between; 又需要穿梭框占满容器,
需通过浏览器调试工具,获取到穿梭框容器宽度为1168px,按钮区域占用200px,则面板宽度应该设置为 (1168-200)/2 = 484px,
如果所在容器已经设置justify-content: space-between; 又需要穿梭框占满容器,面板宽度可以不用精细计算
*/
width: 550px;
height: 100% !important;
display: flex;
flex-direction: column;
}
/* 面板头部样式 */
:deep(.el-transfer-panel__header) {
background-color: #f5f7fa;
display: flex;
}
/* 面板主体 */
:deep(.el-transfer-panel__body) {
flex: 1;
display: flex;
flex-direction: column;
}
/* 列表区域 - 关键滚动区域 */
:deep(.el-transfer-panel__list) {
flex: 1;
overflow-y: auto;
}
/* 列表选项 */
:deep(.el-transfer-panel__item) {
flex: 1;
display: flex;
align-items: center; /* 垂直居中 */
margin: 0;
padding-right: 15px;
height: 32px;
}
/* 列表选项 - 最后一个选项 */
:deep(.el-transfer-panel__item:last-child) {
padding-right: 15px;
}
/* 按钮区域 */
:deep(.el-transfer__buttons) {
display: flex;
flex-direction: column-reverse; /* 垂直反序排列 */
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
padding: 0;
}
/* 按钮 */
:deep(.el-transfer__buttons .el-button) {
margin: 20px 10px;
width: 40px;
padding: 8px 15px;
/* 蓝色渐变背景 */
// background: linear-gradient(135deg, #409eff 0%, #3375b9 100%);
// border: none;
// color: white;
// box-shadow: 0 2px 5px rgba(64, 158, 255, 0.3);
}
/* 按钮文字 */
:deep(.el-transfer__buttons .el-button span) {
display: none; /* 隐藏按钮文字 */
}
/* 按钮内容 */
:deep(.el-transfer__buttons .el-button:first-child::after) {
content: "<="; /* 设置按钮显示内容为 <= */
}
:deep(.el-transfer__buttons .el-button:last-child::after) {
content: "=>"; /* 设置按钮显示内容为 => */
}
.transfer-list-option {
flex: 1;
display: flex;
flex-direction: row; /* 水平排列 */
align-items: center; /* 垂直居中 */
height: 32px;
&-left {
flex: 1;
display: flex;
justify-content: start; /* 贴左边 */
align-items: center; /* 垂直居中 */
max-width: 220px;
}
&-right {
flex: 1;
display: flex;
justify-content: end; /* 贴右边 */
align-items: center; /* 垂直居中 */
&-amount {
width: 40px;
}
.input-apply-amount {
width: 85px;
// 输入框的内容居中
& :deep(.el-input__inner) {
text-align: center;
}
}
}
.el-tag {
font-size: 14px;
}
}
.transfer-left-footer {
display: flex;
height: 100%;
}
}
.dialog-btn {
width: 100px;
}
</style>
应用效果: