上次客户过来讨论的时候,说起其旧系统很多字段选择是通过弹出表格选择记录的,希望沿袭这个使用习惯,否则客户对新系统开发可能不适应,问我如何在Vue3+ElementPlus前端中是否可以实现,我说你基于JQuery的都可以实现,那么Vue3上开发肯定没问题的,而且响应会更加丝滑的,于是我就琢磨做一个通用的案例,整合在我的SqlSugar开发框架的Vue3+ElementPlus前端中。既然要弄就弄个通用的自定义表格选择组件,以便在更多的场合下可以使用,通过动态配置表格字段和相关的属性即可显示和选择。
本篇例子结合Popover 弹出框和Input输入框实现用户记录的选择,以及结合Popover 弹出框和按钮选择实现菜单中多语言键值的选择两项功能实现进行介绍。
1、结合Popover 弹出框和Input输入框实现表格记录选择
在el-Popover的组件中,我们也都看到了他的一个弹出表格的简单案例,如下所示。

不过这个例子太过简单,参考下可以,但是和我们实现通用的数据记录查询以及表格内容可以变化等需求不符合,我们需要根据这样的模式,把Input输入和表格显示整合到里面去,这样可以在对应的表格上进行数据查询过滤,这样可以快速选择到记录。
我的大概需求如下:
使用 Vue 3 + Element Plus 实现的自定义组件示例,它可以用在不同的业务表中,要求通用化,功能上它集成了:
-
el-popover
弹出框,用于显示选择面板
-
el-input
输入框,点击触发弹出
-
el-table
表格控件,展示可选项,还可以对数据进行过滤查询
- 可点击表格行来选择记录并回填到输入框中
该组件适用于选择一个记录项(如客户、产品等)并回传给父组件。
你可以根据需要扩展以下功能:
- ✅ 支持远程搜索(使用
el-table
的 @filter-change
或增加 el-input
搜索框)
- ✅ 支持分页(结合
el-pagination
)
- ✅ 支持多选(表格加
type="selection"
,回传数组)
- ✅ 支持懒加载(点击时加载 options)
我们先来看看成品的效果,下面案例是我基于用户记录表进行选择的处理下效果。

单击选择用户的输入框,就会弹出对应的数据表格进行显示,我们可以在其中进行过滤查询,然后单击记录可以选中返回。

我们自定义组件名称命名为 TableSelector,那么它的使用代码如下所示。

<el-form-item label="选择用户名">
<TableSelector
v-model="selectedUser"
filter-placeholder="请输入人员编号/姓名/电话/住址等搜索"
:columns="[
{ prop: 'id', label: 'ID', width: 80 },
{ prop: 'name', label: '用户名' },
{ prop: 'fullname', label: '真实姓名' },
{ prop: 'mobilephone', label: '移动电话' },
{ prop: 'email', label: '邮箱' }
]"
:fetchData="loadUsers"
row-key="name"
placeholder="请选择用户"
/>
</el-form-item>

v-model绑定选中的记录对象,其中 columns 属性我们可以根据不同的业务表进行配置显示,而 fetchData 则是传入一个函数给它获取数据的,交给调用的父组件进行数据的过滤和处理即可。
其中的loadUsers的实现,我们根据不同的业务实现它的数据请求查询即可,如下是函数代码。

//加载用户,供选择表格处理
async function loadUsers(filter: string) {
const params = {
maxresultcount: 100,
skipcount: 0,
sorting: '',
filter: filter
};
const res = await user.GetAllByFilter(params);
return res.items;
}

这样我们就可以实现通用的处理,不同的业务记录显示,我们配置表格内容显示和获取数据的逻辑即可。
自定义组件的props定义和emits事件如下所示。

// Props
const props = defineProps<{
columns: ColumnDef[];
data?: any[];
modelValue?: any | any[]; // 当前选中值(单个对象或数组)
rowKey?: string; // 要显示的字段 key(如 name)
multiple?: boolean;
pagination?: {
page: number;
pageSize: number;
total: number;
onPageChange: (page: number) => void;
};
fetchData?: (filter: string) => Promise<any[]>; // 外部提供的数据加载函数
placeholder?: string;
filterPlaceholder?: string;
}>();
// Emits
const emit = defineEmits<{
(e: 'update:modelValue', val: any | any[]): void;
}>();

我们为了剥离具体表格字段的处理,因此配置了动态的columns参数,因此在自定义组件显示表格的时候,根据配置的信息进行显示字段即可,如下所示。

<template>
<div>
<el-popover
v-model:visible="visible"
placement="bottom-start"
width="600"
trigger="focus"
:teleported="false"
>
<div style="margin-bottom: 8px">
<el-input
v-model="filterText"
:placeholder="filterPlaceholder || '输入关键字筛选'"
clearable
/>
</div>
<!-- 表格区域 -->
<el-table
v-loading="loading"
:data="tableData"
:height="300"
@selection-change="handleSelectionChange"
@row-click="handleRowClick"
:row-key="rowKey"
:highlight-current-row="isSingleSelect"
style="width: 100%"
>
<el-table-column v-if="multiple" type="selection" width="40" />
<template v-for="col in columns" :key="col.prop">
<el-table-column
:prop="col.prop"
:label="col.label"
:width="col.width"
/>
</template>
</el-table>

