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>
相关文章:
vue3.2 + element-plus 实现跟随input输入框的弹框,弹框里可以分组或tab形式显示选项
效果 基础用法(分组选项) 高级用法(带Tab栏) <!-- 弹窗跟随通用组件 SmartSelector.vue --> <!-- 弹窗跟随通用组件 --> <template><div class"smart-selector-container"><el-popove…...
Vue 2.0和3.0笔记
Vue 3 关于组件 今天回顾了下2.0关于组件的内容,3.0定义组件的方式多了一种就是通过单文件组件(Single-File Component)的方式将Vue的模板,逻辑和样式放到一个文件中,2.0则不同,它是将模板放到一个属性中…...
Windows VsCode Terminal窗口使用Linux命令
背景描述: 平时开发环境以Linux系统为主,有时又需要使用Windows系统下开发环境,为了能像Linux系统那样用Windows VsCode,Terminal命令行是必不可少内容。 注:Windows11 VsCode 1.99.2 下面介绍,如何在V…...
负载均衡的实现方式有哪些?
负载均衡实现方式常见的有: 软件负载均衡、硬件负载均衡、DNS负载均衡 扩展 二层负载均衡:在数据链路层,基于MAC地址进行流量分发,较少见于实际应用中 三层负载均衡:在网络层,基于IP地址来分配流量,例如某…...
Oracle 中的 NOAUDIT CREATE SESSION 命令详解
Oracle 中的 NOAUDIT CREATE SESSION 命令详解 NOAUDIT CREATE SESSION 是 Oracle 数据库中用于取消对用户登录会话审计的命令,它与 AUDIT CREATE SESSION 命令相对应。 一、基本语法 NOAUDIT CREATE SESSION [BY user1 [, user2]... | BY [SESSION | ACCESS]] …...
OutputStreamWriter 终极解析与记忆指南
OutputStreamWriter 终极解析与记忆指南 一、核心本质 OutputStreamWriter 是 Java 提供的字符到字节的桥梁流,属于 Writer 的子类,负责将字符流按指定编码转换为字节流。 注意:OutputStreamWriter也是一个字符流,也是一个转换…...
1022 Digital Library
1022 Digital Library 分数 30 全屏浏览 切换布局 作者 CHEN, Yue 单位 浙江大学 A Digital Library contains millions of books, stored according to their titles, authors, key words of their abstracts, publishers, and published years. Each book is assigned an u…...
LWIP学习笔记
TCP/ip协议结构分层 传输层简记 TCP:可靠性强,有重传机制 UDP:单传机制,不可靠 UDP在ip层分片 TCP在传输层分包 应用层传输层网络层,构成LWIP内核程序: 链路层;由mac内核STM芯片的片上外设…...
Nodejs Express框架
参考:Node.js Express 框架 | 菜鸟教程 第一个 Express 框架实例 接下来我们使用 Express 框架来输出 "Hello World"。 以下实例中我们引入了 express 模块,并在客户端发起请求后,响应 "Hello World" 字符串。 创建 e…...
LeetCode面试热题150中19-22题学习笔记(用Java语言描述)
Day 04 19、最后一个单词的长度 需求:给你一个字符串 s,由若干单词组成,单词前后用一些空格字符隔开。返回字符串中 最后一个 单词的长度。 单词 是指仅由字母组成、不包含任何空格字符的最大子字符串。 代码表示 public class Q19_1 {p…...
道路运输安全员企业负责人考试内容与范围
道路运输企业主要负责人(安全员)考证要求 的详细说明,适用于企业法定代表人、分管安全负责人等需取得的 《道路运输企业主要负责人和安全生产管理人员安全考核合格证明》(交通运输部要求)。 考试内容与范围 1. 法律法…...
Visual Studio Code 开发 树莓派 pico
开发环境 MCU:Pico1(无wifi版)使用固件:自编译版本开发环境:Windows 10开发工具:Visual Studio Code 1.99.2开发语言:MicroPython & C 插件安装 找到Raspberry Pi Pico并安装开启科学上网…...
Oracle 11G RAC 删除添加节点(一):删除节点
1、查看节点删除前的资源状态 用集群资源查看命令查看一下状态 1 [gridlvmrac1 ~]$crsctl stat res ‐t 2 ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐…...
面试宝典(C++基础)-02
文章目录 1.C++基础1.1 说说new和malloc的区别1.2 说说const和define的区别。1.3 说说C++中函数指针和指针函数的区别1.4 说说const int *a, int const *a, const int a, int *const a, const int *consta分别是什么,有什么特点。1.5 说说使用指针需要注意什么?1.6 说说内联函…...
express框架使用cors包解决跨域问题时,还是存在问题的原因。
express框架使用cors包解决跨域问题时,还是存在问题的原因。 今天我在使用express框架写一个后台管理系统时,发现存在这样的问题,那就是跨域问题,但是我明明是使用了 cors 包解决了跨域问题了。当我调用其他接口的时候࿰…...
Python与R语言用XGBOOST、NLTK、LASSO、决策树、聚类分析电商平台评论信息数据集
全文链接:https://tecdat.cn/?p41501 分析师:Rui Liu 在当今数字化浪潮席卷的时代,电商市场的蓬勃发展犹如一部波澜壮阔的史诗,蕴藏着无尽的商业价值与潜力。电商平台积累的海量数据,宛如一座等待挖掘的宝藏ÿ…...
半导体制造如何数字化转型
半导体制造的数字化转型正通过技术融合与流程重构,推动着这个精密产业的全面革新。全球芯片短缺与工艺复杂度指数级增长的双重压力下,头部企业已构建起四大转型支柱: 1. 数据中枢重构产线生态 台积电的「智慧工厂4.0」部署着30万物联网传感器…...
LabVIEW 程序持续优化
LabVIEW 以其独特的图形化编程方式,在工业自动化、测试测量、数据分析等众多领域发挥着关键作用。为了让 LabVIEW 程序始终保持高效、稳定,并契合不断变化的实际需求,持续改进必不可少。下面将从多个关键维度,为大家细致地介绍通用…...
Windows10系统RabbitMQ无法访问Web端界面
项目场景: 提示:这里简述项目相关背景: 项目场景: 在一个基于 .NET 的分布式项目中,团队使用 RabbitMQ 作为消息队列中间件,负责模块间的异步通信。开发环境为 Windows 10 系统,开发人员按照官…...
初阶数据结构--链式二叉树
二叉树(链式结构) 前面的文章首先介绍了树的相关概念,阐述了树的存储结构是分为顺序结构和链式结构。其中顺序结构存储的方式叫做堆,并且对堆这个数据结构进行了模拟实现,并进行了相关拓展,接下来会针对链…...
Tree Shaking(摇树优化)详解
Tree Shaking(摇树优化)详解 Tree Shaking 是现代 JavaScript 打包工具(如 Webpack、Rollup、Vite等)中的一项重要优化技术,它的名字形象地比喻为"摇动一棵树,让没用的叶子掉下来"。 核心概念 …...
SpringAI版本更新:向量数据库不可用的解决方案!
Spring AI 前两天(4.10 日)更新了 1.0.0-M7 版本后,原来的 SimpleVectorStore 内存级别的向量数据库就不能用了,Spring AI 将其全部源码删除了。 此时我们就需要一种成本更低的解决方案来解决这个问题,如何解决呢&…...
BladeX单点登录与若依框架集成实现
1. 概述 本文档详细介绍了将BladeX认证系统与若依(RuoYi)框架集成的完整实现过程。集成采用OAuth2.0授权码流程,使用户能够通过BladeX账号直接登录若依系统,实现无缝单点登录体验。 2. 系统架构 2.1 总体架构 #mermaid-svg-YxdmBwBtzGqZHMme {font-fa…...
JVM 内存调优
内存调优 内存泄漏(Memory Leak)和内存溢出(Memory Overflow)是两种常见的内存管理问题,它们都可能导致程序执行不正常或系统性能下降,但它们的原因和表现有所不同。 内存泄漏 内存泄漏(Memo…...
Shell脚本提交Spark任务简单案例
一、IDEA打包SparkETL模块,上传值HDFS的/tqdt/job目录 二、创建ods_ETL.sh脚本 mkdir -p /var/tq/sh/dwd vim /var/tq/sh/dwd/ods_ETL.sh chmod 754 /var/tq/sh/dwd/ods——ETL.sh #脚本内容如下 #!/bin/bash cur_date$(date %Y-%m-%d) /opt/bigdata/spark-3.3.2/b…...
国标GB28181视频平台EasyCVR视频汇聚系统,打造别墅居民区智能监控体系
一、现状背景 随着国家经济的快速增长,生活水平逐渐提高,私人别墅在城市、乡镇和农村的普及率也在逐年增加。然而,由于别墅区业主经济条件较好,各类不法事件也日益增多,主要集中在以下几个方面: 1&#x…...
BGP分解实验·23——BGP选路原则之路由器标识
在选路原则需要用到Router-ID做选路决策时,其对等体Router-ID较小的路由将被优选;其中,当路由被反射时,包含起源器ID属性时,该属性将代替router-id做比较。 实验拓扑如下: 实验通过调整路由器R1和R2的rout…...
机器学习(5)——支持向量机
1. 支持向量机(SVM)是什么? 支持向量机(SVM,Support Vector Machine)是一种监督学习算法,广泛应用于分类和回归问题,尤其适用于高维数据的分类。其核心思想是寻找最优分类超平面&am…...
访问不到服务器上启动的llamafactory-cli webui
采用SSH端口转发有效,在Windows上面进行访问 在服务器上启动 llamafactory-cli webui 后,访问方式需根据服务器类型和网络环境选择以下方案: 一、本地服务器(物理机/虚拟机) 1. 直接访问 若服务器与操作设备处于同一…...
【玩泰山派】MISC(杂项)- 使用vscode远程连接泰山派进行开发
文章目录 前言流程1、安装、启动sshd2、配置一下允许root登录3、vscode中配置1、安装remote插件2、登录 **注意** 前言 有时候要在开发板中写一写代码,直接在终端中使用vim这种工具有时候也不是很方便。这里准备使用vscode去通过ssh远程连接泰山派去操作࿰…...
