# Vue怎么实现底部弹窗多选项功能 ## 目录 - [一、需求分析与技术选型](#一需求分析与技术选型) - [二、基础弹窗组件实现](#二基础弹窗组件实现) - [三、多选功能核心逻辑](#三多选功能核心逻辑) - [四、高级功能扩展](#四高级功能扩展) - [五、性能优化方案](#五性能优化方案) - [六、完整代码实现](#六完整代码实现) - [七、常见问题排查](#七常见问题排查) - [八、最佳实践总结](#八最佳实践总结) ## 一、需求分析与技术选型 ### 1.1 典型业务场景 移动端应用中常见的底部弹窗多选场景包括: - 商品筛选(价格区间、品牌、分类) - 表单多选(城市选择、兴趣标签) - 权限配置(功能权限勾选) - 批量操作(消息删除、订单处理) ### 1.2 技术方案对比 | 方案 | 优点 | 缺点 | |---------------------|-----------------------|-----------------------| | 原生Picker | 性能好,原生体验 | 定制化能力弱 | | Vant/Picker组件 | 开箱即用 | 样式修改成本高 | | 自定义Transition组件 | 完全可控 | 开发成本较高 | | Teleport+自定义组件 | 可跨DOM层级 | 需要处理z-index问题 | ### 1.3 关键技术点 ```javascript // 技术栈依赖 const dependencies = { "vue": "^3.2.0", "vue-router": "^4.0.0", "vant": "^4.0.0", // 可选 "animate.css": "^4.0.0" // 动画库 }
<template> <teleport to="body"> <transition name="slide-up"> <div v-show="visible" class="multi-select-modal" @click.self="handleClose" > <div class="modal-content"> <!-- 头部标题区 --> <header class="modal-header"> <h3>{{ title }}</h3> <van-icon name="close" @click="handleClose" /> </header> <!-- 选项内容区 --> <div class="options-container"> <slot></slot> </div> <!-- 底部操作区 --> <footer class="modal-footer"> <van-button plain type="primary" @click="handleReset" > 重置 </van-button> <van-button type="primary" @click="handleConfirm" > 确认 </van-button> </footer> </div> </div> </transition> </teleport> </template>
/* 蒙层样式 */ .multi-select-modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 999; } /* 内容区动画 */ .slide-up-enter-active, .slide-up-leave-active { transition: transform 0.3s ease-out; } .slide-up-enter-from, .slide-up-leave-to { transform: translateY(100%); } /* 选项容器滚动 */ .options-container { max-height: 60vh; overflow-y: auto; -webkit-overflow-scrolling: touch; }
interface OptionItem { id: string | number label: string disabled?: boolean selected?: boolean } const state = reactive({ options: [] as OptionItem[], tempSelected: new Set<string | number>() })
const toggleSelect = (option) => { if (option.disabled) return // 多选模式处理 if (props.multiple) { if (state.tempSelected.has(option.id)) { state.tempSelected.delete(option.id) } else { state.tempSelected.add(option.id) } } // 单选模式处理 else { state.tempSelected.clear() state.tempSelected.add(option.id) } }
<template v-for="item in options" :key="item.id"> <div class="option-item" :class="{ 'selected': tempSelected.has(item.id), 'disabled': item.disabled }" @click="toggleSelect(item)" > <van-checkbox v-if="multiple" :model-value="tempSelected.has(item.id)" :disabled="item.disabled" /> <span>{{ item.label }}</span> </div> </template>
const searchQuery = ref('') const filteredOptions = computed(() => { return props.options.filter(item => item.label.toLowerCase().includes(searchQuery.value.toLowerCase()) ) })
<template v-for="group in optionGroups" :key="group.title"> <div class="option-group"> <h4 class="group-title">{{ group.title }}</h4> <div v-for="item in group.options" class="option-item" > <!-- 选项内容 --> </div> </div> </template>
const loading = ref(false) const hasMore = ref(true) const loadMore = async () => { if (loading.value || !hasMore.value) return loading.value = true try { const newData = await fetchMoreOptions() state.options.push(...newData) hasMore.value = newData.length > 0 } finally { loading.value = false } }
<RecycleScroller class="scroller" :items="filteredOptions" :item-size="56" key-field="id" v-slot="{ item }" > <OptionItem :item="item" /> </RecycleScroller>
const confirmSelection = debounce(() => { emit('confirm', Array.from(state.tempSelected)) hide() }, 300)
// 使用WeakMap存储大对象 const optionData = new WeakMap() const getOptionData = (option) => { if (!optionData.has(option)) { optionData.set(option, processLargeData(option)) } return optionData.get(option) }
<!-- MultiSelectModal.vue --> <script setup> import { computed, reactive, ref } from 'vue' import { debounce } from 'lodash-es' const props = defineProps({ // 完整props定义 }) const emit = defineEmits(['update:modelValue', 'confirm']) // 完整逻辑实现 </script> <template> <!-- 完整模板 --> </template> <style scoped> /* 完整样式 */ </style>
<template> <button @click="showModal">打开多选弹窗</button> <MultiSelectModal v-model:visible="show" :options="options" :multiple="true" @confirm="handleConfirm" /> </template> <script setup> const options = [ { id: 1, label: '选项1' }, // 更多选项... ] </script>
解决方案:
.modal-open { overflow: hidden; position: fixed; width: 100%; }
推荐层级管理:
const zIndexManager = { modal: 1000, dropdown: 1100, toast: 1200, loading: 1300 }
Android解决方案:
const adjustInputPosition = () => { if (!isAndroid()) return window.addEventListener('resize', () => { const activeElement = document.activeElement if (activeElement?.tagName === 'INPUT') { activeElement.scrollIntoView({ block: 'center' }) } }) }
<div role="checkbox" :aria-checked="isSelected" tabindex="0" @keydown.space="toggleSelect" > <!-- 选项内容 --> </div>
describe('MultiSelectModal', () => { test('多选模式正常工作', async () => { // 测试用例 }) test('搜索过滤功能', () => { // 测试用例 }) })
技术演进方向:
1. 考虑使用Web Components实现跨框架复用
2. 探索与Pinia的状态集成方案
3. 适配折叠屏设备的特殊布局处理 “`
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。