(15)城市选择页开发——⑤ “搜索框”逻辑实现 | Vue.js 项目实战: 移动端“旅游网站”开发

290 阅读4分钟
转载请注明出处,未经同意,不可修改文章内容。

🔥🔥🔥“前端一万小时”两大明星专栏——“从零基础到轻松就业”、“前端面试刷题”,已于本月大改版,合二为一,干货满满,欢迎点击公众号菜单栏各模块了解。

1 需求

在城市选择页的搜索框中,无论输入的是“拼音”还是“文字”,搜索框下方都会显示筛选出的包含对应“关键字”的城市。当没有对应城市时,则显示“没有找到匹配数据”:

视频01.gif

2 “搜索框”布局

需求分析:

当在搜索框输入内容后,搜索框下方显示“搜索”出的对应内容,城市列表的内容不见了:

但,城市列表的内容并不是“消失”了,它其实只是被“搜索内容”给遮挡住了。当进行搜索时,则显示对应搜索内容;当没有进行搜索时,就显示城市列表内容。

而进行搜索,却没有对应的搜索城市时,显示的“没有找到匹配数据”,其实也算是另一种对应的“搜索内容”。只要没有对应的城市,就显示“没有找到匹配数据”这一结果:

OK,理清思路,我们就可以进行代码的编写了。

1️⃣打开 city 下 components 中的 Search.vue

<template>
  <div> <!-- 1️⃣-④:添加一层 div 包裹所有的内容; -->
    
    <div class="search">
      <input class="search-input" type="text" placeholder="输入城市名或拼音">
    </div>
    <div class="search-content"> <!-- 1️⃣-①:添加一个类名为 search-content 的 div,
																它表示搜索内容区域,里边的 ul 标签中有两个类名为 search-item
																的 li 标签;-->
      <ul>
        <li class="search-item border-bottom"></li> <!-- 1️⃣-②:一个 li 标签用来循环搜索
																										到的对应数据; -->
                                                    <!-- 1️⃣-⑦:两个 li 标签都增加
                                                    border-bottom 类名,添加一像素下边
																										框。-->

        <li class="search-item border-bottom">
          没有找到匹配数据
        </li> <!--1️⃣-③:一个 li 标签的内容为“没有找到匹配数据”,作为没有对应搜索数据时
              显示的“搜索结果”; -->
      </ul>
    </div>
  </div>
</template>

<script>
export default {
  name: 'CitySearch'
}
</script>

<style lang="stylus" scoped>
@import '~styles/varibles.styl'
.search
  height: .72rem
  padding: 0 .1rem
  background: $bgColor
  .search-input
    box-sizing: border-box
    width: 100%
    padding: 0 .1rem
    height: .62rem
    line-height: .62rem
    color: #666
    text-align: center
    border-radius: .06rem

.search-content /*
  							1️⃣-⑤:.sear-content 绝对定位,距离顶部 1.58rem,空出头部和搜索框的距离;
  							z-index 设为 1,添加背景色 #eee;
  							❗️注意:.search-content 与 .search 平级。
  							 */
  overflow: hidden
  position: absolute
  top: 1.58rem
  left: 0
  right: 0
  bottom: 0
  z-index: 1
  background: #eee

  .search-item /*
  						 1️⃣-⑥:.search-item 的 padding-left 设为 0.2rem,line-height 0.62rem,
  						 背景色设为白色,字体颜色为 #666;
  							*/
    padding-left: .2rem
    line-height: .62rem
    background: #fff
    color: #666
</style>

保存后,返回页面查看,搜索内容区域显示在页面上,控制台无报错:

3 “搜索框”逻辑

🔗前置知识:
《JavaScript 初识——④ 流程控制语句》
《JavaScript 基础——JS 数组:② ES5 数组方法》
《Vue 初识——② 简易 TodoList》
《Vue 入门——⑦ 条件渲染》

2️⃣ Search.vue 要显示搜索的城市,就需要接收外部传来的城市数据。打开 city 下的 City.vue

<template>
  <div>
    <city-header></city-header>
    
    <city-search :cities="cities"></city-search> <!-- 2️⃣-①:通过属性 :cities 传递数据
																								 cities 给 Search.vue; -->

    <city-list :hot="hotCities" :cities="cities" :letter="letter"></city-list>
    <city-alphabet :cities="cities" @change="handleLetterChange"></city-alphabet>
  </div>
