在移动端表单开发中,多选组件几乎是每个项目都会遇到的标配功能。但当我们真正开始使用时,往往会发现现成的UI库组件总是差那么一点"味道"——要么展示效果生硬,要么交互体验不够流畅。特别是当用户选择了多个选项后,如何优雅地展示这些选项成为了一个常见的痛点。
市面上优秀的移动端UI库如Vant确实提供了丰富的组件,但在实际业务场景中,我们常常需要更精细的控制和更优雅的展示方式。原生的多选组件通常有以下不足:
javascript复制// 原生Vant多选组件的基本使用
<van-checkbox-group v-model="selectedItems">
<van-checkbox v-for="item in items" :key="item.value" :name="item.value">
{{ item.label }}
</van-checkbox>
</van-checkbox-group>
表格:原生组件与封装组件的功能对比
| 功能点 | 原生Vant组件 | 封装后的组件 |
|---|---|---|
| 选中项展示 | 简单文本拼接 | 可定制标签样式 |
| 长文本处理 | 无 | 自动截断+提示 |
| 交互反馈 | 基础 | 增强型动画效果 |
| 样式定制 | 需要覆盖默认样式 | 开箱即用的主题 |
| 性能优化 | 一般 | 虚拟滚动支持 |
我们的封装组件需要保持Vant的设计语言,同时增强其多选展示能力。整体结构分为三个层次:
html复制<template>
<div class="enhanced-multiple-picker">
<!-- 输入展示层 -->
<van-field
v-model="displayText"
readonly
is-link
@click="showPicker">
<template #extra>
<div class="tag-container">
<van-tag
v-for="(tag, index) in selectedTags"
:key="index"
type="primary">
{{ truncate(tag) }}
</van-tag>
</div>
</template>
</van-field>
<!-- 弹出选择层 -->
<van-popup v-model="show" position="bottom">
<div class="picker-header">
<button @click="cancel">取消</button>
<div class="title">{{ title }}</div>
<button @click="confirm">确认</button>
</div>
<van-checkbox-group v-model="internalValue">
<van-cell-group>
<van-cell
v-for="(item, index) in options"
:key="item.value"
:title="item.label"
clickable
@click="toggle(index)">
<template #right-icon>
<van-checkbox :name="item.value" />
</template>
</van-cell>
</van-cell-group>
</van-checkbox-group>
</van-popup>
</div>
</template>
组件的核心在于如何优雅地处理选中项的展示。我们通过计算属性和自定义渲染实现了这一目标:
javascript复制<script>
export default {
props: {
value: { type: Array, default: () => [] },
options: { type: Array, required: true },
maxDisplayLength: { type: Number, default: 15 },
maxTags: { type: Number, default: 3 }
},
data() {
return {
show: false,
internalValue: [...this.value]
}
},
computed: {
// 获取选中项对应的标签文本
selectedTags() {
return this.options
.filter(opt => this.internalValue.includes(opt.value))
.map(opt => opt.label)
},
// 处理展示文本
displayText() {
if (this.selectedTags.length === 0) return '请选择'
if (this.selectedTags.length <= this.maxTags) {
return this.selectedTags.join(',')
}
return `${this.selectedTags.slice(0, this.maxTags).join(',')}等${this.selectedTags.length}项`
}
},
methods: {
// 文本截断处理
truncate(text) {
if (text.length <= this.maxDisplayLength) return text
return `${text.substring(0, this.maxDisplayLength)}...`
},
toggle(index) {
const checkbox = this.$refs.checkboxes[index]
checkbox.toggle()
},
confirm() {
this.$emit('input', [...this.internalValue])
this.show = false
},
cancel() {
this.internalValue = [...this.value]
this.show = false
}
}
}
</script>
为了让选中项展示更加直观,我们采用了标签式展示方案:
less复制.enhanced-multiple-picker {
.tag-container {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 4px;
.van-tag {
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
}
}
.picker-header {
display: flex;
justify-content: space-between;
padding: 12px;
border-bottom: 1px solid #eee;
.title {
font-weight: bold;
}
}
}
javascript复制// 添加防抖处理示例
import { debounce } from 'lodash'
methods: {
showPicker: debounce(function() {
if (this.disabled) return
this.show = true
}, 200),
handleLongPress(tag) {
this.$toast({
message: tag,
position: 'middle',
duration: 1500
})
}
}
对于选项较多的场景,添加搜索功能可以极大提升用户体验:
html复制<van-popup v-model="show" position="bottom">
<div class="picker-header">
<van-search
v-model="searchText"
placeholder="搜索选项"
@search="onSearch"
/>
</div>
<van-checkbox-group v-model="internalValue">
<van-cell-group>
<van-cell
v-for="(item, index) in filteredOptions"
:key="item.value"
:title="item.label">
<template #right-icon>
<van-checkbox :name="item.value" />
</template>
</van-cell>
</van-cell-group>
</van-checkbox-group>
</van-popup>
当选项具有分类时,可以分组展示提升可浏览性:
javascript复制computed: {
groupedOptions() {
const groups = {}
this.options.forEach(option => {
if (!groups[option.category]) {
groups[option.category] = []
}
groups[option.category].push(option)
})
return groups
}
}
javascript复制// 虚拟滚动实现示例
import { RecycleScroller } from 'vue-virtual-scroller'
components: {
RecycleScroller
},
<recycle-scroller
class="scroller"
:items="filteredOptions"
:item-size="56"
key-field="value">
<template v-slot="{ item }">
<van-cell :title="item.label">
<template #right-icon>
<van-checkbox
v-model="internalValue"
:name="item.value" />
</template>
</van-cell>
</template>
</recycle-scroller>
在多个项目中实践这套封装方案后,我们发现了一些值得注意的细节:
键盘遮挡问题:在iOS设备上,弹出键盘可能会遮挡部分选项。解决方案是通过window.scrollTo确保当前选中项可见。
性能监控:当选项超过500个时,即使使用虚拟滚动,初始渲染也可能有延迟。我们添加了加载状态指示器来改善体验。
表单验证集成:与VeeValidate等验证库配合使用时,需要注意事件触发的时机,确保验证在正确的时间点执行。
主题一致性:虽然组件是封装的,但样式应该与项目中其他Vant组件保持统一,我们创建了一个mixin来处理主题变量。
javascript复制// 主题mixin示例
export const pickerMixin = {
methods: {
getThemeStyles() {
return {
'--tag-color': this.$style.primaryColor,
'--tag-background': lighten(this.$style.primaryColor, 40%),
'--tag-border': `1px solid ${this.$style.primaryColor}`
}
}
}
}