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

news2025/5/22 16:59:12

效果

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

高级用法(带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.groups
    if (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 || item
  if (Array.isArray(props.modelValue)) {
    return props.modelValue.includes(value)
  }
  return props.modelValue === value
}

const handleSelect = (item: any) => {
  const value = item?.value || item?.label || item

  if (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)) return

      const newValue = currentValue
        ? `${currentValue}${props.separator}${value}`
        : value
      emit('update:modelValue', newValue)
    }
  }

  emit('select', item)
  if (props.singleSelect) {
    visible.value = false
  }
}

const handleTabChange = (tab: any) => {
  activeTab.value = tab.props.name
  emit('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;
}

/**
    使用示例

    <SmartSelector
      v-model="form.symptom"
      :options="options"
      placeholder="症状/主诉"
    />

    数据传参格式

    纯字符串格式(最简单)
    const options = ['头痛', '发热', '咳嗽']

    简约对象格式
    const options = [
      { label: '头痛' }, 
      { value: '发热' }, 
      '咳嗽' // 混合格式也可以
    ]

    标准对象格式
    const options = [
      { label: '头痛', value: 'headache' },
      { label: '发热', value: 'fever' }
    ]


    基础用法(分组选项):
    <template>
      <SmartSelector
        v-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>
      <SmartSelector
        v-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.coloradmin.cn/o/2336992.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Windows VsCode Terminal窗口使用Linux命令

背景描述&#xff1a; 平时开发环境以Linux系统为主&#xff0c;有时又需要使用Windows系统下开发环境&#xff0c;为了能像Linux系统那样用Windows VsCode&#xff0c;Terminal命令行是必不可少内容。 注&#xff1a;Windows11 VsCode 1.99.2 下面介绍&#xff0c;如何在V…

负载均衡的实现方式有哪些?

负载均衡实现方式常见的有: 软件负载均衡、硬件负载均衡、DNS负载均衡 扩展 二层负载均衡&#xff1a;在数据链路层&#xff0c;基于MAC地址进行流量分发&#xff0c;较少见于实际应用中 三层负载均衡&#xff1a;在网络层&#xff0c;基于IP地址来分配流量&#xff0c;例如某…

LWIP学习笔记

TCP/ip协议结构分层 传输层简记 TCP&#xff1a;可靠性强&#xff0c;有重传机制 UDP&#xff1a;单传机制&#xff0c;不可靠 UDP在ip层分片 TCP在传输层分包 应用层传输层网络层&#xff0c;构成LWIP内核程序&#xff1a; 链路层&#xff1b;由mac内核STM芯片的片上外设…

Nodejs Express框架

参考&#xff1a;Node.js Express 框架 | 菜鸟教程 第一个 Express 框架实例 接下来我们使用 Express 框架来输出 "Hello World"。 以下实例中我们引入了 express 模块&#xff0c;并在客户端发起请求后&#xff0c;响应 "Hello World" 字符串。 创建 e…

Visual Studio Code 开发 树莓派 pico

开发环境 MCU&#xff1a;Pico1&#xff08;无wifi版&#xff09;使用固件&#xff1a;自编译版本开发环境&#xff1a;Windows 10开发工具&#xff1a;Visual Studio Code 1.99.2开发语言&#xff1a;MicroPython & C 插件安装 找到Raspberry Pi Pico并安装开启科学上网…

Python与R语言用XGBOOST、NLTK、LASSO、决策树、聚类分析电商平台评论信息数据集

全文链接&#xff1a;https://tecdat.cn/?p41501 分析师&#xff1a;Rui Liu 在当今数字化浪潮席卷的时代&#xff0c;电商市场的蓬勃发展犹如一部波澜壮阔的史诗&#xff0c;蕴藏着无尽的商业价值与潜力。电商平台积累的海量数据&#xff0c;宛如一座等待挖掘的宝藏&#xff…

半导体制造如何数字化转型

半导体制造的数字化转型正通过技术融合与流程重构&#xff0c;推动着这个精密产业的全面革新。全球芯片短缺与工艺复杂度指数级增长的双重压力下&#xff0c;头部企业已构建起四大转型支柱&#xff1a; 1. 数据中枢重构产线生态 台积电的「智慧工厂4.0」部署着30万物联网传感器…

LabVIEW 程序持续优化

LabVIEW 以其独特的图形化编程方式&#xff0c;在工业自动化、测试测量、数据分析等众多领域发挥着关键作用。为了让 LabVIEW 程序始终保持高效、稳定&#xff0c;并契合不断变化的实际需求&#xff0c;持续改进必不可少。下面将从多个关键维度&#xff0c;为大家细致地介绍通用…

Windows10系统RabbitMQ无法访问Web端界面

项目场景&#xff1a; 提示&#xff1a;这里简述项目相关背景&#xff1a; 项目场景&#xff1a; 在一个基于 .NET 的分布式项目中&#xff0c;团队使用 RabbitMQ 作为消息队列中间件&#xff0c;负责模块间的异步通信。开发环境为 Windows 10 系统&#xff0c;开发人员按照官…

初阶数据结构--链式二叉树

二叉树&#xff08;链式结构&#xff09; 前面的文章首先介绍了树的相关概念&#xff0c;阐述了树的存储结构是分为顺序结构和链式结构。其中顺序结构存储的方式叫做堆&#xff0c;并且对堆这个数据结构进行了模拟实现&#xff0c;并进行了相关拓展&#xff0c;接下来会针对链…

SpringAI版本更新:向量数据库不可用的解决方案!

Spring AI 前两天&#xff08;4.10 日&#xff09;更新了 1.0.0-M7 版本后&#xff0c;原来的 SimpleVectorStore 内存级别的向量数据库就不能用了&#xff0c;Spring AI 将其全部源码删除了。 此时我们就需要一种成本更低的解决方案来解决这个问题&#xff0c;如何解决呢&…

BladeX单点登录与若依框架集成实现

1. 概述 本文档详细介绍了将BladeX认证系统与若依(RuoYi)框架集成的完整实现过程。集成采用OAuth2.0授权码流程&#xff0c;使用户能够通过BladeX账号直接登录若依系统&#xff0c;实现无缝单点登录体验。 2. 系统架构 2.1 总体架构 #mermaid-svg-YxdmBwBtzGqZHMme {font-fa…

JVM 内存调优

内存调优 内存泄漏&#xff08;Memory Leak&#xff09;和内存溢出&#xff08;Memory Overflow&#xff09;是两种常见的内存管理问题&#xff0c;它们都可能导致程序执行不正常或系统性能下降&#xff0c;但它们的原因和表现有所不同。 内存泄漏 内存泄漏&#xff08;Memo…

Shell脚本提交Spark任务简单案例

一、IDEA打包SparkETL模块&#xff0c;上传值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视频汇聚系统,打造别墅居民区智能监控体系

一、现状背景 随着国家经济的快速增长&#xff0c;生活水平逐渐提高&#xff0c;私人别墅在城市、乡镇和农村的普及率也在逐年增加。然而&#xff0c;由于别墅区业主经济条件较好&#xff0c;各类不法事件也日益增多&#xff0c;主要集中在以下几个方面&#xff1a; 1&#x…

BGP分解实验·23——BGP选路原则之路由器标识

在选路原则需要用到Router-ID做选路决策时&#xff0c;其对等体Router-ID较小的路由将被优选&#xff1b;其中&#xff0c;当路由被反射时&#xff0c;包含起源器ID属性时&#xff0c;该属性将代替router-id做比较。 实验拓扑如下&#xff1a; 实验通过调整路由器R1和R2的rout…

【玩泰山派】MISC(杂项)- 使用vscode远程连接泰山派进行开发

文章目录 前言流程1、安装、启动sshd2、配置一下允许root登录3、vscode中配置1、安装remote插件2、登录 **注意** 前言 有时候要在开发板中写一写代码&#xff0c;直接在终端中使用vim这种工具有时候也不是很方便。这里准备使用vscode去通过ssh远程连接泰山派去操作&#xff0…

同步/异步日志系统

同步/异步日志系统 项目演示基础测试性能测试测试环境&#xff1a;同步日志器单线程同步日志器多线程异步日志器单线程异步日志器多线程 工具类&#xff08;util.hpp&#xff09;日志等级level.hpp 日志消息message.hpp 日志消息格式化formatter.hpp 日志消息落地sink.hpp 日志…

typescript html input无法输入解决办法

input里加上这个&#xff1a; onkeydown:(e: KeyboardEvent) > {e.stopPropagation();

游戏引擎学习第224天

回顾游戏运行并指出一个明显的图像问题。 回顾一下之前那个算法 我们今天要做一点预加载的处理。上周刚完成了游戏序章部分的所有剪辑内容。在运行这一部分时&#xff0c;如果观察得足够仔细&#xff0c;就会注意到一个问题。虽然因为视频流压缩质量较低&#xff0c;很难清楚…