当前位置: 首页 > news >正文

vue3.2 + element-plus 实现跟随input输入框的弹框,弹框里可以分组或tab形式显示选项

效果

基础用法(分组选项)
在这里插入图片描述

高级用法(带Tab栏)
在这里插入图片描述

<!-- 弹窗跟随通用组件  SmartSelector.vue -->
<!-- 弹窗跟随通用组件 -->
<template><div class="smart-selector-container"><el-popover :visible="visible" :width="width" :placement="placement" trigger="manual" :popper-class="popperClass"@show="$emit('open')" @hide="$emit('close')"><template #reference><el-input ref="inputRef" v-model="selectedText" :placeholder="placeholder" :style="{ width: inputWidth }":type="multiline ? 'textarea' : 'text'" :autosize="autosize" :size="size" :readonly="readonly"@click="togglePopup"><template #suffix><el-icon><arrow-down /></el-icon></template></el-input></template><div class="smart-selector-content"><!-- Tab栏 --><el-tabs v-if="hasTabs" v-model="activeTab" @tab-click="handleTabChange"><el-tab-pane v-for="tab in tabs" :key="tab.name" :label="tab.label" :name="tab.name" /></el-tabs><el-scrollbar :max-height="maxHeight"><!-- 分组选项 --><template v-for="(group, index) in currentGroups" :key="index"><div v-if="group.title" class="group-title">{{ group.title }}</div><div class="options-grid"><div v-for="(item, itemIndex) in group.options" :key="itemIndex" class="option-item":class="{ 'is-selected': isSelected(item) }" @click="handleSelect(item)">{{ getOptionLabel(item) }}</div></div><el-divider v-if="index < currentGroups.length - 1" /></template></el-scrollbar></div></el-popover></div>
</template><script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
import { ArrowDown } from '@element-plus/icons-vue'const props = defineProps({modelValue: { type: [String, Array], default: '' },options: { type: Array, default: () => [] },groups: { type: Array, default: () => [] }, // 分组格式: [{title: '分组1', options: [...]}]tabs: { type: Array, default: () => [] }, // Tab格式: [{name: 'tab1', label: 'Tab1', options: [...]}]placeholder: { type: String, default: '请选择' },width: { type: String, default: '500px' },inputWidth: { type: String, default: '200px' },maxHeight: { type: String, default: '300px' },separator: { type: String, default: ',' },multiline: Boolean,autosize: { type: [Object, Boolean], default: () => ({ minRows: 2, maxRows: 4 }) },placement: { type: String, default: 'bottom-start' },readonly: { type: Boolean, default: false },popperClass: String,size: {type: String, default: 'default', validator: (value: string) => ['large', 'default', 'small'].includes(value)},singleSelect: Boolean, // 是否单选模式
})const emit = defineEmits(['update:modelValue', 'select', 'open', 'close', 'tab-change'])const inputRef = ref<HTMLElement | null>(null)
const popoverRef = ref<HTMLElement | null>(null)
const visible = ref(false)
const activeTab: any = ref('')const tabs: any = props.tabs || []const hasTabs = computed(() => tabs.length > 0)
const currentGroups = computed(() => {if (hasTabs.value) {const tab = tabs.value.find((t: any) => t.name === activeTab.value)if (tab?.groups) return tab.groupsif (tab?.options) return [{ options: tab.options }]return []}return props.groups.length > 0 ? props.groups : [{ options: props.options }]
})const selectedText = computed({get: () => {if (Array.isArray(props.modelValue)) {return props.modelValue.join(props.separator)}return props.modelValue || ''},set: (val) => emit('update:modelValue', val)
})// 初始化第一个Tab
if (hasTabs.value) {activeTab.value = tabs.value[0].name
}const togglePopup = () => {visible.value = !visible.value
}// 获取选项显示文本
const getOptionLabel = (item: any) => {return item?.label || item?.value || item
}// 检查是否已选中
const isSelected = (item: any) => {const value = item.value || item.label || itemif (Array.isArray(props.modelValue)) {return props.modelValue.includes(value)}return props.modelValue === value
}const handleSelect = (item: any) => {const value = item?.value || item?.label || itemif (props.singleSelect) {// 单选模式emit('update:modelValue', value)} else {// 多选模式if (Array.isArray(props.modelValue)) {const newValue = props.modelValue.includes(value)? props.modelValue.filter(v => v !== value): [...props.modelValue, value]emit('update:modelValue', newValue)} else {const currentValue = props.modelValue || ''if (currentValue.includes(value)) returnconst newValue = currentValue? `${currentValue}${props.separator}${value}`: valueemit('update:modelValue', newValue)}}emit('select', item)if (props.singleSelect) {visible.value = false}
}const handleTabChange = (tab: any) => {activeTab.value = tab.props.nameemit('tab-change', tab.props.name)
}// 处理键盘事件
const handleKeydown = (e: any) => {if (e.key === 'Escape') {visible.value = false}
}onMounted(() => {document.addEventListener('keydown', handleKeydown)
})onUnmounted(() => {document.removeEventListener('keydown', handleKeydown)
})
</script><style scoped lang="scss">
.smart-selector-container {position: relative;display: inline-block;
}.smart-selector-content {padding: 8px;:deep(.el-tabs__header) {margin: 0 0 12px 0;}
}.group-title {padding: 8px 0;font-weight: bold;color: var(--el-color-primary);
}.options-grid {display: flex;flex-wrap: wrap;gap: 8px;padding: 4px 0;
}.option-item {padding: 6px 12px;background: #f5f7fa;border-radius: 4px;cursor: pointer;transition: all 0.2s;white-space: nowrap;font-size: 14px;&:hover {background: var(--el-color-primary);color: white;transform: translateY(-1px);}&.is-selected {background: var(--el-color-primary);color: white;}
}:deep(.el-divider--horizontal) {margin: 12px 0;
}/**使用示例<SmartSelectorv-model="form.symptom":options="options"placeholder="症状/主诉"/>数据传参格式纯字符串格式(最简单)const options = ['头痛', '发热', '咳嗽']简约对象格式const options = [{ label: '头痛' }, { value: '发热' }, '咳嗽' // 混合格式也可以]标准对象格式const options = [{ label: '头痛', value: 'headache' },{ label: '发热', value: 'fever' }]基础用法(分组选项):<template><SmartSelectorv-model="form.symptom":groups="symptomGroups"placeholder="症状/主诉"separator=","multiline:autosize="{ minRows: 2, maxRows: 6 }"@select="handleSelect"/></template><script setup>import { ref } from 'vue'import SmartSelector from '@/components/popup/SmartSelector.vue'const form = ref({symptom: ''})const symptomGroups = ref([{title: '常见症状',options: [{ label: '头痛', value: '头痛' },{ label: '发热', value: '发热' },{ label: '咳嗽', value: '咳嗽' }]},{title: '特殊症状',options: [{ label: '心悸', value: '心悸' },{ label: '气短', value: '气短' },{ label: '胸闷', value: '胸闷' }]}])const handleSelect = (item) => {console.log('选中:', item)}</script>高级用法(带Tab栏):<template><SmartSelectorv-model="form.symptom":tabs="symptomTabs"placeholder="症状/主诉"separator=","width="600px"@select="handleSelect"@tab-change="handleTabChange"/></template><script setup>import { ref } from 'vue'import SmartSelector from '@/components/popup/SmartSelector.vue'const form = ref({symptom: ''})const symptomTabs = ref([{name: 'common',label: '常见症状',options: [{ label: '头痛', value: '头痛' },{ label: '发热', value: '发热' },{ label: '咳嗽', value: '咳嗽' }]},{name: 'special',label: '特殊症状',groups: [{title: '心血管症状',options: [{ label: '心悸', value: '心悸' },{ label: '胸闷', value: '胸闷' }]},{title: '呼吸症状',options: [{ label: '气短', value: '气短' },{ label: '呼吸困难', value: '呼吸困难' }]}]}])const handleSelect = (item) => {console.log('选中:', item)}const handleTabChange = (tabName) => {console.log('切换到Tab:', tabName)}</script>*/
</style>

http://www.mrgr.cn/news/98703.html

相关文章:

  • YOLOV8 OBB 海思3516训练流程
  • 十八、TCP多线程、多进程并发服务器
  • Tomcat与Servlet
  • vmcore分析锁问题实例(x86-64)
  • 面试经历---某能源公司
  • vue动态组件实现动态表单的方法
  • 从标准九九表打印解读单行表达式的书写修炼(Python)
  • 05-DevOps-Jenkins自动拉取构建代码
  • Rust : 关于*const () 与type erase
  • ReportLab 导出 PDF(图文表格)
  • 21天Python计划:零障碍学语法(更新完毕)
  • OSPF综合实验
  • Oracle 中的 NOAUDIT CREATE SESSION 命令详解
  • 双指针算法(一)
  • GR00T N1:面向通用类人机器人的开放基础模型
  • 一天时间,我用AI(deepseek)做了一个配色网站
  • Leetcode 325. 和等于 k 的最长子数组长度【Plus题】
  • PyTorch - Tensor 学习笔记
  • 【前端】Vue一本通 ESLint JSX
  • Vue3+Vite+TypeScript+Element Plus开发-17.Tags-组件构建