使用Vue开发项目(黑马头条项目)--第六天

812 阅读2分钟

需要实现的主要功能如下:

资讯列表、标签页切换,文章举报,频道管理、文章详情、阅读记忆,关注功能、点赞功能、评论功能、回复评论、搜索功能、登录功能、个人中心、编辑资料、小智同学 ...

今天要实现的功能主要是:搜索功能

layout/layout.vue

0 实现根据用户是否登录显示我的和未登录功能

<van-tabbar route>
      <van-tabbar-item to="/" icon="home-o">
        首页
      </van-tabbar-item>
      <van-tabbar-item to="/question" icon="chat-o">
        问答
      </van-tabbar-item>
      <van-tabbar-item to="/video" icon="video-o">
        视频
      </van-tabbar-item>
      <van-tabbar-item to="/user" icon="search">
+        {{$store.state.tokenInfo.token ? '我的' : '未登陆' }}
      </van-tabbar-item>
    </van-tabbar>

1 搜索页面

1.1新建 src/views/search/search.vue页面

<template>
  <div>搜索页面</div>
</template>

<script>
export default {

}
</script>

<style lang="less" scoped>
</style>

1.2 补充路由配置

 
  const routes = [
  {
    path: '/login',
    name: 'login',
    component: Login
  },
  {
    path: '/',
    name: 'layout',
    component: Layout,
    // ....
  },
  {
+    path: '/search',
+    name: 'search',
+    component: () => import(/* webpackChunkName: "search" */ '../views/search/search.vue')
  }
]

1.3 设置路由跳转

src\views\layout\layout.vue中, 点击按钮实现路由跳转。

<!-- 顶部logo搜索导航区域 -->
    <van-nav-bar
      fixed
    >
      <div slot="left" class="logo"></div>
      <van-button
        slot="right"
        class="search-btn"
        round
        type="info"
        size="small"
        icon="search"
+        @click="$router.push('/search')"
        >
        搜索
      </van-button>
    </van-nav-bar>

1.4 直接在地址栏中测试http://localhost:8080/#/search

image.png

2搜索页面-组件布局

2.1 分析结构

从上到下,页面结构可以分成四部分

  • 导航区

  • 输入区

  • 智能提示区。联想建议

  • 搜索历史记录区

2.2布局

$router.back()表示后退的意思

<template>
  <div>
    <!-- nav-bar
      this.$router.push() : 路由跳转
      this.$router.back() : 路由后退  ===== 页面中的后退按钮
    -->
    <van-nav-bar title="搜索中心" left-arrow @click-left="$router.back()"></van-nav-bar>
    <!-- 1. 搜索区域 输入框 -->
    <van-search
      v-model.trim="keyword"
      show-action
      placeholder="请输入搜索关键词"
    >
    <!-- #action  ==== slot="action" -->
      <!-- <template slot="action">
        <div>搜索</div>
      </template> -->
      <div slot="action">搜索</div>
    </van-search>

    <!-- 2. 搜索建议 -->
    <van-cell-group>
      <van-cell title="单元格" icon="search"/>
      <van-cell title="单元格" icon="search"/>
      <van-cell title="单元格" icon="search"/>
    </van-cell-group>

    <!-- 3. 历史记录 -->
    <van-cell-group>
      <van-cell title="历史记录"/>

      <van-cell title="单元格">
        <van-icon name="close"></van-icon>
      </van-cell>

      <van-cell title="单元格">
        <van-icon name="close"></van-icon>
      </van-cell>
    </van-cell-group>
  </div>
</template>

<script>
export default {
  name: 'Search',
  data () {
    return {
      keyword: ''
    }
  }
}
</script>

2.3 效果如图

image.png

3 搜索页面-实现联想建议功能

用户在输入框中写入内容的同时,在输入框下方显示联想建议内容

3.1封装api

创建 api/search.js 并写入

// 用来封装所有与搜索操作相关的业务

import request from '../utils/request'

/**
 * 获取搜索建议
 * @param {*} keyword 搜索关键字
 * @returns
 */
export const getSuggestion = (keyword) => {
  return request({
    url: 'v1_0/suggestion',
    method: 'GET',
    params: {
      q: keyword
    }
  })
}

3.2当搜索内容变化时请求获取数据

<!-- 1. 搜索区域 输入框 -->
    <van-search
+     @input="hInput"
      v-model.trim="keyword"
      show-action
      placeholder="请输入搜索关键词"
    >
