当前位置: 首页 > news >正文

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

前言

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

搜索.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

总结

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

相关文章:

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

前言 最近在用自己的卡盒小程序的时候&#xff0c;发现卡片越来越多&#xff0c;有时候要找到某一张来看看笔记要找半天&#xff0c;于是自己做了一个搜索功能&#xff0c;先看效果&#xff1a; 怎么样&#xff0c;是不是还挺不错的&#xff0c;那么这篇文章就讲讲这样一个搜索…...

MATLAB中的合并分类数组

目录 创建分类数组 串联分类数组 创建具有不同类别的分类数组 串联具有不同类别的数组 分类数组的并集 此示例演示了如何合并两个分类数组。 创建分类数组 创建分类数组 A&#xff0c;其中包含教室 A 中的 25 个学生的首选午餐饮料。 rng(default) A randi(3,[25,1]); …...

ShardingSphere-JDBC

1. 什么是分库分表&#xff1f; 分库分表是一种数据库扩展技术&#xff0c;通过将数据拆分到多个数据库&#xff08;分库&#xff09;或多个表&#xff08;分表&#xff09;中来解决单一数据库或表带来的性能瓶颈。分库分表可以有效提升系统的可扩展性、性能和高并发处理能力&…...

企业如何选择远程控制软件来远程IT运维?

在当今企业的日常运作中&#xff0c;IT运维无疑是核心环节之一&#xff0c;它对于保持企业信息系统的稳定运行和数据安全至关重要。随着科技的快速进步&#xff0c;远程控制软件在IT运维中的应用变得越来越重要。今天&#xff0c;我们就来探讨一下远程控制软件如何助力企业IT运…...

Meta Llama 3.3 70B:性能卓越且成本效益的新选择

Meta Llama 3.3 70B&#xff1a;性能卓越且成本效益的新选择 引言 在人工智能领域&#xff0c;大型语言模型一直是研究和应用的热点。Meta公司最近发布了其最新的Llama系列模型——Llama 3.3 70B&#xff0c;这是一个具有70亿参数的生成式AI模型&#xff0c;它在性能上与4050…...

【银河麒麟高级服务器操作系统】修改容器中journal服务日志存储位置无效—分析及解决方案

了解更多银河麒麟操作系统全新产品&#xff0c;请点击访问 麒麟软件产品专区&#xff1a;https://product.kylinos.cn 开发者专区&#xff1a;https://developer.kylinos.cn 文档中心&#xff1a;https://documentkylinos.cn 服务器环境以及配置 【机型】 整机类型/架构&am…...

go语言zero框架对接阿里云消息队列MQ的rabbit的配置与调用

在 Go 语言中对接阿里云消息队列&#xff08;MQ&#xff09;的 RabbitMQ 配置与调用&#xff0c;首先需要安装和配置相关的 Go 库&#xff0c;并了解如何通过 RabbitMQ 与阿里云消息队列进行交互。 ### 步骤一&#xff1a;安装 RabbitMQ Go 客户端库 阿里云的消息队列&#x…...

《Vue进阶教程》第四课:reactive()函数详解

往期内容&#xff1a; 《Vue零基础入门教程》合集&#xff08;完结&#xff09; 《Vue进阶教程》第一课&#xff1a;什么是组合式API 《Vue进阶教程》第二课&#xff1a;为什么提出组合式API 《Vue进阶教程》第三课&#xff1a;Vue响应式原理 通过前面的学习, 我们了解到r…...

【开源】A065—基于SpringBoot的库存管理系统的设计与实现

&#x1f64a;作者简介&#xff1a;在校研究生&#xff0c;拥有计算机专业的研究生开发团队&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的网站项目。 代码可以查看项目链接获取⬇️&#xff0c;记得注明来意哦~&#x1f339; 赠送计算机毕业设计600个选题ex…...

memmove函数(带图详解)

c语言系列 文章目录 c语言系列一、memmove函数介绍1.1、函数基本功能1.2、函数参数2.3、函数返回值 二、memmove的使用2.1、拷贝字节不可大于目标空间2.2、同一空间拷贝 三、函数功能的模拟实现3.1、函数参数及其返回值的设定3.2、函数体实现 四、代码实现 一、memmove函数介绍…...

