功能分析
- 点击搜素框进行路由页面跳转
- 当搜索框中内容为空时展示历史搜索记录,并且可以对历史搜索记录进行一键清空和删除
- 在搜索框中输入内容时可以根据输入的内容进行联想词汇搜索
- 当点击搜索或者联想的内容时要显示搜索出的结果
静态页面搭建
- 使用了vant组件库中的搜索组件,搭建搜索页面的搜索样式,利用list组件和cell单元格组件,搭建了搜索记录联想词汇和搜索结果的面板
控制页面显示功能
- 功能技术点:使用v-if属性控制什么时候显示合适的面板,当搜索框有内容时展示联想面板,然后定义一个变量isResultShow为false当点击搜索时改为true让搜索结果面板显示
<!-- 搜索结果 -->
<search-result v-if="isResult" />
<!-- /搜索结果 -->
<!-- 联想建议 -->
<search-suggestion v-else-if="searchText" />
<!-- /联想建议 -->
<!-- 搜索历史记录 -->
<search-history v-else />
<!-- /搜索历史记录 -->
联想词汇搜索功能
- 功能逻辑:当搜索框输入内容时候,请求加载联想建议的数据,然后将请求得到的结果绑定到模版中
- 功能技术点:这里需要把联想词汇搜索功能二次封装成组件,然后利用父传子的技术将父组件搜索框中的 内容传给联想词汇组件,并且在联想词汇组件中使用wacth属性监听父组件搜索框内容的变化,当内容变化时就在其中发起网络请求
watch: {
searchText: {
// 监视的处理函数
handler (val) {
this.loadSearchSuggestion(val)
},
// 首次监视触发
immediate: true
}
},
created () {},
mounted () {},
methods: {
async loadSearchSuggestion (q) {
try {
const { data } = await getSearchSuggestion(q)
this.suggestions = data.data.options
} catch {
this.$toast('获取失败')
}
}
}
- 功能技术点2:这里为了让用户在频繁输入内容时不重复的发送网络请求,所以做了防抖优化,使用了第三方的lodash包,调用了其中的debunce模块做防抖优化
// lodash 支持按需加载,有利于打包结果优化
import { debounce } from "lodash"
// debounce 函数
// 参数1:函数
// 参数2:防抖时间
// 返回值:防抖之后的函数,和参数1功能是一样的
handler: debounce(function (val) {
this.loadSearchSuggestion(val)
}, 1000)
- 功能技术点3:这里对用户搜索的关键字在联想到面板时做了高亮处理效果
- 实现逻辑:找到需要高亮的字符,然后使用模版字符串的方式拼接一个带有高亮类名的html标签,然后new RegExp构造函数,使用gi全局匹配的方式找到所有符合条件的字符,最后将符合条件的不高亮字符替换成高亮字符
/**
* 处理高亮文本
* 思路:
* 1. 想要在一个字符串中,将固定的字符特殊显示(改变颜色)
* 2. 那么就需要在这个字符串中,找出该字符,然后为该字符设置单独的样式(span.active)
* 拆解:
* 1. 找出字符
* 2. 替换字符
* 3. 设置单独的样式比较容易(替换字符),难点在于找出字符
* 如何找出字符:
* 1. 那么《处理高亮文本》的问题,就变成,《如何在字符串中找出固定的字符》
* 2. 在字符串中找出固定字符,大家首先想到的就应该是使用 -》 正则表达式
* 3. 简单使用正则(text.replace(/匹配的内容/gi, highlightStr)) , 无法插入响应式数据
* 4. 所以我们使用了 RegExp 对象。RegExp 构造函数创建了一个正则表达式对象,用于将文本与一个模式匹配。MDN: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/RegExp
* 5. 通过 RegExp 来完成响应式数据的正则匹配
*/
highlightText(text) {
const highlightStr = `<span class="active">${this.searchText}</span>`;
// 正则表达式 // 中间的内容都会当作匹配字符来使用,而不是数据变量
// 如果需要根据数据变量动态的创建正则表达式,则手动 new RegExp
// RegExp 正则表达式构造函数
// 参数1:匹配模式字符串,它会根据这个字符串创建正则对象
// 参数2:匹配模式,要写到字符串中
const reg = new RegExp(this.searchText, 'gi');
// text.replace(/匹配的内容/gi, highlightStr)
return text.replace(reg, highlightStr);
}
- 最后在联想列表模版中绑定使用,这里需要用到v-html标签绑定使用才可以使用
<!-- 联想建议 -->
<van-cell-group v-else-if="searchContent">
<van-cell
icon="search"
v-for="(item, index) in suggestions"
:key="index"
@click="onSearch(item)"
>
<div slot="title" v-html="highlight(item)"></div>
</van-cell>
</van-cell-group>
<!-- /联想建议 -->
搜索结果功能
- 功能实现逻辑:使用了vant组件库内置的搜索组件,该组件绑定了onSearch事件,当按下回车时触发搜索功能,这个时候需要把输入框里的值作为形参传递给定义好的searchText使用父传子的方式传给搜索结果组件,并在搜索结果组件中发送网络请求,请求数据并渲染页面
- 功能技术点:使用了vant组件库的list组件,该组件给数据的方式不是直接给空数组赋值,而是通过push的方式将数据添加进空数组,并且该组件的loading属性的控制是组件生效的关键,当触发事件时组件会自动将loading属性改为true,当数据添加玩后需要手动改回false否则会一直加载,当数据加载完成时需要将finish属性设置为true
methods: {
async onLoad () {
if(this.searchText == ""){
return this.loading = false
}
// 1. 请求获取数据
const { data } = await getSearch({
page: this.page, // 页码
per_page: this.perPage, // 每页大小
q: this.searchText // 搜索关键字
})
// 2. 将数据添加到列表中
const { results } = data.data
this.list.push(...results)
// 3. 设置加载状态结束
this.loading = false
// 4. 判断数据是否加载完毕
if (results.length) {
this.page++ // 更新获取下一页数据的页码
} else {
this.finished = true // 没有数据了,将加载状态设置结束,不再 onLoad
}
}
搜索历史记录功能
- 功能技术点1:这里需要在父组件中定义一个空数组来储存搜索的历史记录,但是由于历史记录不可重复,所以首先需要对该数组进行查重的判断,这里完使用了数组的indexOf方法,当数组中元素重复时会返回一个-1所以我只需要判断值为-1时将该元素从数组中删除即可,如果不为-1那就表示不重复,就可以正常添加数据
onSearch (val) {
// 更新文本框内容
this.searchText = val
// 存储搜索历史记录
// 要求:不要有重复历史记录、最新的排在最前面
const index = this.searchHistories.indexOf(val)
if (index !== -1) {
this.searchHistories.splice(index, 1)
}
this.searchHistories.unshift(val)
// 渲染搜索结果
this.isResultShow = true
},
- 功能技术点2:需要使用父传子的技术,将数据传给搜索历史记录的组件,在组件中完成渲染页面和删除的后续操作
<!-- 搜索历史 -->
<search-history v-else-if="!value" :searchHistory="searchHistory" />
props: {
searchHistory: {
type: Array,
required: true
}
}
- 功能技术点3:在删除操作前需要对按钮做出判断,当按钮为删除图标时点击变成完成,然后执行删除操作,当为完成状态时点击变为删除图标,不可执行删除操作,这里为定义了一个布尔变量,使用v-if属性控制面板的变化
<!-- 历史记录 -->
<van-cell-group v-else>
<van-cell title="历史记录">
<template v-if="isDeleteShow">
<span @click="searchHistories = []">全部删除</span>
<span @click="isDeleteShow = false">完成</span>
</template>
<van-icon v-else name="delete" @click="isDeleteShow = true"></van-icon>
</van-cell>
<van-cell
:title="item"
v-for="(item, index) in searchHistories"
:key="index"
@click="onSearch(item)"
>
<van-icon
v-show="isDeleteShow"
name="close"
@click="searchHistories.splice(index, 1)"
></van-icon>
</van-cell>
</van-cell-group>
<!-- /历史记录 -->
data () {
return {
...
isDeleteShow: false
}
}
- 删除单条数据:给数据绑定一个点击事件,当为可以删除状态时点击使用数组的splice方法删除对应数据,否则则使用$emit子传父的方式重新调用搜索的方法
onHistoryClick (item, index) {
// 如果是删除状态,则执行删除操作
if (this.isDeleteShow) {
this.searchHistory.splice(index, 1)
} else {
// 否则执行搜索操作
this.$emit('search', item)
}
}
2:删除全部历史记录:这里要注意,由于vue单项数据流的特性,所以如果给数组重新赋值,我们需要在父组件中操作,所以这里还是需要使用$emit的子传父方法,在点击事件调用时在父组件中将数组重新设置为空
<span @click="$emit('clear-search-history')" style="margin-right: 10px;">全部删除</span>
- 功能技术点4:最后需要做一个数据持久化的处理,需要使用vue的watch属性监听输入框中值的变化,当值变化时,使用本地存储将输入的值存储到本地中,并且在data中定义的数据从一开始就从本地中取值,就可以完成数据的持久化处理
watch: {
searchHistories (val) {
// 同步到本地存储
setItem('serach-histories', val)
}
},
data () {
return {
...
searchHistories: getItem('serach-histories') || [],
}
}