import { getSuggestion } from '@/api/search.js'
return {
   keyword: '',
+  suggestions: [] // 联想建议
}
// 用户在搜索框输入的内容发生了变化
    async hInput () {
      console.log(this.keyword)

      // 如果用户没有输入任何内容,则清空建议,不要发请求了
      if (this.keyword === '') {
        this.suggestions = []
        return
      }
      try {
       const { data: { data } } = await getSuggestion(this.keyword)
        this.suggestions = data.options
      } catch (err) {
        console.log(err)
      }
    },

3.3 数据渲染

<!-- 2. 搜索建议
     根据你在上面输入的关键字,后端会返回建议
    -->
    <van-cell-group>
			<van-cell
        v-for="(suggestion,idx) in suggestions"
        :key="idx"
        :title="suggestion"
        icon="search"
      />
    </van-cell-group>

4 搜索页面-高亮展示搜索关键字

由于原数据在后续操作中还会用到,所以,不能直接去修改原数据,而是应该产生一个副本

使用一个计算属性来保存高亮处理之后的建议项。

  • 在计算属性中,对原数据要做加工:用正则+replace对内容进行字符串替换,达成高亮处理的目标。

  • 用v-html显示数据。

4.1 封装辅助高亮函数

// 源字符串,要高亮的片段
export const heightLight = (str, key) => {
  const reg = new RegExp(key, 'ig')
  return str.replace(reg, (val) => {
    return `<span style="color:red">${val}</span>`
  })
}

4.2 添加计算属性

computed: {
  cSuggestions () {
      // this.suggestions中的每一项都去做替换
      // suggestions中的每一项   ====> heightLight(suggestions中的每一项, this.keyword)
      return this.suggestions.map(item => {
        return heightLight(item, this.keyword)
      })
    }
},

4.3 渲染

<!-- 2. 搜索建议
     根据你在上面输入的关键字,后端会返回建议
    -->
<van-cell-group>
  <van-cell
         v-for="(item,idx) in cSuggestions"
         :key="idx"
         icon="search">
    <div v-html="item"></div>
  </van-cell>
</van-cell-group>

4.4 查看效果

image.png

5搜索页面-显示搜索历史

用户在搜索某个关键字后,把它记录下来,以便后期快速搜索

1. 保存记录的格式和位置

格式:数组。例如:['a','手机','javascript']

2. 在如下两种情况下要保存记录:

  1. 在搜索框上点击搜索时保存

  2. 在系统给出的联想建议项上点击时保存

5.1搜索页面-添加搜索记录

data () {
    return {
      keyword: '',
+     historys: ['天津', '万达'],
      suggestions: []
    }
  },
<!-- 搜索历史记录 -->
<van-cell-group>
  <van-cell title="历史记录"></van-cell>
  <van-cell v-for="(item,idx) in historys"
            :key="idx"
            :title="item">
    <van-icon name="close" />
  </van-cell>
</van-cell-group>
<!-- /搜索历史记录 -->

封装添加历史记录的方法,方便之后调用

// 添加一条历史
addHistory (str) {
  this.historys.push(str)
  // todo: 
},

5.2 点击联想建议时

<van-cell-group>
  <van-cell
      v-for="(item,idx) in cSuggestions"
      :key="idx"
      icon="search"
+     @click="hClickSuggetion(idx)">
    <div v-html="item"></div>
  </van-cell>
</van-cell-group>
//  情况2: 用户点击了搜索建议
hClickSuggetion (idx) {
  // 1. 添加一条历史
  this.addHistory(this.suggestions[idx])
  // 2. 跳转到搜索结果页
}

5.3 点击 搜索按钮时

<div slot="action" @click="hSearch">搜索</div>
// 情况1:用户点击了搜索
hSearch () {
  // 1. 添加一条历史
  this.addHistory(this.keyword)
  // 2. 跳转到搜索结果页
  //  todo
},

5.4 检查所绑定的事件是否生效

图略

5.5 搜索页面-优化添加历史记录的方法

完善添加历史记录功能

在添加历史记录时,有如下两个注意事项:

  • 不能有重复项
  • 后加入要放在数组的最前面
// 添加一条历史
// 1. 不能有重复项
// 2. 后加入要放在数组的最前面
addHistory (str) {
  // (1) 找一下,是否有重复,如果有,就是删除
  const idx = this.history.findIndex(item => item === str)
  
  // idx !== -1 && this.historys.splice(idx, 1)
  if (idx > -1) {
    this.history.splice(idx, 1)
  }
  // (2) 加在数组的前面
  this.history.unshift(str)
}