</template>

<script>
import axios from 'axios'
import CityHeader from './components/Header'
import CitySearch from './components/Search'
import CityList from './components/List'
import CityAlphabet from './components/Alphabet'

export default {
  name: 'City',
  components: {
    CityHeader,
    CitySearch,
    CityList,
    CityAlphabet
  },
  data () {
    return {
      hotCities: [],
      cities: {},
      letter: ''
    }
  },
  methods: {
    getCityInfo () {
      axios.get('/api/city.json')
        .then(this.getCityInfoSucc)
    },
    getCityInfoSucc (res) {
      res = res.data
      if (res.ret && res.data) {
        const data = res.data
        this.hotCities = data.hotCities
        this.cities = data.cities
      }
    },
    handleLetterChange (letter) {
      this.letter = letter
    }
  },
  mounted () {
    this.getCityInfo()
  }
}
</script>

<style></style>

2️⃣-②:打开 city 下 components 中的 Search.vue

<template>
  <div>
    <div class="search">
      <input
        class="search-input"
        type="text"
        placeholder="输入城市名或拼音"
        v-model="keyword"
      > <!-- 2️⃣-⑤:input 框上使用 v-model 指令与数据 keyword 进行双向绑定; -->

    </div>
    <div class="search-content">
      <ul>
        <li
          class="search-item border-bottom"
          v-for="item of list"
          :key="item.id"
        > <!-- 2️⃣-⑯:循环 list,绑定 key 值为每个循环项的 id; -->

          {{item.name}} <!-- 2️⃣-⑰:li 标签的内容为每一个循环项的 name(即包含 keyword 的
												城市名)。 -->
        </li>

        <li class="search-item border-bottom">
          没有找到匹配数据
        </li>
      </ul>
    </div>
  </div>
</template>

<script>
export default {
  name: 'CitySearch',
  props: { // 2️⃣-③:在 props 中接收 cities,它的类型为 Object;
    cities: Object
  },
  data () {
    return {
      keyword: '', /*
                   2️⃣-④:在 data 中定义一个变量 keyword,表示 input 框中输入的关键字,
                   默认为空;
                    */

      list: [], // 2️⃣-⑥:定义一个变量 list,用来放最终的搜索结果,它的值为空数组;

      timer: null // 2️⃣-⑦:定义一个变量 timer 默认为空,方便用节流函数提高代码执行效率;
    }
  },
  watch: {
    keyword () {
      if (this.timer) { // 2️⃣-⑧:如果 this.timer 已经存在,就清除 this.timer;
        clearTimeout(this.timer)
      }

      this.timer = setTimeout(() => { /*
                                      2️⃣-⑨:否则就创建一个 timer,让它延迟 100 毫秒执行
                                      箭头函数中的内容(即当 keyword 发生改变时,再让它延
                                      迟 100 毫秒执行);
                                       */

        const result = [] // 2️⃣-⑩:定义一个变量 result 为空数组;

        for (let i in this.cities) { // 2️⃣-⑪:使用 for...in 循环对象 cities;

          this.cities[i].forEach((value) => { /*
                                              2️⃣-⑫:遍历 cities 下的每一个数组,并接收
                                              一个参数 value(即对数据 cities 中的 A/B/C
                                              所对应的内容进行遍历,value 就是 A/B/C 中的
                                              每个城市的数据,包含 id、spell、name);
                                               */

            // 2️⃣-⑬:如果 value.spell 或 value.name 中能找到 keyword 对应的关键字;
            if (value.spell.indexOf(this.keyword) > -1 || value.name.indexOf(this.keyword) > -1) {

              result.push(value) // 2️⃣-⑭:就把这一项内容,添加到 result 中;
            }
          })
        }
        this.list = result /*
                           2️⃣-⑮:循环完成后,把结果 result 赋值给 list(即 list 中存储
                           了包含 keyword 的城市数据);
                            */
      }, 100)
    }
  }
}
</script>

<style lang="stylus" scoped>
@import '~styles/varibles.styl'
.search
  height: .72rem
  padding: 0 .1rem
  background: $bgColor
  .search-input
    box-sizing: border-box
    width: 100%
    padding: 0 .1rem
    height: .62rem
    line-height: .62rem
    color: #666
    text-align: center
    border-radius: .06rem
