优化表单交互:在 el-select 组件中嵌入表格显示选项

介绍了一种通过 el-select 插槽实现表格样式数据展示的方案,可更直观地辅助用户选择。支持列配置、行数据绑定及自定义搜索,简洁高效,适用于复杂选择场景。完整代码见GitHub 仓库

背景

在进行业务开发选择订单时,如果单纯的根据单号是无法编写是哪一条订单的,这个时候就可以通过表格的方式去展示这条订单的其他信息,辅助用户分辨出订单,不用再去查,更快速、更友好。

效果如下:

el-select-table-click-demo

实现

环境与依赖

  • node 22
  • vue 3
  • element-plus

思路

原理很简单,利用插槽。

虽然通过 el-table 可以实现表格效果,但对这种简单需求来说过于臃肿。而直接使用原生 table 标签结构实现太过于繁琐,不便于实现遍历 el-option

1
2
3
4
5
6
7
<el-select>
<!-- 表头,遍历 -->
<!-- 表体,遍历 -->
<el-option>
<!-- 行数据,遍历 -->
</el-option>
</el-select>

因此,我采用列表结构实现,结合 CSS 美化出表格效果。具体做法是遍历「列配置」生成表头,再遍历「options」生成行数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<el-select 
ref="inputRef"
v-model="selectValue"
clearable
filterable
:placeholder="placeholder"
:disabled="disabled"
:filter-method="filterMethod"
@change="handleChange"
>
<!-- 表头 -->
<ul class="select-ul">
<li v-for="column in columnConfig" :key="column.label">{{ column.label }}</li>
</ul>
<el-option v-for="item in showOptions" :key="item[keyNameCom]" :value="item[valueName]">
<ul class="select-ul select-ul-data">
<li v-for="column in columnConfig" :key="column.label">{{ item[column.prop] }}</li>
</ul>
</el-option>
</el-select>

其中,el-option 的数据绑定属性名默认是 id,可通过配置修改。同时,遍历 key 属性支持自定义,若为空则默认使用 value 属性名,减少额外数据处理。

如何使用

一个简单演示。

  • options:行数据
  • columnConfig:列配置

template 结构和 js 部分如下:

1
2
3
4
5
6
<BaseTableSelect 
v-model="selectValue"
:options="options"
:columnConfig="columnConfig"
>
</BaseTableSelect>
1
2
3
4
5
6
7
8
9
10
11
12
13
const options = ref([
{id: "213", department: '古典风格号', createTime: '2024-12-23', place: '学校'},
{id: "546", department: '都听好', createTime: '2024-12-23', place: '家里'},
{id: "345", department: '按时到岗', createTime: '2024-12-23', place: '医院'}
])

const columnConfig = ref([
{label: '单号', prop: 'id'},
{label: '部门', prop: 'department'},
{label: '时间', prop: 'createTime'},
{label: '地点', prop: 'place'},
])

自定义全表格搜索

el-select-table-input-filter-demo

组件 el-select 默认的搜索功能只会根据 label 属性的值去搜索,在表格展示的场景下并不符合,因此需要用到 filter-method 自定义搜索方法属性。

初步简单实现可使用 JSON.stringify() 将选项对象转为字符串,并检测是否包含搜索关键词:

1
2
3
const filterMethod = queryString => {
showOptions.value = props.options.filter(item => (JSON.stringify(item).includes(queryString) ? true : false))
}

不过,这种方式会将未展示的属性也纳入搜索,导致结果不准确。因此,优化后的搜索方法仅匹配已展示的列数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const filterMethod = queryString => {
if (!queryString) {
showOptions.value = props.options;
return;
}

showOptions.value = props.options.filter(item => {
// 针对每个要过滤的列进行判断
return props.columnConfig.some(config => {
const propValue = item[config.prop];
// 将属性值转换为字符串并检查是否包含查询字符串
return propValue && propValue.toString().includes(queryString);
});
});
}

扩展

本文提供的是基础实现。如果需要进一步功能扩展,例如控制搜索功能 (filterable) 或其他交互行为,可通过额外配置实现。但基础版本已满足我的需求,便不写太多不利于阅读代码。

组件代码

仓库:🔗 el-select-table-option

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
<template>
<el-select
ref="inputRef"
v-model="selectValue"
clearable
filterable
:placeholder="placeholder"
:disabled="disabled"
:filter-method="filterMethod"
@change="handleChange"
>
<!-- 表头 -->
<ul class="select-ul">
<li v-for="column in columnConfig" :key="column.label">{{ column.label }}</li>
</ul>
<el-option v-for="item in showOptions" :key="item[keyNameCom]" :value="item[valueName]">
<ul class="select-ul select-ul-data">
<li v-for="column in columnConfig" :key="column.label">{{ item[column.prop] }}</li>
</ul>
</el-option>
</el-select>
</template>

<script setup>
import { ref, reactive, onMounted, computed, watch, nextTick } from 'vue'

defineOptions({
name: 'BaseTableSelect'
})

const emit = defineEmits(['update:modelValue', 'keyup-enter', 'focus', 'change'])

const props = defineProps({
modelValue: null,
disabled: {
type: Boolean,
default: false
},
placeholder: {
type: String,
default: '请选择'
},
/* 列配置 */
columnConfig: {
type: Array,
default(rawProps) {
return []
}
},
options: {
type: Array,
default(rawProps) {
return []
}
},
valueName: {
type: String,
default: 'id'
},
keyName: {
type: String,
default: ''
}
})

const selectValue = computed({
get: () => props.modelValue,
set: val => {
emit('update:modelValue', val)
}
})

// 实际 keyName
// 优先使用 keyName,keyName 为空时,使用 valueName
const keyNameCom = computed(() => {
return props.keyName !== '' ? props.keyName : props.valueName
})

const handleChange = val => {
emit('change', val)
}


// ref
const inputRef = ref(null)

// 渲染用的options
const showOptions = ref(props.options)
watch(
() => props.options,
newValue => {
showOptions.value = newValue
selectValue.value = ''
},
)
// 筛选
const filterMethod = queryString => {
if (!queryString) {
showOptions.value = props.options;
return;
}

showOptions.value = props.options.filter(item => {
// 针对每个要过滤的列进行判断
return props.columnConfig.some(config => {
const propValue = item[config.prop];
// 将属性值转换为字符串并检查是否包含查询字符串
return propValue && propValue.toString().includes(queryString);
});
});
}

// 得到焦点
const getFocus = () => {
nextTick(() => {
inputRef.value.focus()
})
}

defineExpose({
getFocus
})
</script>

<style lang="scss" scoped></style>
<style scoped lang="scss">
.select-ul {
padding-right: 0;
list-style: none;
display: flex;
justify-content: space-between;
text-align: center;
padding-inline-start: 0;
padding: 0 20px;
> li {
display: inline-block;
width: 100px;
// margin: 6px;
overflow: hidden;
text-overflow: ellipsis;
}
}
.el-select-dropdown__item {
padding: 0;
}
</style>

属性

参数 说明 类型 默认值
disabled 是否禁用 boolean false
placeholder 请选择 string ‘请选择’
columnConfig 列配置,[{label: ‘’, prop: ‘’}] array []
options 选项 array []
valueName 选项值的属性名 string ‘id’
keyName 遍历选项时的 key string ‘’

参考