6 搜索页面-删除历史记录

给用户提供删除历史记录的功能:在每条记录的后边都有一个关闭按钮,点击这个按钮就可以删除这条记录

6.1给X图标添加点击事件

<!-- 搜索历史记录 -->
<van-cell-group>
   <van-cell title="历史记录"></van-cell>
   <van-cell v-for="(item,idx) in historys"
            :key="idx"
            :title="item">
+       <van-icon name="close" @click="hDelHistory(idx)"/>
   </van-cell>
</van-cell-group>
<!-- /搜索历史记录 -->
// 用户点击了删除历史记录
hDelHistory (idx) {
  this.historys.splice(idx, 1)
}

6.2 搜索页面-搜索历史持久化

将历史记录保存到localstorage中

  • 封装一个用来持久化历史记录的模块(设置,删除)
  • 当搜索历史变化 (添加,删除)时保存一次
  • 在初始时使用引入本地数据(从localstorage中去取出数据)

6.2.1 创建utils/storageHistory.js

// 消除魔术字符串
const HISTORY_STR = 'HistoryInfo'

export const getHistory = () => {
  return JSON.parse(localStorage.getItem(HISTORY_STR))
}

export const setHistory = HistoryInfo => {
  localStorage.setItem(HISTORY_STR, JSON.stringify(HistoryInfo))
}

export const removeHistory = () => {
  localStorage.removeItem(HISTORY_STR)
}

6.2.2 引入并使用

`import { setHistory, getHistory } from '@/utils/storageHistory.js'`
data () {
    return {
      keyword: '', // 搜索关键字
      // 初始化,先从本地存储中取值,取不到,则用[]
      historys: getHistory()  || [], // 保存历史记录  ['正则', 'javascript']
      suggestions: [] // 当前的搜索建议
    }
  }
// 添加一条历史
    // 1. 不能有重复项
    // 2. 后加入要放在数组的最前面
    // 3. 持久化
    addHistory (str) {
      // (1) 找一下,是否有重复,如果有,就是删除
      const idx = this.history.findIndex(item => item === str)
      if (idx > -1) {
        this.historys.splice(idx, 1)
      }
      // (2) 加在数组的前面
      this.historys.unshift(str)

      // (3) 持久化
+     setHistory(this.history)
    },
// 用户点击了删除历史记录
    hDelHistory (idx) {
      this.historys.splice(idx, 1)
      // 持久化
+     setHistory(this.history)
    }

6.3 搜索页面-联想建议和历史记录的切换显示

联想建议 和 搜索历史 这两个区域是互斥的:

  • 如果当前开始去搜索内容,则不显示搜索历史,而显示联想建议。
  • 如果当前并没有搜索内容,则显示搜索搜索历史,不显示联想建议。
<!-- 联想建议
    v-html来正常显示html字符串效果-->
<!-- 2. 搜索建议
     根据你在上面输入的关键字,后端会返回建议

     v-if="suggestion.length":如果有搜索建议
    -->
<van-cell-group v-if="suggestions.length">
    ... 
</van-cell-group>
<!-- /联想建议 -->

<!-- 搜索历史记录 -->
<van-cell-group v-else>
    ... 
</van-cell-group>
<!-- /搜索历史记录 -->

7 防抖和节流功能

在输入框中字符变化会立刻发请求获取搜索建议。

  • 对用户来说,虽然能及时收到搜索建议,但在此搜索的过程,你录入的单词并没有写完,你得到的搜索建议多半是无用
  • 对服务器来说,调用这个接口的频率太高了,给服务器添加了负担。

7.1 实现防抖功能

// 用户的输入发生了变化
    hInput () {
      clearTimeout(this.timer)
      console.log(this.keyword)
      this.timer = setTimeout(() => {
        this.doAjax()
      }, 200)
    },
    async doAjax () {
      if (this.keyword === '') {
        this.suggestions = []
        return
      }

      try {
        const res = await getSuggestion(this.keyword)
        // console.log(res)
        this.suggestions = res.data.data.options
      } catch (err) {
        console.log(err)
      }
    },

7.2 实现节流功能