在输入框的处理上,我们设置它的显示内容和清空按钮等处理,如下所示。

<!-- 弹出内容插槽的触发器 -->
<template #reference>
<el-input
v-model="displayText"
readonly
:placeholder="placeholder || '请选择...'"
suffix-icon="el-icon-arrow-down"
@click.stop="visible = true"
:clearable="true"
>
<template #suffix>
<el-icon
v-if="displayText"
class="cursor-pointer"
@click.stop="clearSelection"
>
<CircleClose />
</el-icon> </template
></el-input>
</template>

而且输入框里面的显示displaytext是根据选中记录的属性进行计算显示的,如下代码所示。

const displayText = computed(() => {
const data = props.modelValue;
if (!data) return '';
if (Array.isArray(data)) {
return data.map(v => v?.[props.rowKey || 'id']).join(', ');
}
return data?.[props.rowKey || 'id'] || '';
});

通过对显示和过滤的属性进行监控,我们可以对数据的加载逻辑进行处理,从而实现数据的动态展示和过滤。

const loadData = async () => {
loading.value = true;
try {
if (props.fetchData) {
tableData.value = await props.fetchData(filterText.value);
} else if (props.data) {
tableData.value = props.data;
}
// 恢复选中状态(仅多选模式)
if (props.multiple && Array.isArray(props.modelValue)) {
selectedRows.value = props.modelValue;
}
} finally {
loading.value = false;
}
};
watch(
() => visible.value,
async val => {
if (val) {
loadData();
}
}
);
// 输入过滤条件时防抖加载
watch(
filterText,
debounce(() => {
if (visible.value) loadData();
}, 300)
);

我们也可以使用它实现多选记录的处理,多选提供复选框选择多个记录,并通过确认按钮返回,如下所示。

2、结合Popover 弹出框和按钮选择实现菜单中多语言键值的选择
除了上面通过输入框的方式进行弹出表格数据供用户选择外,有时候我们想在不影响常规输入框的情况下,提供一个额外的按钮,触发弹出选择记录的对话框。
如下面案例,我需要把多语言的键值列出来供我们定义菜单的或者一些界面元素的名称,界面效果如下所示。

选中后,我们就可以及时的显示多语言的键和对应的语言内容了。

上面的自定义控件的使用代码如下所示。

<el-form-item label="显示名称" prop="name" class="flex flex-row">
<el-input v-model="editForm.name" style="width: 180px" />
<TableSelectorButton v-model="selectedLocal"
filter-placeholder="请输入键名搜索"
:columns="[
{ prop: 'key', label: '语言键名' },
{ prop: 'text', label: '国际化名称' }
]"
:fetchData="loadLocals"
row-key="key"
@update:model-value="updateLocaleName"
></TableSelectorButton>
</el-form-item>

通过对事件
@update:model-value="updateLocaleName"
进行跟踪,我们就可以在内容变化的时候,及时通知其他控件进行内容更新了。
上面的多语言处理和选择用户的输入框控件很相似,只是为了可以不影响输入框的可编辑性,我们通过按钮来选择在某些场合可能更为合理,因此扩展了这个选择组件,他们的差异只是把输入框换为按钮的处理,如下代码。

<!-- 弹出内容插槽的触发器 -->
<template #reference>
<el-button
:type="props.buttonType || 'primary'"
class="m-2"
round
plain
>
{{ props.buttonText || '...' }}
</el-button>
</template>

这样我们定义的属性中,需要增加按钮的类型Type和按钮的名称来自定义即可,其他属性不变。

// Props
const props = defineProps({
columns: {
type: Array as PropType<ColumnDef[]>,
required: true
},
data: {
type: Array as PropType<any[]>,
required: false
},
modelValue: {
type: [Object, Array] as PropType<any | any[]>, // 可以是对象或数组
required: false
},
rowKey: {
type: String,
required: false
},
multiple: {
type: Boolean,
required: false
},
pagination: {
type: Object as PropType<{
page: number;
pageSize: number;
total: number;
onPageChange: (page: number) => void;
}>,
required: false
},
fetchData: {
type: Function as PropType<(filter: string) => Promise<any[]>>,
required: false
},
buttonText: {
type: String,
required: false
},
buttonType: {
type: String as PropType<'default' | 'text' | 'success' | 'primary' | 'warning' | 'info' | 'danger'>,
required: false },
filterPlaceholder: {
type: String,
required: false
}
});

以上两个例子,都是基于Popover 弹出框进行的自定义控件封装,主要就是为用户选择其他表或记录更加友好 ,从而快速实现数据的查询和选择处理的过程。
熟悉对前端自定义控件的封装,可以根据我们实际业务的需求进行界面逻辑的抽离和重用,实现统一化的界面效果处理。