六、文章搜索
6.1 创建组件并配置路由
1、给搜索按钮添加to属性
<!-- 导航栏 -->
<van-nav-bar class="page-nav-bar" fixed>
<van-button
class="search-btn"
slot="title"
type="info"
size="small"
round
icon="search"
to="/search"
>搜索</van-button>
</van-nav-bar>
<!-- /导航栏 -->
1、创建 src/views/search/index.vue
<template>
<div class="search-container">搜索页面</div>
</template>
<script>
export default {
name: "SearchPage",
components: {},
props: {},
data() {
return {};
},
computed: {},
watch: {},
created() {},
methods: {}
};
</script>
<style scoped></style>
2、然后把搜索页面的路由配置到根组件路由(一级路由)
{
path: '/search',
component: Search
}
最后访问 /search
测试。
6.2 页面布局
6.2.1 搜索栏
1、搜索栏结构, vant组件搜索栏中的事件监听
<template>
<div class="search-container">
<!-- 搜索栏》事件监听 -->
<form action="/">
<van-search
v-model="value"
show-action
placeholder="请输入搜索关键词"
@search="onSearch"
@cancel="onCancel"
/>
</form>
</div>
</template>
<script>
export default {
name: 'search-container',
data () {
return {
value: ''
}
},
created () {
},
methods: {
onSearch (val) {
this.$toast(val)
},
onCancel () {
this.$toast('取消')
}
}
}
</script>
<style scoped lang='less'>
</style>
6.2.2 完成 - 搜索历史
1、创建 src/views/search/components/search-history.vue
<template>
<div class="search-history">
<van-cell title="搜索历史">
<span>全部删除</span>
<span>完成</span>
<van-icon name="delete" />
</van-cell>
<van-cell title="hello">
<van-icon name="close" />
</van-cell>
<van-cell title="hello">
<van-icon name="close" />
</van-cell>
<van-cell title="hello">
<van-icon name="close" />
</van-cell>
<van-cell title="hello">
<van-icon name="close" />
</van-cell>
</div>
</template>
<script>
export default {
name: 'SearchHistory',
components: {},
props: {},
data () {
return {}
},
computed: {},
watch: {},
created () {},
mounted () {},
methods: {}
}
</script>
<style scoped lang="less"></style>
6.2.3 完成 - 联想建议
2、创建 src/views/search/components/search-suggestion.vue
<template>
<div class="search-suggestion">
<van-cell title="程序员..." icon="search"></van-cell>
<van-cell title="程序员..." icon="search"></van-cell>
<van-cell title="程序员..." icon="search"></van-cell>
<van-cell title="程序员..." icon="search"></van-cell>
<van-cell title="程序员..." icon="search"></van-cell>
</div>
</template>
<script>
export default {
name: 'SearchSuggestion',
components: {},
props: {},
data () {
return {}
},
computed: {},
watch: {},
created () {},
mounted () {},
methods: {}
}
</script>
<style scoped lang="less"></style>
6.2.4 完成 - 搜索结果
3、创建 src/views/search/components/search-result.vue
<template>
<div class="search-result">
<van-list
v-model="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<van-cell v-for="item in list" :key="item" :title="item" />
</van-list>
</div>
</template>
<script>
export default {
name: 'SearchResult',
components: {},
props: {},
data () {
return {
list: [],
loading: false,
finished: false
}
},
computed: {},
watch: {},
created () {},
mounted () {},
methods: {
onLoad () {
// 异步更新数据
// setTimeout 仅做示例,真实场景中一般为 ajax 请求
setTimeout(() => {
for (let i = 0; i < 10; i++) {
this.list.push(this.list.length + 1)
}
// 加载状态结束
this.loading = false
// 数据全部加载完成
if (this.list.length >= 40) {
this.finished = true
}
}, 1000)
}
}
}
</script>
<style scoped lang="less"></style>
4、搜索组件内容如下:
<template>
<div class="search-container">
<!-- 搜索栏 -->
<!--
Tips: 在 van-search 外层增加 form 标签,且 action 不为空,即可在 iOS 输入法中显示搜索按钮
-->
<form action="/">
<van-search
v-model="searchText"
show-action
placeholder="请输入搜索关键词"
background="#3296fa"
@search="onSearch"
@cancel="onCancel"
/>
</form>
<!-- /搜索栏 -->
<!-- 搜索历史记录 -->
<search-history />
<!-- /搜索历史记录 -->
<!-- 联想建议 -->
<search-suggestion />
<!-- /联想建议 -->
<!-- 历史记录 -->
<search-result />
<!-- /历史记录 -->
</div>
</template>
<script>
import SearchHistory from './components/search-history'
import SearchSuggestion from './components/search-suggestion'
import SearchResult from './components/search-result'
export default {
name: 'SearchIndex',
components: {
SearchHistory,
SearchSuggestion,
SearchResult
},
props: {},
data () {
return {
searchText: ''
}
},
computed: {},
watch: {},
created () {},
mounted () {},
methods: {
onSearch (val) {
console.log(val)
},
onCancel () {
this.$router.back()
}
}
}
</script>
<style scoped lang="less">
.search-container {
.van-search__action {
color: #fff;
}
}
</style>
6.3 处理页面展示逻辑
1、在 data
中添加数据用来控制搜索结果的显示状态
data () {
...
isResultShow: false
}
2、在模板中绑定条件渲染
<!-- 搜索结果 -->
<search-result v-if="isResult" />
<!-- /搜索结果 -->
<!-- 联想建议 -->
<search-suggestion v-else-if="searchText" />
<!-- /联想建议 -->
<!-- 搜索历史记录 -->
<search-history v-else />
<!-- /搜索历史记录 -->
3、触发搜索,显示搜索结果
onSearch (val) {
this.isResult = true
}
4、获取焦点,隐藏搜索结果
<van-search
v-model="searchText"
show-action
placeholder="请输入搜索关键词"
@search="onSearch"
@cancel="onCancel"
@focus="isResult = false"
/>
6.4 搜索联想建议
基本思路:
- 当搜索框输入内容的时候,请求加载联想建议的数据
- 将请求得到的结果绑定到模板中
6.4.1 获取并监听输入框内容的变化
一、将父组件中搜索框输入的内容传给联想建议子组件
<search-suggestion :searchText="searchText" v-else></search-suggestion>
二、在子组件中监视搜索框输入内容的变化,如果变化则请求获取联想建议数据
props: {
searchText: {
type: String,
required: true
}
},
watch: {
searchText: {
// 监视的处理函数
handler (val) {
console.log(val)
},
// 首次监视触发
immediate: true
}
}
6.4.2 请求获取展示数据
一、将获取到的联想建议数据展示到列表中
- 定义获取建议的方法 search.js
import request from '@/utils/request'
/**
* 获取搜索联想建议
*/
export const getSearchSuggestion = q => {
return request({
method: 'GET',
url: '/app/v1_0/suggestion',
params: {
q
}
})
}
- 引入getSearchSuggestion
import { getSearchSuggestion } from '@/api/search.js'
- 定义加载数据的方法并调用getSearchSuggestion
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('获取失败')
}
}
}
- 模板渲染
<div class="search-suggestion">
<van-cell
v-for="(item, index) in suggestions"
:key="index"
:title="item"
icon="search"></van-cell>
</div>
6.4.3 防抖优化
1、安装 lodash
# yarn add lodash
npm i lodash
2、防抖处理
// lodash 支持按需加载,有利于打包结果优化
import { debounce } from "lodash"
不建议下面这样使用,因为这样会加载整个模块。
import _ from 'lodash' _.debounce()
// debounce 函数
// 参数1:函数
// 参数2:防抖时间
// 返回值:防抖之后的函数,和参数1功能是一样的
handler: debounce(function (val) {
this.loadSearchSuggestion(val)
}, 1000)
6.4.5 搜索关键字高亮
6.4.5.1 思路分析
如何将字符串中的指定字符在网页中高亮展示?
"Hello World";
将需要高亮的字符包裹 HTML 标签,为其单独设置颜色。
"Hello <span style="color: red">World</span>"
在 Vue 中如何渲染带有 HTML 标签的字符串?
data () {
return {
htmlStr: 'Hello <span style="color: red">World</span>'
}
}
<div>{{ htmlStr }}</div>
<div v-html="htmlStr"></div>
如何把字符串中指定字符统一替换为高亮(包裹了 HTML)的字符?
const str = "Hello World"
// 结果:<span style="color: red">Hello</span> World
"Hello World".replace('Hello', '<span style="color: red">Hello</span>')
// 需要注意的是,replace 方法的字符串匹配只能替换第1个满足的字符
// <span style="color: red">Hello</span> World Hello abc
"Hello World Hello abc".replace('Hello', '<span style="color: red">Hello</span>')
// 如果想要全文替换,使用正则表达式
// g 全局
// i 忽略大小写
// <span style="color: red">Hello</span> World <span style="color: red">Hello</span> abc
"Hello World Hello abc".replace(/Hello/gi, '<span style="color: red">Hello</span>')
一个小扩展:使用字符串的 split 结合数组的 join 方法实现高亮
var str = "hello world 你好 hello"; // ["", " world 你好 ", ""] const arr = str.split("hello"); // "<span>hello</span> world 你好 <span>hello</span>" arr.join("<span>hello</span>");
6.4.5.2 搜索关键字高亮
提前将文本设置为插槽
<van-cell
v-for="(item, index) in suggestions"
:key="index"
icon="search">
<span slot="title" v-html="item"></span>
</van-cell>
1、在 methods 中添加一个方法处理高亮
/**
* 处理高亮文本
* 思路:
* 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);
}
2、然后在联想建议列表项中绑定调用
<!-- 联想建议 -->
<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>
<!-- /联想建议 -->
6.5 搜索结果
思路:
- 找到数据接口
- 请求获取数据
- 将数据展示到模板中
6.5.1 点击建议,传递搜索内容
点击联想进行搜索
<van-cell
v-for="(item, index) in suggestions"
:key="index"
@click="$emit('search', item)"
icon="search">
<span slot="title" v-html="highlight(item)"></span>
</van-cell>
定义自定义事件和函数
<!-- 搜索联想 -->
<search-suggestion @search="onSearch" v-else :search-text="value" />
// 事件函数
onSearch (val) {
this.value = val
this.isShowSearchResult = true
}
6.5.2 处理完成
6.5.2.1 内容获取搜索关键字 - 处理完成
一、获取搜索关键字
1、声明接收父组件中的搜索框输入的内容
props: {
searchText: {
type: String,
require: true
}
}
2、在父组件给子组件传递数据
<!-- 搜索结果 -->
<search-result v-if="isResultShow" :searchText="searchText" />
<!-- /搜索结果 -->
最后在调试工具中查看确认是否接收到 props 数据。
二、请求获取数据
1、在 api/serach.js
添加封装获取搜索结果的请求方法
/**
* 获取搜索结果
*/
export function getSearch(params) {
return request({
method: "GET",
url: "/app/v1_0/search",
params
})
}
2、请求获取
+ import { getSearch } from '@/api/search'
export default {
name: 'SearchResult',
components: {},
props: {
q: {
type: String,
require: true
}
},
data () {
return {
list: [],
loading: false,
finished: false,
+ page: 1,
+ perPage: 20
}
},
computed: {},
watch: {},
created () {},
mounted () {},
methods: {
+++ async onLoad () {
// 1. 请求获取数据
const { data } = await getSearch({
page: this.page, // 页码
per_page: this.perPage, // 每页大小
q: this.q // 搜索关键字
})
// 2. 将数据添加到列表中
const { results } = data.data
this.list.push(...results)
// 3. 设置加载状态结束
this.loading = false
// 4. 判断数据是否加载完毕
if (results.length) {
this.page++ // 更新获取下一页数据的页码
} else {
this.finished = true // 没有数据了,将加载状态设置结束,不再 onLoad
}
}
}
}
三、最后,模板绑定
<van-list
v-model="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<van-cell
+ v-for="(article, index) in list"
+ :key="index"
+ :title="article.title"
/>
</van-list>
6.5.2.2 错误提示
<van-list
v-model="loading"
:finished="finished"
finished-text="没有更多了"
:error.sync="error"
error-text="请求失败,点击重新加载"
@load="onLoad"
>
<van-cell v-for="item in list" :key="item.art_id" :title="item.title" />
</van-list>
catch (err) {
this.error = true
this.loading = false
}
6.5.2.3 顶部固定
.search {
padding-top: 106px;
.van-search {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1;
}
.van-search__action {
color: #fff;
}
}
6.6 搜索历史记录
6.6.1 添加历史记录
当发生搜索的时候我们才需要记录历史记录。
1、在 data 中添加一个数据用来存储历史记录
data () {
return {
...
searchHistories: []
}
}
2、在触发搜索的时候,记录历史记录
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
},
6.6.2 展示历史记录
将数据传递给搜索历史组件
<!-- 搜索历史 -->
<search-history v-else-if="!value" :searchHistory="searchHistory" />
搜索历史组件接收数据
props: {
searchHistory: {
type: Array,
required: true
}
}
显示搜索历史
<!-- 历史记录 -->
<van-cell-group v-else>
<van-cell title="历史记录">
<van-icon name="delete" />
<span>全部删除</span>
<span>完成</span>
</van-cell>
<van-cell
:title="item"
v-for="(item, index) in searchHistory"
:key="index"
>
<van-icon name="close"></van-icon>
</van-cell>
</van-cell-group>
<!-- /历史记录 -->
6.6.3 删除历史记录
基本思路:
- 给历史记录中的每一项注册点击事件
- 在处理函数中判断
- 如果是删除状态,则执行删除操作
- 如果是非删除状态,则执行搜索操作
6.6.3.1 处理删除相关元素的展示状态
1、在 data 中添加一个数据用来控制删除相关元素的显示状态
data () {
return {
...
isDeleteShow: false
}
}
2、绑定使用
<!-- 历史记录 -->
<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>
<!-- /历史记录 -->
6.6.3.2 处理删除操作
<!-- 历史记录 -->
<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-cell>
<van-cell
:title="item"
v-for="(item, index) in searchHistories"
:key="index"
+ @click="onHistoryClick(item, index)"
>
<van-icon v-show="isDeleteShow" name="close"></van-icon>
</van-cell>
</van-cell-group>
<!-- /历史记录 -->
onHistoryClick (item, index) {
// 如果是删除状态,则执行删除操作
if (this.isDeleteShow) {
this.searchHistory.splice(index, 1)
} else {
// 否则执行搜索操作
}
}
删除全部
<van-cell title="搜索历史">
<div v-if="isDeleteShow">
<span @click="$emit('clear-search-history')" style="margin-right: 10px;">全部删除</span>
<span @click="isDeleteShow = false">完成</span>
</div>
<van-icon v-else @click="isDeleteShow = true" name="delete" />
</van-cell>
<van-cell
:title="item"
v-for="(item, index) in searchHistory"
:key="index"
@click="onHistoryClick(item, index)"
>
<van-icon v-if="isDeleteShow" name="close"></van-icon>
</van-cell>
<search-history @clear-search-history="searchHistories = []" :searchHistory="searchHistories" v-else-if="!searchText"></search-history>
点击搜索
onHistoryClick (item, index) {
// 如果是删除状态,则执行删除操作
if (this.isDeleteShow) {
this.searchHistory.splice(index, 1)
} else {
// 否则执行搜索操作
this.$emit('search', item)
}
}
<search-history @search="onSearch" @clear-search-history="searchHistories = []" :searchHistory="searchHistories" v-else-if="!searchText"></search-history>
6.6.4 数据持久化
1、利用 watch 监视统一存储数据
watch: {
searchHistories (val) {
// 同步到本地存储
setItem('serach-histories', val)
}
},
2、初始化的时候从本地存储获取数据
data () {
return {
...
searchHistories: getItem('serach-histories') || [],
}
}