// 节流
    hInput () {
      const dt = Date.now() // 获取当前时间戳 ms为单位
      if (dt - this.startTime > 500) {
        this.doAjax()
        this.startTime = dt
      } else {
        console.log('当前的时间戳是', dt, '距离上一次执行不够500ms,所以不执行')
      }
    },
    async doAjax () {
      if (this.keyword === '') {
        this.suggestions = []
        return
      }

      try {
        const res = await getSuggestion(this.keyword)
        // console.log(res)
        this.suggestions = res.data.data.options
      } catch (err) {
        console.log(err)
      }
    }

7.3 节流和防抖只需二选一即可

8 搜索结果

搜索结果是单独在另一个页面显示的:

  • 路由转跳,并传入你要搜索的关键字
  • 收到关键字后,调接口
  • 取回查询结果,并显示。

8.1 创建组件

views/search/result.vue

<template>
  <div class="serach-result">
    <!-- 导航栏 -->
    <van-nav-bar
      title="xxx 的搜索结果"
      left-arrow
      fixed
      @click-left="$router.back()"
    />
    <!-- /导航栏 -->

    <!-- 文章列表 -->
    <van-list
      class="article-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',
  data () {
    return {
      list: [],
      loading: false,
      finished: false
    }
  },

  methods: {
    onLoad () {
      // 异步更新数据
      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
        }
      }, 500)
    }
  }
}
</script>

<style lang="less" scoped>
.serach-result {
  height: 100%;
  overflow: auto;
  .article-list {
    margin-top: 39px;
  }
}
</style>

8.2 创建路由

{
    path: '/searchResult',
    name: 'searchResult',
    component: () => import('../views/search/searchResult.vue')
  },

8.3 使用路由传参并跳转页面

// 情况1:用户点击了搜索
hSearch () {
  if (this.keyword === '') {
    return
  }
  // 1. 添加一条历史
  this.addHistory(this.keyword)
  // 2. 跳转到搜索结果页
+ this.$router.push('/search/result?keyword=' + this.keyword)
}

<!-- 2. 历史记录 -->
<van-cell-group v-else>
   <van-cell title="历史记录"/>
       <van-cell
           v-for="(item,idx) in history"
           :key="idx"
           :title="item"
+          @click="$router.push('/search/result?keyword=' + item)">
        <van-icon name="close" @click.stop="hDelHistory(idx)"></van-icon>
    </van-cell>
</van-cell-group>
//  情况3: 用户点击了搜索建议
hClickSuggetion (idx) {
  // 1. 添加一条历史
  this.addHistory(this.suggestion[idx])
  // 2. 跳转到搜索结果页
+ this.$router.push('/search/result?keyword=' + this.suggestions[idx])
}

9 搜索结果-获取查询结果并展示

search/result.vue 中,我们可以通过this.$route.query.keyword来获取传入的查询关键字

created () {
    var keyword = this.$route.query.keyword
    alert(keyword)
}

9.1 封装接口

api/serach.js 封装请求方法

/**
 * 获取查询结果
 * @param {*} keyword 关键字
 * @param {*} page 页码
 */
export const getSearchResult = (keyword, page) => {
  return request({
    method: 'GET',
    url: 'v1_0/search',
    params: {
      q: keyword,
      page: page
    }
  })
}

9.2 调用接口获取数据

import { getSearchResult } from '@/api/search.js'
export default {
  name: 'SearchResult',
  data () {
    return {
      list: [],
      page: 1, // 当前的页码
      loading: false,
      finished: false
    }
  }
// ---
async onLoad () {
      console.log(this.$route.query.keyword)
      // 1. 发请求
      const res = await getSearchResult(this.$route.query.keyword, this.page)
      const arr = res.data.data.results
      // 2. 数据回来之后,填充到list中
      this.list.push(...arr)
      // 3. 手动结束加载状态
      this.loading = false
      // 4. 判断是否还有更多数据
      this.finished = !arr.length
      // 5. 页码+1
      this.page++
    }

9.3 数据渲染

<template>
  <div class="serach-result">
    <!-- 导航栏 -->
    <van-nav-bar
      :title="`${$route.query.keyword} 的搜索结果`"
      left-arrow
      fixed
      @click-left="$router.back()"
    />
    <!-- /导航栏 -->

    <!-- 文章列表 -->
    <van-list
      class="article-list"
      v-model="loading"
      :finished="finished"
      finished-text="没有更多了"
      @load="onLoad"
    >
      <van-cell
        v-for="item in list"
        @click="$router.push('/article/' + item.art_id)"
        :key="item.art_id"
        :title="item.title"
      />
    </van-list>
    <!-- /文章列表 搜索到的结果-->
  </div>
</template>

9.4 查看效果

image.png