.search-content
  overflow: hidden
  position: absolute
  top: 1.58rem
  left: 0
  right: 0
  bottom: 0
  z-index: 1
  background: #eee
  .search-item
    padding-left: .2rem
    line-height: .62rem
    background: #fff
    color: #666
</style>

保存后,返回页面查看。当输入拼音或文字时,都能显示出对应的城市名。

但存在几个问题:

  1. “没有找到匹配数据”一直在页面显示;
  2. 搜索结束清空输入框后,页面没有清空“搜索内容”,且一直没有显示出城市列表的内容;
  3. 搜索出的结果过多时,不能滚动查看。

视频02.gif

❓如何解决这些问题呢?

答:控制页面内容“显示”与“隐藏”,可以使用 v-show 指令来解决。不能滚动的问题,我们依然可以通过 BetterScroll 来解决。

3️⃣返回 city 下 components 中的 Search.vue

<template>
  <div>
    <div class="search">
      <input
        class="search-input"
        type="text"
        placeholder="输入城市名或拼音"
        v-model="keyword"
      >
    </div>
    
    <div
      class="search-content"
      ref="search"
      v-show="keyword"
    > <!-- 3️⃣-②:给 .search-content 添加 ref 名为 search; -->
      <!-- 3️⃣-⑦:添加 v-show 指令,值为 keyword(即当 keyword 存在时,
			才显示搜索内容 .search-content)。 -->
      
      <ul>
        <li
          class="search-item border-bottom"
          v-for="item of list"
          :key="item.id"
        >
          {{item.name}}
        </li>

        <li
          class="search-item border-bottom"
          v-show="hasNoData"
        > <!-- 3️⃣-⑥:在第二个 li 标签上,添加 v-show 指令,hasNoData 为 true 时,
					显示“没有找到匹配数据”; -->
          
          没有找到匹配数据
        </li>
        
      </ul>
    </div>
  </div>
</template>

<script>
import BScroll from 'better-scroll' // 3️⃣-①:引入 BetterScroll;

export default {
  name: 'CitySearch',
  props: {
    cities: Object
  },
  data () {
    return {
      keyword: '',
      list: [],
      timer: null
    }
  },
  computed: { /*
  						3️⃣-⑤:在 computed 中添加一个计算属性 hasNoData(即 list 中没有内容、
    					搜索结果不存在时,hasNoData 为 true);
               */
    hasNoData () {
      return !this.list.length
    }
  },
  watch: {
    keyword () {
      if (this.timer) {
        clearTimeout(this.timer)
      }
      
      if (!this.keyword) { /*
      										 3️⃣-④:侦听 keyword 时,如果没有关键字,就让 list 变为
                           空数组后返回(即清空搜索的内容);
                            */
        this.list = []
        return
      }
      
      this.timer = setTimeout(() => {
        const result = []
        for (let i in this.cities) {
          this.cities[i].forEach((value) => {
            if (value.spell.indexOf(this.keyword) > -1 || value.name.indexOf(this.keyword) > -1) {
              result.push(value)
            }
          })
        }
        this.list = result
      }, 100)
    }
  },
  mounted () { // 3️⃣-③:在 mounted 中,创建一个 scroll,传入 search;
    this.scroll = new BScroll(this.$refs.search)
  }
}
</script>

<style lang="stylus" scoped>
@import '~styles/varibles.styl'
.search
  height: .72rem
  padding: 0 .1rem
  background: $bgColor
  .search-input
    box-sizing: border-box
    width: 100%
    padding: 0 .1rem
    height: .62rem
    line-height: .62rem
    color: #666
    text-align: center
    border-radius: .06rem
.search-content
  overflow: hidden
  position: absolute
  top: 1.58rem
  left: 0
  right: 0
  bottom: 0
  z-index: 1
  background: #eee
  .search-item
    padding-left: .2rem
    line-height: .62rem
    background: #fff
    color: #666
</style>

保存后,返回页面查看,刚才的问题都解决了,功能一切正常:

视频03.gif

以上,我们完成了城市选择页“搜索”部分的功能。

祝好,qdywxs ♥ you!