【Java数据结构】时间和空间复杂度

本章开始将进入数据结构的知识&#xff0c;时间复杂度主要衡量的是一个算法的运行速度&#xff0c;而空间复杂度主要衡量一个算法所需要的额外空间&#xff0c;。 时间复杂度 算法中执行的次数决定了时间复杂度。 在计算执行次数时&#xff0c;只需要计算大概的次数&#xff…...

八斗深度学习

八斗深度学习第二周笔记 一、深度学习步骤&#xff1a;1. 选定模型结构2. 模型参数随机初始化3. 构造模型损失函数4. 选择优化算法并设置超参数5. 数据准备与预处理6. 训练模型7. 模型评估8. 测试模型9. 应用模型 损失函数极小值、导向意义 超参数的影响迭代次数epoch批次量大小…...

安卓报错Switch Maven repository ‘maven‘....解决办法

例如&#xff1a;Switch Maven repository ‘maven(http://developer.huawei.com/repo/)’ to redirect to a secure protocol 在库链接上方添加配置代码&#xff1a;allowInsecureProtocol true...

Scala编程技巧:正则表达式与隐式转换

1. 引言 在Scala编程中&#xff0c;正则表达式和隐式转换是处理字符串匹配和类型转换的强大工具。本文将通过一个实用的示例——电话号码和身份证号码验证器&#xff0c;来展示如何使用这些工具。 2. 知识概括 2.1 正则表达式基础 正则表达式是用于字符串搜索和匹配的强大工…...

UnityShaderLab 实现黑白着色器效果

实现思路&#xff1a;取屏幕像素的RGB值&#xff0c;将三个通道的值相加&#xff0c;除以一个大于值使颜色值在0-1内&#xff0c;再乘上一个强度值调节黑白强度。 在URP中实现需要开启Opaque Texture ShaderGraph实现&#xff1a; ShaderLab实现&#xff1a; Shader "Bl…...

在Windows 10中使用SSH远程连接服务器(附花生壳操作方法)

SSH 在 linux 中是一种重要的系统组件&#xff0c;用户可以使用 SSH 来远程连接 linux 系统的计算机&#xff0c;或者传输文件。不过在 win10 以前&#xff0c;windows 并不原生支持 SSH&#xff0c;需要借助第三方工具来使用 SSH 功能。而实际上&#xff0c;微软在 2015 年就曾…...

在算网云平台云端在线部署stable diffusion (0基础小白超详细教程)

Stable Diffusion无疑是AIGC领域中的AI绘画利器&#xff0c;具有以下显著优势&#xff1a; 1、开源性质&#xff0c;支持本地部署 2、能够实现对图像生成过程的精确控制 虽然SD在使用上有很多的有点&#xff0c;但缺点也是不言而喻的&#xff0c;由于AI绘画的整个过程以及现…...

ubuntu存储空间不足快速解决

几个自己常用的释放空间命令&#xff0c;备忘 将文件夹下的文件按从大到小排列 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 清理系统日志文件&#xff1a; sudo journalctl --…...

Prescan simulink carsim联合仿真平台搭建问题总结

解决办法主要来自忠厚的老王&#xff1a;自动驾驶决策规划算法第二章第一节 决策规划仿真平台搭建_哔哩哔哩_bilibili 这部分直接复制的老王视频的&#xff1a; Q1:prescan安装了&#xff0c;但是找不到Demo_Carsim3D A1:这个文件夹是我自己建立的不是prescan自带的&#xff0…...

STM32(HAL_工程模板的搭建)

目录 一、准备文件 二、创建工程 三、创建分组 四、配置文件处理 五、编译错误处理 一、准备文件 准备HAL库文件: ST官网( 意法半导体-STMicroelectronics )搜索STM32Cube, 本文使用“STM32Cube_FW_F4_V1.24.1” 版本的HAL库, 使用的是F4的库文件。 创建文件&#xff1a…...

Java 语言特性(面试系列1)

