给我的小程序加了个丝滑的搜索功能,踩坑表情包长度问题

前言

最近在用自己的卡盒小程序的时候,发现卡片越来越多,有时候要找到某一张来看看笔记要找半天,于是自己做了一个搜索功能,先看效果:

搜索.gif

怎么样,是不是还挺不错的,那么这篇文章就讲讲这样一个搜索展示的功能是如何实现的。

代码实现

首先我们分析这个搜索功能包含哪些需要实现的点:

  1. 输入关键字,匹配对应的卡片展示
  2. 匹配的卡片上面,要把符合搜索条件的所有词给高亮展示出来,不然文本多的时候容易看花了。
  3. 点击匹配到的卡片,要跳转到卡片的位置,并且闪烁两下,这样一看就知道要找卡片在哪了。

分析完成后,我们一点一点实现。

匹配关键字

我的小程序目前是纯前端搜索的,只是目前是这样,所以搜索逻辑也是在前端实现。搜索逻辑如果简单实现的话就是将搜索框的内容与列表中的每一项进行对比,看看内容中有没有包含这个字符串的,如果有就把这个项给返回回去。

先看代码:

const onSearch = () => {// 检查搜索文本框是否有值if (searchText.value) {// 创建一个正则表达式对象,用于在卡片内容中搜索文本// 'giu' 标志表示全局搜索、不区分大小写和支持 Unicodeconst searchTextRegex = new RegExp(searchText.value, 'giu')// 遍历所有的卡片盒子const matchCardBox = cardDataStore.cardBoxList.map((cardBox) => {// 对每个卡片盒子,创建一个新对象,包含原始属性和修改后的卡片项目return {...cardBox,// 映射并过滤卡片项目,只保留匹配搜索文本的项目cardItems: cardBox.cardItems.map((cardItem) => {// 初始化前面和背面内容的匹配数组const frontMatches = []const backMatches = []let match// 在卡片前面内容中搜索匹配项while ((match = searchTextRegex.exec(cardItem.frontContent)) !== null) {// 记录每个匹配项的起始和结束索引frontMatches.push({startIndex: match.index,endIndex: match.index + match[0].length,})}// 重置正则表达式的 lastIndex 属性,以便重新搜索searchTextRegex.lastIndex = 0// 在卡片背面内容中搜索匹配项while ((match = searchTextRegex.exec(cardItem.backContent)) !== null) {// 记录每个匹配项的起始和结束索引backMatches.push({startIndex: match.index,endIndex: match.index + match[0].length,})}// 检查是否有匹配项(前面或背面)const isMatched = frontMatches.length > 0 || backMatches.length > 0// 返回一个新的卡片项目对象,包含是否匹配和匹配项的位置return {...cardItem,isMatched,frontMatches,backMatches,}})// 过滤掉不匹配的项目.filter((item) => item.isMatched),}})// 过滤掉没有匹配项目的卡片盒子filteredCards.value = matchCardBox.filter((cardBox) => cardBox.cardItems.length > 0)} else {// 如果没有搜索文本,则清空过滤后的卡片列表filteredCards.value = []}
}
1. 创建正则表达式
const searchTextRegex = new RegExp(searchText.value, 'giu') 
  • searchText.value:这是用户输入的搜索文本。
    • new RegExp(...) :通过传入的搜索文本和标志(‘giu’)创建一个新的正则表达式对象。
    • g:全局搜索标志,表示搜索整个字符串中的所有匹配项,而不是在找到第一个匹配项后停止。
    • i:不区分大小写标志,表示搜索时忽略大小写差异。 - u:Unicode 标志,表示启用 Unicode 完整匹配模式,这对于处理非 ASCII 字符很重要。
2. 搜索匹配项
let match 
  • let match:声明一个变量 match,它将用于存储 RegExp.exec() 方法找到的匹配项。
