日常学习开发记录-select组件(2)
日常学习开发记录-select组件(2)
- 第二阶段:增强功能
给现有select组件新增功能
第二阶段:增强功能
- 键盘操作支持
- 支持键盘上下箭头选择选项
- 支持回车键确认选择
- 支持Esc键关闭下拉菜单
<template><div:class="['my-select', { 'is-disabled': disabled }]"@click.stop="toggleDropdown"@mouseenter="handleMouseEnter"@mouseleave="handleMouseLeave"v-click-outside="closeDropdown"tabindex="0"@keydown="handleKeydown"ref="select"><!-- 选择器触发器 --><div class="my-select__trigger"><span v-if="!currentValue && !multiple" class="my-select__placeholder">{{ placeholder }}</span><span v-else-if="!multiple" class="my-select__label">{{ getSelectedLabel() }}</span><div v-else class="my-select__tags"><spanv-for="item in selected":key="typeof item === 'object' ? item.value : item"class="my-select__tag">{{ typeof item === 'object' ? item.label : item }}<i class="my-select__tag-close" @click.stop="removeTag(item)">×</i></span></div><iv-if="clearable && currentValue && !visible && hover"class="my-select__clear"@click.stop="clearSelection">×</i><i v-else class="my-select__arrow" :class="{ 'is-reverse': visible }"></i></div><!-- 下拉菜单 --><div v-show="visible" class="my-select__dropdown"><divv-for="(item, index) in options":key="typeof item === 'object' ? item.value : item"class="my-select__option":class="{'is-selected': isSelected(item),'is-disabled': item.disabled,'is-highlighted': highlightedIndex === index,}"@click.stop="handleOptionClick(item)"@mouseenter="highlightedIndex = index"><slot name="option" :item="item" :index="index">{{ typeof item === 'object' ? item.label : item }}</slot></div><div v-if="options.length === 0" class="my-select__empty"><slot name="empty">无数据</slot></div></div></div>
</template><script>export default {name: 'MySelect',directives: {clickOutside: {bind(el, binding) {el.clickOutsideEvent = event => {if (!(el == event.target || el.contains(event.target))) {binding.value(event)}}document.addEventListener('click', el.clickOutsideEvent)},unbind(el) {document.removeEventListener('click', el.clickOutsideEvent)},},},props: {value: {type: [String, Array],default: '',},options: {type: Array,default: () => [],},multiple: {type: Boolean,default: false,},clearable: {type: Boolean,default: false,},placeholder: {type: String,default: '请选择',},disabled: {type: Boolean,default: false,},},data() {return {visible: false,currentValue: this.value,selected: [],hover: false,highlightedIndex: -1, // 当前高亮的选项索引}},watch: {value: {handler(newVal) {this.currentValue = newValif (this.multiple) {this.selected = Array.isArray(newVal) ? [...newVal] : []}},immediate: true,},visible(val) {if (val) {// 当下拉菜单打开时,重置高亮索引或设置为当前选中项this.initHighlightIndex()} else {// 关闭时重置高亮索引this.highlightedIndex = -1}},options() {// 当选项变化时,重置高亮索引if (this.visible) {this.initHighlightIndex()}},},methods: {handleMouseEnter() {this.hover = true},handleMouseLeave() {this.hover = false},isSelected(item) {return this.multiple ? this.selected.includes(item) : this.currentValue === item.value},toggleDropdown() {if (this.disabled) returnthis.visible = !this.visibleif (this.visible) {// 切换后聚焦以便捕获键盘事件this.$nextTick(() => {this.$refs.select.focus()})}},closeDropdown() {this.visible = false},getSelectedLabel() {return (this.options.find(item => item.value === this.currentValue)?.label || this.placeholder)},handleOptionClick(item) {if (item.disabled) returnif (this.multiple) {if (this.selected.includes(item)) {this.selected = this.selected.filter(i => i !== item)} else {this.selected.push(item)}this.$emit('input', this.selected)this.$emit('change', this.selected)} else {this.currentValue = item.valuethis.$emit('input', item.value)this.$emit('change', item.value)this.closeDropdown()}},clearSelection(event) {event.stopPropagation()this.currentValue = ''this.selected = []this.$emit('input', this.multiple ? [] : '')this.$emit('clear')},removeTag(item) {this.selected = this.selected.filter(i => i !== item)this.$emit('input', this.selected)this.$emit('remove-tag', item)},// 键盘事件处理handleKeydown(event) {if (this.disabled) return// 只有在下拉菜单打开或按下方向键、回车键和ESC键时才处理const keyCode = event.keyCodeif (this.visible) {// 下拉菜单已打开switch (keyCode) {case 38: // 上箭头event.preventDefault()this.navigateOptions('prev')breakcase 40: // 下箭头event.preventDefault()this.navigateOptions('next')breakcase 13: // 回车键event.preventDefault()if (this.highlightedIndex > -1 && this.options[this.highlightedIndex]) {this.handleOptionClick(this.options[this.highlightedIndex])}breakcase 27: // ESC键event.preventDefault()this.closeDropdown()break}} else {// 下拉菜单未打开switch (keyCode) {case 38: // 上箭头case 40: // 下箭头event.preventDefault()this.toggleDropdown()breakcase 13: // 回车键event.preventDefault()this.toggleDropdown()break}}},// 导航选项navigateOptions(direction) {if (this.options.length === 0) returnlet newIndexif (direction === 'next') {// 如果当前高亮选项是最后一个,则跳转到第一个if (this.highlightedIndex >= this.options.length - 1) {newIndex = 0} else {newIndex = this.highlightedIndex + 1}} else if (direction === 'prev') {// 如果当前高亮选项是第一个,则跳转到最后一个if (this.highlightedIndex <= 0) {newIndex = this.options.length - 1} else {newIndex = this.highlightedIndex - 1}}// 跳过禁用的选项const numOptions = this.options.lengthlet counter = 0// 如果当前高亮选项是禁用的,则继续跳转while (counter++ < numOptions &&this.options[newIndex] &&this.options[newIndex].disabled) {if (direction === 'next') {newIndex = newIndex >= this.options.length - 1 ? 0 : newIndex + 1} else {newIndex = newIndex <= 0 ? this.options.length - 1 : newIndex - 1}}this.highlightedIndex = newIndexthis.scrollToOption()},// 滚动到当前高亮的选项scrollToOption() {this.$nextTick(() => {const dropdown = this.$el.querySelector('.my-select__dropdown')if (!dropdown) returnconst options = dropdown.querySelectorAll('.my-select__option')if (this.highlightedIndex < 0 || !options[this.highlightedIndex]) returnconst highlighted = options[this.highlightedIndex]const scrollTop = dropdown.scrollTopconst offsetTop = highlighted.offsetTop// 如果高亮选项在可视区域上方,则滚动到高亮选项上方if (offsetTop < scrollTop) {dropdown.scrollTop = offsetTop} else if (offsetTop > scrollTop + dropdown.clientHeight - highlighted.offsetHeight) {// 如果高亮选项在可视区域下方,则滚动到高亮选项下方dropdown.scrollTop = offsetTop - dropdown.clientHeight + highlighted.offsetHeight}})},// 初始化高亮索引initHighlightIndex() {this.highlightedIndex = -1// 尝试高亮当前选中项if (!this.multiple && this.currentValue) {const selectedIndex = this.options.findIndex(item => item.value === this.currentValue)if (selectedIndex > -1) {this.highlightedIndex = selectedIndexthis.scrollToOption()return}}// 否则高亮第一个非禁用选项for (let i = 0; i < this.options.length; i++) {if (!this.options[i].disabled) {this.highlightedIndex = ithis.scrollToOption()break}}},},}
</script><style lang="scss" scoped>.my-select {position: relative;display: inline-block;width: 240px;font-size: 14px;cursor: pointer;outline: none; // 移除默认的焦点轮廓,可以添加自定义样式&:focus {.my-select__trigger {border-color: #409eff;outline: 0;box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);}}&.is-disabled {.my-select__trigger {background-color: rgb(245, 247, 250);color: rgb(192, 196, 204);cursor: not-allowed;border-color: rgb(228, 231, 237);}}&__trigger {display: flex;align-items: center;background-color: #fff;border: 1px solid #dcdfe6;border-radius: 4px;padding: 0 35px 0 15px;min-height: 40px;line-height: 40px;position: relative;transition: border-color 0.2s;box-sizing: border-box;&:hover {border-color: #c0c4cc;}}&__placeholder {color: #909399;}&__arrow {position: absolute;right: 15px;top: 50%;transform: translateY(-50%);transition: transform 0.3s;width: 0;height: 0;border-style: solid;border-width: 5px 5px 0 5px;border-color: #c0c4cc transparent transparent transparent;&.is-reverse {transform: translateY(-50%) rotate(180deg);}}&__clear {position: absolute;right: 15px;top: 50%;transform: translateY(-50%);color: #c0c4cc;font-size: 14px;&:hover {color: #909399;}}&__dropdown {position: absolute;top: 100%;left: 0;margin-top: 5px;background-color: #fff;border: 1px solid #e4e7ed;border-radius: 4px;box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);box-sizing: border-box;z-index: 1000;width: 100%;max-height: 274px;overflow-y: auto;}&__option {padding: 0 20px;position: relative;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;height: 34px;line-height: 34px;box-sizing: border-box;&:hover {background-color: #f5f7fa;}&.is-selected {color: #409eff;font-weight: 700;}&.is-highlighted {background-color: #f5f7fa;}&.is-disabled {color: #c0c4cc;cursor: not-allowed;}}&__empty {padding: 10px 0;text-align: center;color: #909399;}&__tags {display: flex;flex-wrap: wrap;line-height: normal;max-width: 100%;overflow: hidden;}&__tag {display: inline-flex;align-items: center;max-width: 100%;margin: 2px 0 2px 6px;padding: 0 5px 0 10px;background-color: #f0f2f5;border-radius: 4px;height: 24px;line-height: 24px;white-space: nowrap;overflow: hidden;box-sizing: border-box;}&__tag-close {margin-left: 5px;color: #909399;font-size: 12px;cursor: pointer;&:hover {color: #606266;}}}
</style>
箭头键导航:
当下拉菜单关闭时,按上下箭头键可以打开下拉菜单
当下拉菜单打开时,按上下箭头键可以在选项中导航
导航会自动跳过禁用的选项
当到达列表顶部或底部时会循环到另一端
回车键确认:
当下拉菜单关闭时,按回车键会打开下拉菜单
当下拉菜单打开时,按回车键会选中当前高亮的选项
Esc键关闭:
按Esc键会关闭下拉菜单
此外,我还添加了以下增强功能:
高亮状态:
添加了视觉高亮效果,显示当前键盘导航位置
鼠标悬停时也会更新高亮状态
滚动同步:
当使用键盘导航时,会自动滚动到当前高亮的选项位置
确保高亮选项始终在可视区域内
焦点样式:
添加了组件获得焦点时的视觉反馈
使用蓝色边框和轻微阴影提示当前可以使用键盘操作
以上修改使Select组件的可访问性大大提高,既提供了键盘操作支持,又保留了原有的鼠标操作功能。
相关文章:
日常学习开发记录-select组件(2)
日常学习开发记录-select组件(2) 第二阶段:增强功能 给现有select组件新增功能 第二阶段:增强功能 键盘操作支持 支持键盘上下箭头选择选项支持回车键确认选择支持Esc键关闭下拉菜单 <template><div:class"[my-s…...
微服务 - 高级篇
微服务 - 高级篇 一、服务治理(一)服务注册与发现(二)负载均衡(三)服务熔断与降级 二、分布式事务(一)解决方案(二)最终一致性 三、性能优化(一&a…...
服务器入门笔记
服务器 采用linux操作系统 SN号 服务器的唯一标识 1U的服务器的高度——4.445cm 服务器上UID灯用于定位服务器 服务器是计算机的一种。在网络中为其他客户机提供计算或者应用服务。 服务器用来响应终端的服务请求,并进行处理 服务器的分类—— 按物理形态&#…...
【Linux】VMware17 安装 Ubuntu24.04 虚拟机
目录 安装教程 一、下载 Ubuntu 桌面版iso映像 二、安装 VMware 三、安装 Ubuntu 桌面版 VMware 创建虚拟机 挂载 Ubuntu ISO 安装 Ubuntu 系统 安装教程 一、下载 Ubuntu 桌面版iso映像 链接来自 清华大学开源软件镜像站 ISO文件地址:ubuntu-24.04.2-des…...
WPS宏开发手册——JSA语法
目录 系列文章2、JSA语法2.1、打印输出2.2、注释2.3、变量2.4、数据类型2.5、函数2.6、运算符2.7、比较2.8、if else条件语句2.9、for循环2.10、Math对象(数字常用方法)2.11、字符串常用方法2.12、数组常用方法 系列文章 使用、工程、模块介绍 JSA语…...
word中指定页面开始添加页码
第一步: 插入页码 第二步: 把光标放到指定起始页码处 第三步: 取消链接到前一节 此时关掉页脚先添加分节符 添加完分节符后恢复点击 第四步: 设置页码格式,从1开始 第五步: 删掉不要的页码,…...
详解“二分”系列算法
前言 1.学习建议 网上教二分系列算法的视频或者文章不在少数,每个人对于二分算法的理解都是不一样的,作者不建议小白刚学习二分系列算法就看很多不同的视频或者博客去学习,举个例子,有些教学提供的方法会把left赋值为-1…...
Python实现deepseek接口的调用
简介:DeepSeek 是一个强大的大语言模型,提供 API 接口供开发者调用。在 Python 中,可以使用 requests 或 httpx 库向 DeepSeek API 发送请求,实现文本生成、代码补全,知识问答等功能。本文将介绍如何在 Python 中调用 …...
文档处理控件Aspose.Words 教程:.NET版中增强的 AI 文档摘要功能
Aspose.Words是一个功能强大的 Word 文档处理库。它可以帮助开发人员自动编辑、转换和处理文档。 自 24.11 版以来,Aspose.Words for .NET 提供了 AI 驱动的文档摘要功能,使用户能够从冗长的文本中快速提取关键见解。在 25.2 版中,我们通过使…...
【Linux 维测专栏 5 -- linux pstore 使用介绍】
文章目录 Linux pstore 功能简介1. pstore 概述2. pstore 的核心功能3. pstore 的工作原理4. pstore 的使用示例5. pstore 的优势6. 典型应用场景配置示例1)DTS配置2)config配置运行测试及log问题小结Linux pstore 功能简介 1. pstore 概述 pstore(Persistent Storage)是…...
19,C++——11
目录 一、 C11简介 二、 新增的列表初始化 三、 新增的STL容器 四、 简化声明 1,auto 2,decltype 3,nullptr 五、右值引用 1,左值引用和右值引用 2,两种引用的比较 3,左值引用的使用场景 4&…...
风尚云网|前端|前后端分离架构深度剖析:技术革新还是过度设计?
前后端分离架构深度剖析:技术革新还是过度设计? 作者:风尚云网 在数字化转型浪潮中,前后端分离架构已成为现代Web开发的主流模式。但这项技术真的是银弹吗?本文将从工程实践角度,剖析其优势与潜在风险&am…...
ffmpeg介绍(一)——解封装
解封装 常用函数 1. avformat_open_input() 作用 打开媒体文件或网络资源:解析文件路径或 URL,识别媒体格式(如 MP4、AVI、RTSP 等)。初始化 AVFormatContext:分配并初始化 AVFormatContext 结构体,…...
版本控制GIT的使用
在 GitCode 上进行代码提交的步骤与在 GitHub 或其他 Git 托管平台上提交代码的步骤类似。以下是一个基本的流程: 1. 安装 Git 如果你还没有安装 Git,首先需要在你的计算机上安装 Git。你可以从 Git 官方网站 下载并安装适合你操作系统的版本。 2. 配…...
本周安全速报(2025.3.18~3.24)
合规速递 01 2025欧洲网络安全报告:DDoS攻击同比增长137%,企业应如何应对? 原文: https://hackread.com/european-cyber-report-2025-137-more-ddos-attacks/ 最新的Link11《欧洲网络安全报告》揭示了一个令人担忧的趋势:DDo…...
CMS网站模板设计与用户定制化实战评测
内容概要 在数字化转型背景下,CMS平台作为企业内容管理的核心载体,其模板架构的灵活性与用户定制能力直接影响运营效率。通过对WordPress、Baklib等主流系统的技术解构发现,模块化设计理念已成为行业基准——WordPress依托超过6万款主题库实…...
【后端开发面试题】每日 3 题(二十)
✍个人博客:Pandaconda-CSDN博客 📣专栏地址:https://blog.csdn.net/newin2020/category_12903849.html 📚专栏简介:在这个专栏中,我将会分享后端开发面试中常见的面试题给大家,每天的题目都是独…...
搭建个人博客教程(Hexo)
如何快速搭建一套本地的博客系统呢?这里有一套gitNode.jsHexo的部署方案来进行解决。 安装git Git 是一款免费开源的分布式版本控制系统,由 Linus Torvalds 于 2005 年为 Linux 内核开发设计。它通过本地仓库和远程仓库实现代码管理,支持分支…...
Docker 可视化工具 Portainer
Docker 可视化工具 Portainer安装 官方安装地址:https://docs.portainer.io/start/install-ce/server/docker/wsl 一,首先,创建 Portainer Server 用来存储数据库的卷: docker volume create portainer_data二,然后…...
数据库基础知识点(系列二)
1.关系数据模型由哪三个要素组成。 答:关系数据模型由关系数据结构、关系操作集合和关系完整性约束三部分组成。 2.简述关系的性质。(关系就是一张二维表格,但不是任何二维表都叫关系) 答:(1…...
Docker-Compose部署 EasySearch 异常问题排查
近期将原本运行在 macOS 上的 EasySearch、Console 和 Coco-server 等服务迁移至群晖 NAS 平台。在迁移过程中遇到了EasySearch容器无法正常启动或运行中意外终止的问题。本文记录了这些问题的具体表现及解决方案,旨在为后续类似部署提供参考。 基础部署配置 以下…...
如何进行灌区闸门自动化改造-闸门远程控制系统建设
改造背景 操作效率低:人工启闭耗时耗力,单次操作需2-3人配合,耗时长。 水资源浪费:依赖经验估算放水量,易导致漫灌或供水不足。 管理滞后:无法实时监控水位、流量,故障响应延迟。 …...
深入解析 Vue3 响应式系统:原理、性能优化与应用场景
文章目录 1. Vue3 响应式系统的基本原理:Proxy 与 Reflect1.1 Proxy 和 Reflect 概述1.1.1 Proxy1.1.2 Reflect1.1.3 Proxy 和 Reflect 的协作 1.2 Vue3 响应式系统:如何通过 Proxy 实现数据代理1.3 Vue3 中 Proxy 的核心概念:响应式数据的创…...
C++11QT复习(二)
文章目录 Day4-4 New 与 delete 表达式(2025.03.20)1. new 表达式的三个步骤2. delete 表达式的两个步骤3. new[] 与 delete[] Day5 类的定义和关键字再探(2025.03.24)1. C 关键字 const、static、extern2. 类的定义:C…...
【数据挖掘】数据预处理——以鸢尾花数据集为例
数据预处理——以鸢尾花数据集为例 一、实验手册(一)实验目的(二)实验原理(三)实验环境(四)实验步骤(五)实验报告要求 二、案例代码(以鸢尾花数据…...
【算法笔记】图论基础(二):最短路、判环、二分图
目录 最短路松弛操作Dijkstra朴素Dijkstra时间复杂度算法过程例题 堆优化Dijkstra时间按复杂度算法过程例题 bellman-ford时间复杂度为什么dijkstra不能处理负权边?dijkstra的三个步骤:反例失效的原因 算法过程例题 spfa时间复杂度算法过程例题spfa求最短…...
HTTP/HTTPS 中 GET 请求和 POST 请求的区别与联系
一、基础概念 HTTP (HyperText Transfer Protocol, 超文本传输协议) 是一种用于浏览器与服务器之间进行数据交互的协议。HTTPS (加密的 HTTP) 则通过 SSL/TLS 协议实现通信加密与数据安全性。 二、GET 和 POST 概述 GET 请求: 用于从服务器获取资源。 POST 请求: 用于将数据…...
Spring、Spring Boot与Spring Cloud深度解析:核心关系与实战应用指南
1. 技术定位 Spring Framework:企业级Java开发的基础框架Spring Boot:快速构建独立运行的Spring应用Spring Cloud:分布式系统开发的微服务全家桶 二、Spring Framework核心解析 1. 关键特性 // 典型Spring MVC控制器示例 Controller Reque…...
EMS小车技术特点与优势:高效灵活的自动化输送解决方案
北成新控伺服技术丨EMS小车调试视频 EMS小车是一种基于单轨运行的电动输送系统,通过电力驱动实现物料的高效搬运和输送,具有高效灵活、节能环保、多功能集成、行业适配性强等特性,广泛应用于汽车制造、工程机械、家电生产、仓储物流等行业自动…...
uniapp运行到支付宝开发者工具
使用uniapp编写专有钉钉和浙政钉出现的样式问题 在支付宝开发者工具中启用2.0构建的时候,在开发工具中页面样式正常 但是在真机调试和线上的时候不正常 页面没问题,所有组件样式丢失 解决 在manifest.json mp-alipay中加入 "styleIsolation&qu…...