一、面向对象编程 1. 封装&#xff08;Encapsulation&#xff09; 定义&#xff1a;将数据&#xff08;属性&#xff09;和操作数据的方法绑定在一起&#xff0c;通过访问控制符&#xff08;private、protected、public&#xff09;隐藏内部实现细节。示例&#xff1a; public …...

基于ASP.NET+ SQL Server实现(Web)医院信息管理系统

医院信息管理系统 1. 课程设计内容 在 visual studio 2017 平台上&#xff0c;开发一个“医院信息管理系统”Web 程序。 2. 课程设计目的 综合运用 c#.net 知识&#xff0c;在 vs 2017 平台上&#xff0c;进行 ASP.NET 应用程序和简易网站的开发&#xff1b;初步熟悉开发一…...

练习(含atoi的模拟实现,自定义类型等练习)

一、结构体大小的计算及位段 &#xff08;结构体大小计算及位段 详解请看&#xff1a;自定义类型&#xff1a;结构体进阶-CSDN博客&#xff09; 1.在32位系统环境&#xff0c;编译选项为4字节对齐&#xff0c;那么sizeof(A)和sizeof(B)是多少&#xff1f; #pragma pack(4)st…...

Java - Mysql数据类型对应

Mysql数据类型java数据类型备注整型INT/INTEGERint / java.lang.Integer–BIGINTlong/java.lang.Long–––浮点型FLOATfloat/java.lang.FloatDOUBLEdouble/java.lang.Double–DECIMAL/NUMERICjava.math.BigDecimal字符串型CHARjava.lang.String固定长度字符串VARCHARjava.lang…...

BCS 2025|百度副总裁陈洋:智能体在安全领域的应用实践

6月5日&#xff0c;2025全球数字经济大会数字安全主论坛暨北京网络安全大会在国家会议中心隆重开幕。百度副总裁陈洋受邀出席&#xff0c;并作《智能体在安全领域的应用实践》主题演讲&#xff0c;分享了在智能体在安全领域的突破性实践。他指出&#xff0c;百度通过将安全能力…...

【论文阅读28】-CNN-BiLSTM-Attention-(2024)

本文把滑坡位移序列拆开、筛优质因子&#xff0c;再用 CNN-BiLSTM-Attention 来动态预测每个子序列&#xff0c;最后重构出总位移&#xff0c;预测效果超越传统模型。 文章目录 1 引言2 方法2.1 位移时间序列加性模型2.2 变分模态分解 (VMD) 具体步骤2.3.1 样本熵&#xff08;S…...

AspectJ 在 Android 中的完整使用指南

一、环境配置&#xff08;Gradle 7.0 适配&#xff09; 1. 项目级 build.gradle // 注意&#xff1a;沪江插件已停更&#xff0c;推荐官方兼容方案 buildscript {dependencies {classpath org.aspectj:aspectjtools:1.9.9.1 // AspectJ 工具} } 2. 模块级 build.gradle plu…...

Mobile ALOHA全身模仿学习

一、题目 Mobile ALOHA&#xff1a;通过低成本全身远程操作学习双手移动操作 传统模仿学习&#xff08;Imitation Learning&#xff09;缺点&#xff1a;聚焦与桌面操作&#xff0c;缺乏通用任务所需的移动性和灵活性 本论文优点&#xff1a;&#xff08;1&#xff09;在ALOHA…...

Angular微前端架构:Module Federation + ngx-build-plus (Webpack)

以下是一个完整的 Angular 微前端示例&#xff0c;其中使用的是 Module Federation 和 npx-build-plus 实现了主应用&#xff08;Shell&#xff09;与子应用&#xff08;Remote&#xff09;的集成。 &#x1f6e0;️ 项目结构 angular-mf/ ├── shell-app/ # 主应用&…...

xmind转换为markdown

文章目录 解锁思维导图新姿势&#xff1a;将XMind转为结构化Markdown 一、认识Xmind结构二、核心转换流程详解1.解压XMind文件&#xff08;ZIP处理&#xff09;2.解析JSON数据结构3&#xff1a;递归转换树形结构4&#xff1a;Markdown层级生成逻辑 三、完整代码 解锁思维导图新…...