给我的小程序加了个丝滑的搜索功能,踩坑表情包长度问题
前言
最近在用自己的卡盒小程序的时候,发现卡片越来越多,有时候要找到某一张来看看笔记要找半天,于是自己做了一个搜索功能,先看效果:
怎么样,是不是还挺不错的,那么这篇文章就讲讲这样一个搜索展示的功能是如何实现的。
代码实现
首先我们分析这个搜索功能包含哪些需要实现的点:
- 输入关键字,匹配对应的卡片展示
- 匹配的卡片上面,要把符合搜索条件的所有词给高亮展示出来,不然文本多的时候容易看花了。
- 点击匹配到的卡片,要跳转到卡片的位置,并且闪烁两下,这样一看就知道要找卡片在哪了。
分析完成后,我们一点一点实现。
匹配关键字
我的小程序目前是纯前端搜索的,只是目前是这样,所以搜索逻辑也是在前端实现。搜索逻辑如果简单实现的话就是将搜索框的内容与列表中的每一项进行对比,看看内容中有没有包含这个字符串的,如果有就把这个项给返回回去。
先看代码:
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 }[]})[]
为什么要大费周章的记录这个索引呢,那是因为下一步需要用到,接下来说说关键词高亮的展示:
关键词高亮
关键词高亮需要在字符串的某几个字符中更改它的样式,因此我们上一步才需要记录一下需要高亮的字符串开始和结束的位置,如此一来我们做这个高亮的组件就不用再执行一次匹配了。那么这个样式要如何实现呢,我们需要遍历这个字符串,在需要高亮的字增加额外的样式,最后再重新拼接成一个字符串。
// 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 了。效果如下:
总结
如果不是遇到了表情包长度问题,这个搜索功能的实现还是比较简单的,重点是交互和设计是否能够让用户快速定位到想找的内容。目前是纯前端实现,而且涉及了很多遍历,性能还有待提升,不过先实现再优化了。学习卡盒已经上线了,大家可以直接搜索到,这个搜索功能也发版了,欢迎体验。
相关文章:
给我的小程序加了个丝滑的搜索功能,踩坑表情包长度问题
前言 最近在用自己的卡盒小程序的时候,发现卡片越来越多,有时候要找到某一张来看看笔记要找半天,于是自己做了一个搜索功能,先看效果: 怎么样,是不是还挺不错的,那么这篇文章就讲讲这样一个搜索…...
MATLAB中的合并分类数组
目录 创建分类数组 串联分类数组 创建具有不同类别的分类数组 串联具有不同类别的数组 分类数组的并集 此示例演示了如何合并两个分类数组。 创建分类数组 创建分类数组 A,其中包含教室 A 中的 25 个学生的首选午餐饮料。 rng(default) A randi(3,[25,1]); …...
ShardingSphere-JDBC
1. 什么是分库分表? 分库分表是一种数据库扩展技术,通过将数据拆分到多个数据库(分库)或多个表(分表)中来解决单一数据库或表带来的性能瓶颈。分库分表可以有效提升系统的可扩展性、性能和高并发处理能力&…...
企业如何选择远程控制软件来远程IT运维?
在当今企业的日常运作中,IT运维无疑是核心环节之一,它对于保持企业信息系统的稳定运行和数据安全至关重要。随着科技的快速进步,远程控制软件在IT运维中的应用变得越来越重要。今天,我们就来探讨一下远程控制软件如何助力企业IT运…...
Meta Llama 3.3 70B:性能卓越且成本效益的新选择
Meta Llama 3.3 70B:性能卓越且成本效益的新选择 引言 在人工智能领域,大型语言模型一直是研究和应用的热点。Meta公司最近发布了其最新的Llama系列模型——Llama 3.3 70B,这是一个具有70亿参数的生成式AI模型,它在性能上与4050…...
【银河麒麟高级服务器操作系统】修改容器中journal服务日志存储位置无效—分析及解决方案
了解更多银河麒麟操作系统全新产品,请点击访问 麒麟软件产品专区:https://product.kylinos.cn 开发者专区:https://developer.kylinos.cn 文档中心:https://documentkylinos.cn 服务器环境以及配置 【机型】 整机类型/架构&am…...
go语言zero框架对接阿里云消息队列MQ的rabbit的配置与调用
在 Go 语言中对接阿里云消息队列(MQ)的 RabbitMQ 配置与调用,首先需要安装和配置相关的 Go 库,并了解如何通过 RabbitMQ 与阿里云消息队列进行交互。 ### 步骤一:安装 RabbitMQ Go 客户端库 阿里云的消息队列&#x…...
《Vue进阶教程》第四课:reactive()函数详解
往期内容: 《Vue零基础入门教程》合集(完结) 《Vue进阶教程》第一课:什么是组合式API 《Vue进阶教程》第二课:为什么提出组合式API 《Vue进阶教程》第三课:Vue响应式原理 通过前面的学习, 我们了解到r…...
【开源】A065—基于SpringBoot的库存管理系统的设计与实现
🙊作者简介:在校研究生,拥有计算机专业的研究生开发团队,分享技术代码帮助学生学习,独立完成自己的网站项目。 代码可以查看项目链接获取⬇️,记得注明来意哦~🌹 赠送计算机毕业设计600个选题ex…...
memmove函数(带图详解)
c语言系列 文章目录 c语言系列一、memmove函数介绍1.1、函数基本功能1.2、函数参数2.3、函数返回值 二、memmove的使用2.1、拷贝字节不可大于目标空间2.2、同一空间拷贝 三、函数功能的模拟实现3.1、函数参数及其返回值的设定3.2、函数体实现 四、代码实现 一、memmove函数介绍…...
【Java数据结构】时间和空间复杂度
本章开始将进入数据结构的知识,时间复杂度主要衡量的是一个算法的运行速度,而空间复杂度主要衡量一个算法所需要的额外空间,。 时间复杂度 算法中执行的次数决定了时间复杂度。 在计算执行次数时,只需要计算大概的次数ÿ…...
八斗深度学习
八斗深度学习第二周笔记 一、深度学习步骤:1. 选定模型结构2. 模型参数随机初始化3. 构造模型损失函数4. 选择优化算法并设置超参数5. 数据准备与预处理6. 训练模型7. 模型评估8. 测试模型9. 应用模型 损失函数极小值、导向意义 超参数的影响迭代次数epoch批次量大小…...
安卓报错Switch Maven repository ‘maven‘....解决办法
例如:Switch Maven repository ‘maven(http://developer.huawei.com/repo/)’ to redirect to a secure protocol 在库链接上方添加配置代码:allowInsecureProtocol true...
Scala编程技巧:正则表达式与隐式转换
1. 引言 在Scala编程中,正则表达式和隐式转换是处理字符串匹配和类型转换的强大工具。本文将通过一个实用的示例——电话号码和身份证号码验证器,来展示如何使用这些工具。 2. 知识概括 2.1 正则表达式基础 正则表达式是用于字符串搜索和匹配的强大工…...
UnityShaderLab 实现黑白着色器效果
实现思路:取屏幕像素的RGB值,将三个通道的值相加,除以一个大于值使颜色值在0-1内,再乘上一个强度值调节黑白强度。 在URP中实现需要开启Opaque Texture ShaderGraph实现: ShaderLab实现: Shader "Bl…...
在Windows 10中使用SSH远程连接服务器(附花生壳操作方法)
SSH 在 linux 中是一种重要的系统组件,用户可以使用 SSH 来远程连接 linux 系统的计算机,或者传输文件。不过在 win10 以前,windows 并不原生支持 SSH,需要借助第三方工具来使用 SSH 功能。而实际上,微软在 2015 年就曾…...
在算网云平台云端在线部署stable diffusion (0基础小白超详细教程)
Stable Diffusion无疑是AIGC领域中的AI绘画利器,具有以下显著优势: 1、开源性质,支持本地部署 2、能够实现对图像生成过程的精确控制 虽然SD在使用上有很多的有点,但缺点也是不言而喻的,由于AI绘画的整个过程以及现…...
ubuntu存储空间不足快速解决
几个自己常用的释放空间命令,备忘 将文件夹下的文件按从大到小排列 ls -lhS /var/log/syslog 过大 sudo truncate -s 0 /var/log/syslog /var/log/Xorg.0.log.old过大 sudo truncate -s 0 /var/log/Xorg.0.log.old 清理系统日志文件: sudo journalctl --…...
Prescan simulink carsim联合仿真平台搭建问题总结
解决办法主要来自忠厚的老王:自动驾驶决策规划算法第二章第一节 决策规划仿真平台搭建_哔哩哔哩_bilibili 这部分直接复制的老王视频的: Q1:prescan安装了,但是找不到Demo_Carsim3D A1:这个文件夹是我自己建立的不是prescan自带的࿰…...
STM32(HAL_工程模板的搭建)
目录 一、准备文件 二、创建工程 三、创建分组 四、配置文件处理 五、编译错误处理 一、准备文件 准备HAL库文件: ST官网( 意法半导体-STMicroelectronics )搜索STM32Cube, 本文使用“STM32Cube_FW_F4_V1.24.1” 版本的HAL库, 使用的是F4的库文件。 创建文件:…...
Flask入门一(介绍、Flask安装、Flask运行方式及使用、虚拟环境、调试模式、配置文件、路由系统)
文章目录 一、Flask介绍二、Flask创建和运行 1.安装2.快速使用3.Flask小知识4.flask的运行方式 三、Werkzeug介绍四、Jinja2介绍五、Click CLI 介绍六、Flask安装 介绍watchdog使用python–dotenv使用(操作环境变量) 七、虚拟环境 介绍Mac/linux创建虚拟…...
CAD C# 批量替换当前图中块
本案例功能为选择当前文档中一个块(旧块),然后选择新图元(新块),运行插件后新块将替换图中所有的旧块。 效果如下: public static class Class1{//选取对象替换块定义[CommandMethod("TT&…...
Android -- [SelfView] 自定义多行歌词滚动显示器
Android – [SelfView] 自定义多行歌词滚动显示器 流畅、丝滑的滚动歌词控件* 1. 背景透明;* 2. 外部可控制进度变化;* 3. 支持屏幕拖动调节进度(回调给外部);效果 歌词文件(.lrc) 一. 使用…...
vscode 配置C/C++环境控制台参数
您可以通过以下步骤在VS Code中配置C/C环境的控制台参数: 1,打开VS Code并进入您的C/C项目 2,点击左侧的"调试"图标,然后点击顶部的齿轮图标,选择“launch.json”。 3,在"launch.json&qu…...
【HarmonyOS学习日志(13)】计算机网络之TCP/IP协议族(二)
文章目录 TCP/IP协议族ARPDNS标志字段:协商具体的通信方式和反馈通信状态DNS查询问题的格式资源记录(Resource Record, RR)格式:被用于应答字段、授权字段和额外信息字段 IP协议IP服务的特点无状态无连接不可靠 IP头部结构IPv4头部…...
多系统对接的实现方案技术分析
前言 随着信息化和大数据时代的到来,数据资产变得至关重要,企业纷纷上线多种软件系统和移动端应用以适应这一变化。这些系统和应用虽然发挥了各自的优势,但也导致了信息孤岛问题。为了解决这一问题,数据中台和异构系统集成技术应…...
kv类型算子使用
对kv类型的RDD数据集进行操作。 keys """ 获取所有的key转换算子"""inputRdd sc.parallelize([(laoda, 11), (laoer, 22), (laosan, 33), (laosi, 44)]) print(inputRdd.keys().collect()) # [laoda, laoer, laosan, laosi] values "&…...
3维建模blender
官网稳定版下载:https://www.blender.org/download/lts/ windows有安装版和portable版 教程:https://www.bilibili.com/video/BV1kX4y1m7G5 1. 基础操作 场景操作 场景位移:shift鼠标中键长按场景旋转:鼠标中键长按场景缩放&…...
百问FB网络编程 - UDP编程简单示例
6.5 UDP编程简单示例 UDP服务器首先进行初始化操作:调用函数socket创建一个数据报类型的套接字,函数bind将这个套接字与服务器的公认地址绑定在一起。然后调用函数recvfrom接收UDP客户机的数据报。UDP客户机首先调用函数socket创建一个数据报套接字&…...
面试题:什么是ThreadLocal,如何实现的?
强烈推荐 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站:人工智能 你是否还在为简历无人阅读而感到沮丧?是否因为寻觅不到理想的工作机会而感到焦虑不安?试试:看看…...
做外贸常用的网站有哪些/谷歌优化排名怎么做
SAP CO模块 KSV5 费用分摊分配解析 2018年07月18日 13:22:40 SAP小菜鸟鸟 阅读数:2073 CO成本控制是SAP财务的一大难点,有些地方可能会比较绕,但是捋顺思路,其实也还行。 (文中所有数据来源皆为作者杜撰,请勿对号入…...
做网站用什么比较好/网络优化工程师是做什么的
消息传递机制: 在OC中,方法的调用不再理解为对象调用其方法,而是要理解成对象接收消息,消息的发送采用‘动态绑定’机制,具体会调用哪个方法直到运行时才能确定,确定后才会去执行绑定的代码。方法的调用实际就是告诉对…...
做网站片头的软件/武汉百度推广开户
github地址:https://github.com/cheesezh/python_design_patterns 适配器模式 适配器模式,将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作[DP]。 当系统的数据和行为都正确,但是接口…...
网站建设是属于软件开发费吗/日本网络ip地址域名
随时随地阅读更多技术实战干货,获取项目源码、学习资料,请关注源代码社区公众号(ydmsq666) 布局资源是Android中最常用的一种资源,Android可以将屏幕中组件的布局方式定义在一个XML中,这有点像Web开发中的HTML页面。我们可以调用A…...
wordpress解释/企业网站的网络营销功能
以华为mate30为例,版本系统为EMUI10,其华为系统恢复获取安装包信息失败的原因如下:1、此情况可能是下载的软件安装包不完整,建议您在网络稳定的情况下,重新下载安装。2、查看手机内存是否充足。3、检查其他软件是否可以…...
python做后台开发移动网站/学计算机哪个培训机构好
背景: 在地图上绘制大量的circleMarker,leaflet能选择使用canvas来渲染,比起默认的svg渲染来说在大量绘制的情况下会更加流畅。但当触发其中某一个circleMarker的tooltip或popup时,浏览器报错“Uncaught RangeError: Maximum call…...