while ((match = searchTextRegex.exec(cardItem.frontContent)) !== null) { // ... } 
  • searchTextRegex.exec(cardItem.frontContent) :在 cardItem.frontContent (卡片的正面内容)中执行正则表达式搜索。
    • 如果找到匹配项,exec() 方法返回一个数组,其中第一个元素(match[0])是找到的匹配文本,index 属性是匹配项在字符串中的起始位置。
  • 如果没有找到匹配项,exec() 方法返回 null。 - while 循环:继续执行,直到 exec() 方法返回 null,表示没有更多的匹配项。
3. 记录匹配项的索引
frontMatches.push({ startIndex: match.index, endIndex: match.index + match[0].length, }) 
  • 在每次循环迭代中,都会找到一个匹配项。
  • startIndex:匹配项在 cardItem.frontContent 中的起始位置。
  • endIndex:匹配项在 cardItem.frontContent 中的结束位置(即起始位置加上匹配文本的长度)。
  • frontMatches.push(...):将包含起始和结束索引的对象添加到 frontMatches 数组中。

经过这么一番操作,我们就可以获得一个筛选后的数组,其中包含了所有匹配的项,每个项还有一个二维数组用来记录匹配位置开头结尾的索引:

cardItems: (CardItem & {id: stringfrontMatches?: { startIndex: number; endIndex: number }[]backMatches?: { startIndex: number; endIndex: number }[]})[]

为什么要大费周章的记录这个索引呢,那是因为下一步需要用到,接下来说说关键词高亮的展示:

关键词高亮

搜索.gif

关键词高亮需要在字符串的某几个字符中更改它的样式,因此我们上一步才需要记录一下需要高亮的字符串开始和结束的位置,如此一来我们做这个高亮的组件就不用再执行一次匹配了。那么这个样式要如何实现呢,我们需要遍历这个字符串,在需要高亮的字增加额外的样式,最后再重新拼接成一个字符串。

// Highlight.vue
<template><view class="flex flex-wrap"><viewv-for="(charWithStyle, index) in styledText"class="text-sm":key="index":class="charWithStyle.isMatched ? 'text-indigo-500 font-semibold' : ''">{{ charWithStyle.char }}</view></view>
</template><script lang="ts" setup>
import { defineProps, computed } from 'vue'interface Props {text: stringmatches: { startIndex: number; endIndex: number }[]
}const props = defineProps<Props>()const styledText = computed(() => {const textArray = _.toArray(props.text)const returnArr = []let index = 0let arrIndex = 0while (index < props.text.length) {let char = ''if (textArray[arrIndex].length > 1) {char = textArray[arrIndex]} else {char = props.text[index]}const isMatched = props.matches.some((match) => {const endIndex = match.endIndexconst startIndex = match.startIndexreturn startIndex <= index && index < endIndex})returnArr.push({ char, isMatched })index += textArray[arrIndex].lengtharrIndex += 1}return returnArr
})
</script>

这里我没有使用 for of 直接遍历字符串,这也是我的一个踩坑点,像 emoji 表情这种字符它的长度其实不是 1,如果你直接使用 for of 去遍历会把它的结构给拆开,最终展示出来的是乱码,如果你想正常展示就要用 Array.from(props.text) 的方式将字符串转换成数组,再进行遍历,这样每个字符就都是完整的。

假设我们打印:

  console.log('😟'[0], '😟'.length)console.log(Array.from('😟')[0], Array.from('😟').length)

在这里插入图片描述

这里我没有使用 Array.from 而是使用了 lodash 中的 toArray,是因为看到这篇文章 https://fehey.com/emoji-length 中提到:

Array.from(props.text) 创建的数组 textArray 中的每个元素实际上是一个 UTF-16 代码单元的字符串表示,而不是完整的 Unicode 字符 Emoji 表情有可能是多个 Emoji + 一些额外的字符 来拼接出来的,像 ‘👩‍👩‍👧‍👧’ 就是由 [‘👩’, ‘’, ‘👩’, ‘’, ‘👧’, ‘’, ‘👧’] 拼接而成的,单个 Emoji 长度为 2,中间的连接字符长度为 1,故返回了 11。

如何获取 ‘👩‍👩‍👧‍👧’ 的长度为视觉的 1 呢,可以使用 lodash 的 toArray 方法,_.toArray(‘👩‍👩‍👧‍👧’).length = 1,其内部实现 了对 unicode 字符转换数组的兼容。

正是因为我们第一步中使用正则去匹配字符串的时候,是根据表情包字符实际的长度返回的索引值,所以我们这里有一个逻辑:

  let index = 0let arrIndex = 0while (index < props.text.length) {let char = ''if (textArray[arrIndex].length > 1) {char = textArray[arrIndex]} else {char = props.text[index]}
//,,,index += textArray[arrIndex].lengtharrIndex += 1}

如果字符的长度大于一我们就从字符串数组中取值,这样表情包就能正常展示了,然后维护两个索引,一个索引给字符长度大于1的字符用,一个给字符长度为1的用,根据不同的情况取不同的值,这样就能处理好表情包的这种情况了。下面这种很多个表情包的文本,也能在正确的位置高亮
请添加图片描述

滚动到指定位置并高亮

这一步就比较简单了,直接上代码:

import { getCurrentInstance } from 'vue'
const instance = getCurrentInstance()
const { safeAreaInsets } = uni.getWindowInfo()// 滚动到卡盒位置
const scrollToCardBox = (position: 'top' | 'bottom' = 'top') => {const query = uni.createSelectorQuery().in(instance.proxy)query.select(`#card-box-${props.cardBoxIndex}`).boundingClientRect((data) => {return data}).selectViewport().scrollOffset((data) => {return data}).exec((data) => {uni.pageScrollTo({scrollTop:data[1].scrollTop +data[0].top -safeAreaInsets.top +(position === 'top' ? 0 : data[0].height),duration: 200,})})
}

关于解析,在 🥲踩坑无数,如何用 uniapp 实现一个丝滑的英语学习卡盒小程序 这篇文章中有详细提到,这里不赘述了。

uni.pageScrollTo 有一个回调,可以用于滚动到指定位置后,执行某个函数,那么我们可以在这里设置触发高亮的动画,动画的实现如下:

<view//...:class="cardItemStore.scrollToCardItemId === props.cardItemData.id ? 'animation-after-search' : ''".animation-after-search {animation: vague 1s;animation-iteration-count: 2;
}@keyframes vague {0%,100% {box-shadow: inset 0 0 0 0 transparent; /* 初始和结束时没有阴影 */}50% {box-shadow: inset 0 0 0 2px #6366f1; /* 中间时刻显示阴影 */}
}

这里没有使用边框,而是使用了内嵌的阴影,避免边框会把容器撑大的问题,滚动完成后动态给指定元素一个执行动画的 class,动画触发完成后再移除 class 就 OK 了。效果如下:

滚动高亮.gif

总结

如果不是遇到了表情包长度问题,这个搜索功能的实现还是比较简单的,重点是交互和设计是否能够让用户快速定位到想找的内容。目前是纯前端实现,而且涉及了很多遍历,性能还有待提升,不过先实现再优化了。学习卡盒已经上线了,大家可以直接搜索到,这个搜索功能也发版了,欢迎体验。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/487806.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HarmonyOS 线性容器List 常用的几个方法

List底层通过单向链表实现&#xff0c;每个节点有一个指向后一个元素的引用。当需要查询元素时&#xff0c;必须从头遍历&#xff0c;插入、删除效率高&#xff0c;查询效率低。List允许元素为null。 List和LinkedList相比&#xff0c;LinkedList是双向链表&#xff0c;可以快速…

华为HarmonyOS NEXT 原生应用开发:页面路由、页面和组件生命周期函数

页面路由、组件生命周期 一、路由的基本使用 1. 如何新建页面 直接右键新建Page。【这个是最直接最常用的】新建普通ets文件&#xff0c;然后通过配置变成页面。 【该方法是遇到这种情况的解决方案】 2. 路由 - 页面之间的跳转 使用 **router.pushUrl&#xff08;{}&#xff…

二叉树概述

目录 一、二叉树的基本结构 二、二叉树的遍历 1.前序 2.中序 3.后序 4.层序遍历 三.计算二叉树的相关参数 1.计算节点总个数 2.计算叶子节点的个数 3.计算树的高度 4.计算第k层的子树个数 5.查找树中val为x的节点 四.刷题 1.单值二叉树 2.检查两棵树是否相同 3.一…

【从零开始入门unity游戏开发之——C#篇01】理论开篇

文章目录 前言前置条件什么是编程&#xff1f;什么是代码&#xff1f;什么是编程语言&#xff1f;常见的编程语言什么是C#&#xff1f;学习Unity为什么要先学习C#&#xff1f;选择适合自己的IDE集成开发环境VSCode安装和环境配置VSCode调试模式专栏推荐完结 前言 这个系列我想…

汽车总线协议分析-CAN总线

随着汽车工业的发展&#xff0c;汽车各系统的控制逐步向自动化和智能化转变&#xff0c;汽车电气系统变得日益复杂。许多车辆设计使用CAN、CAN-FD、LIN、FlexRay或SENT在电子控制单元(ECU)之间以及ECU与传感器&#xff0c;执行器和显示器之间进行通信。这些ECU之间的通信允许车…

SQL 获取今天的当月开始结束范围:

使用 GETDATE() 结合 DATEADD() 和 DATEDIFF() 函数来获取当前月的开始和结束时间范围。以下是实现当前月时间范围查询的 SQL&#xff1a; FDATE > DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0) FDATE < DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()) 1, 0) …

【Java若依框架】RuoYi-Vue的前端和后端配置步骤和启动步骤

&#x1f399;告诉你&#xff1a;Java是世界上最美好的语言 &#x1f48e;比较擅长的领域&#xff1a;前端开发 是的&#xff0c;我需要您的&#xff1a; &#x1f9e1;点赞❤️关注&#x1f499;收藏&#x1f49b; 是我持续下去的动力&#xff01; 目录 一. 作者有话说 …

【OpenCV】图像转换

理论 傅立叶变换用于分析各种滤波器的频率特性。对于图像&#xff0c;使用 2D离散傅里叶变换&#xff08;DFT&#xff09; 查找频域。快速算法称为 快速傅立叶变换&#xff08;FFT&#xff09; 用于计算DFT。 Numpy中的傅立叶变换 首先&#xff0c;我们将看到如何使用Numpy查…

集合ArrayList

黑马程序员Java的个人笔记 BV17F411T7Ao p111~p115 目录 集合存储数据类型的特点 创建对象 ArrayList 成员方法 .add 增加元素 .remove 删除元素 .set 修改元素 .get 查询元素 .size 获取长度 基本数据类型对应的包装类 Character 练习 返回多个数据 集合存储…

MVC基础——市场管理系统(三)Clean Architecture

文章目录 项目地址五、Clean Architecture5.1 user cage driven5.1.1创建CoreBusiness 5.2 创建UseCases5.2.1 创建CategoriesUseCases1. 创建VeiwCategoriesUseCase获取所有Cagegory 5.2.2. 实现ICategoryRepository接口3. 实现获取所有Category的方法4. 实现获取一个Cagegory…

GPT系列模型简要概述

GPT-1&#xff1a;&#xff08;0.117B参数量&#xff0c;0.8B words预训练数据) 动机&#xff1a; 在RNN和Transformer之间&#xff0c;选择了后者。 和《All your need is Attention》翻译模型的Encoder-Decoder架构相比&#xff0c;只保留Decoder&#xff0c;因此去掉了Cross…

关于信号隔离转换器

isolate converter是隔离转换器‌。它是一种在电子电路中用于实现电路隔离、电压转换或信号隔离的设备‌。隔离转换器能在很多场合发挥关键作用&#xff0c;比如可以保护电路、提高安全性&#xff0c;还能帮助不同电压或信号之间的转换与传递‌。 ‌一、产品概述‌ ‌简介‌&a…

C++初阶——模板初阶

目录 1、如何实现一个通用的交换函数 2、函数模板 2.1 函数模板的概念 2.2 函数模板的格式 2.3 函数模板的原理 2.4 函数模板的实例化 2.5 模板参数的匹配原则 3、类模板 3.1 类模板的格式 3.2 类模板的实例化 1、如何实现一个通用的交换函数 void Swap(int& lef…

Text2SQL(NL2sql)对话数据库:设计、实现细节与挑战

Text2SQL&#xff08;NL2sql&#xff09;对话数据库&#xff1a;设计、实现细节与挑战 前言1.何为Text2SQL&#xff08;NL2sql&#xff09;2.Text2SQL结构与挑战3.金融领域实际业务场景4.注意事项5.总结 前言 随着信息技术的迅猛发展&#xff0c;人机交互的方式也在不断演进。…

vmware vsphere5---部署vCSA(VMware vCenter Server)附带第二阶段安装报错解决方案

声明 因为这份文档我是边做边写的&#xff0c;遇到问题重新装了好几次所以IP会很乱 ESXI主机为192.168.20.10 VCSA为192.168.20.7&#xff0c;后台为192.168.20.7:5480 后期请自行对应&#xff0c;后面的192.168.20.57请对应192.168.20.7&#xff0c;或根据自己的来 第一阶段…

ElementUI:el-tabs 切换之前判断是否满足条件

<div class"table-card"><div class"card-steps-class"><el-tabsv-model"activeTabsIndex":before-leave"beforeHandleTabsClick"><el-tab-pane name"1" label"基础设置"><span slot&…

HarmonyOS(65) ArkUI FrameNode详解

Node 1、Node简介2、FrameNode2.1、创建和删除节点2.2、对FrameNode的增删改2.3、 FramNode的查询功能3、demo源码4、总结5、参考资料1、Node简介 在HarmonyOS(63) ArkUI 自定义占位组件NodeContainer介绍了自定义节点复用的原理(阅读本本篇博文之前,建议先读读这个),在No…

2024.12.5——攻防世界Training-WWW-Robots攻防世界baby_web

2024.12.5—攻防世界Training-WWW-Robots 知识点&#xff1a;robots协议 dirsearch工具 本题与第一道Robots协议十分类似&#xff0c;不做wp解析 大致步骤&#xff1a; step 1 打开靶机&#xff0c;发现是robots协议相关 step 2 用dirsearch进行扫描目录 step 3 url传参r…

vue使用百度富文本编辑器

1、安装 npm add vue-ueditor-wrap 或者 pnpm add vue-ueditor-wrap 进行安装 2、下载UEditor 官网&#xff1a;ueditor:rich text 富文本编辑器 - GitCode 整理好的&#xff1a;vue-ueditor: 百度编辑器JSP版 因为官方的我没用来&#xff0c;所以我自己找的另外的包…

Flask使用长连接(Connection会失效)、http的keep-alive、webSocket。---GPU的CUDA会内存不足报错

Flask Curl命令返回状态Connection: close转keep-alive的方法 使用waitress-serve启动 waitress-serve --listen0.0.0.0:6002 manage:app 使用Gunicorn命令启动 gunicorn -t 1000 -w 2 -b 0.0.0.0:6002 --worker-class gevent --limit-request-line 8190 manage:appFlask使用f…