Vue实现移动端侧边栏城市联动功能

13 篇文章 1 订阅
2 篇文章 0 订阅

前言

城市联动功能在业务比较常见,一般用于用户自行搜索或者选择其所在地,然后根据地点获取有关当地的一些推荐信息,比如附近商家、景点、娱乐等推荐,引导用户的出行和消费行为。

效果预览

其实,只要换一下数据结构,它也可以变成一个类似于手机通讯录列表,那个圆形气泡联动跟随,就是模仿 魅族 的手机通讯录上下滑动侧边栏时,指示当前字母的效果。
在这里插入图片描述

功能简介

可以看到,接下来要实现的功能也并不是很复杂,主要包括:

  • 城市搜索,按城市名称或者拼音进行模糊匹配搜索
  • 城市列表,从上到下按城市拼音首字母分块排序显示
  • 右侧城市首字母导航栏,支持点击和上下滑动字母联动左侧城市列表对应显示,加上当前字母圆形气泡跟随显示
  • 选择城市之后,缓存到浏览器localStorage缓存中,保持当前城市的显示状态

项目结构

这里为了方便,我使用了 Vue-cli4 创建项目,项目结构如下:

  • mock 存放了一份城市列表的json数据,页面数据就靠它了,格式如下:
{
  "A": [
    {
      "id": 56,
      "spell": "aba",
      "name": "阿坝"
    },
    {
      "id": 57,
      "spell": "akesu",
      "name": "阿克苏"
    },
    ...
   ],
 "B": [
    {
      "id": 1,
      "spell": "beijing",
      "name": "北京"
    },
    {
      "id": 66,
      "spell": "baicheng",
      "name": "白城"
    },
    ...
   ],
   ...
}

题外话,你拿到的数据不一定就是这个格式,而且也没有按字母分类,而是一份偏平化的城市列表数组,那你可以通过安装 pinyinjs这个库来将城市名转化为拼音,再截取拼音首字母,最后再按字母分类分组。

  • router 存放路由配置,因为此时只有一个页面,所以简单的配置如下:
import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'city',
      component: () => import('@/views/city/City.vue')
    }
  ]
})

你也可以不使用路由,那就直接在 App.vue 中引入写好的 City.vue 组件好了。

  • store 状态管理配置,里面只定义了一个当前城市的属性 currentCity,用于 List 和 Search 组件共享操作,或者也可以使用 this.$emit 来通知父组件更新。
  • utils 里面封装了操作 localStorage 的方法,在 store 配置中引用。
  • views 就是用来存放页面和组件的了,每一个页面对应一个自己命名的文件夹,在这里 city 文件夹中创建 City.vue 页面和其组件目录,根据功能简介来分类,我们可以划分为三个子组件:
    • Search.vue 城市搜索组件
    • LIst.vue 城市列表组件
    • Alphabet.vue 城市首字母导航组件

开发依赖

  • vue-router
  • vuex
  • better-scroll
    首先单个页面不能一次性显示所有的城市列表,需要根须用户搜索、点击或者滑动的选择字母来显示对应的局部列表,这里使用了 better-scroll 滚动插件,它是一个移动端滚动的解决方案,比较好用。
  • axios
    模拟后端请求数据。
  • stylusstylus-loader
    css样式和css预处理,这样可以使用模块化的方法去写css代码。

功能实现

简单的配置完成,接下来我们就可以开始 ‘coding’ 功能实现了,但首先还需要来分析下每个组件需要实现什么功能和需要哪些数据:

  1. 对于 Serach 组件来说,搜索匹配和搜索结果列表都是在它上面实现的,所以只需要传入城市列表数据即可,当选中某个城市时,需要更新 currentCity;
  2. List 组件,因为要显示城市列表和跟随右侧导航栏选中的字母来联动显示,所以它需要传入城市列表数据还有当前选中城市的首字母,然后选中某个城市时,也需要更新 currentCity;
  3. Alphabet 组件, 在点击或者滑动时,需要通知 List 组件当前被选中的城市首字母,显示并更新圆形气泡显示的字母和在垂直方向上位置实现联动跟随,然后城市列表也滚动到指定的位置。

经过以上分析,可以编写出 City.vue 页面文件:
分别引入 Search、List、Alphabet 三个组件和传入它们对应需要的数据。

<template>
  <div>
    <search :citiesList="citiesList"/>
    <list
      :citiesList="citiesList"
      :letter="letter"
    />
    <alphabet @change="letterSelect" />
  </div>
</template>

<script>
import Search from './components/Search'
import List from './components/List'
import Alphabet from './components/Alphabet'
export default {
  name: 'City',
  data () {
    return {
      letter: '',
      citiesList: {}
    }
  },
  created () {
    this.getCityInfo()
  },
  methods: {
    getCityInfo () {
      this.$axios.get('./mock/cities.json')
        .then(res => {
          const { status, data } = res
          if (status === 200 && data) {
            this.citiesList = data
          }
        })
        .catch(err => {
          console.log('getCityInfo -> err', err)
        })
    },
    letterSelect (letter) {
      this.letter = letter
    }
  },
  components: {
    Search,
    List,
    Alphabet
  }
}
</script>

搜索组件 Search.vue:

  1. 使用 watch 监听用户的输入变化,根据输入值从城市列表数据中返回符合模糊匹配的结果,再以下拉列表形式显示出来;
  2. 用户点击列表中的某个城市,通过 vuex 的 mapActions 辅助函数异步更新 store 中 currentCity 的值,然后隐藏搜索列表并更新当前城市;
  3. 当没有匹配数据时,加了个提示告诉用户没有找到他想要查找的数据;
  4. 需要注意的是处理这种用户输入监听的操作,会频繁触发请求,所以有必要加个 防抖 来减少请求的次数。
<template>
  <div>
    <div class="search">
      <input
        v-model="keyword"
        class="search-input"
        type="text"
        placeholder="输入城市名或拼音"
      />
    </div>
    <div class="search-content" ref="search" v-show="keyword">
      <ul>
        <li
          class="search-item border-bottom"
          v-for="item of searchList"
          :key="item.id"
          @click="handleCityClick(item.name)"
        >
          {{ item.name }}
        </li>
        <li class="search-item border-bottom" v-show="hasNoData">
          您输入的内容没有匹配项
        </li>
      </ul>
    </div>
  </div>
</template>

<script>
import { mapActions } from 'vuex'
import BScroll from 'better-scroll'
export default {
  name: 'Search',
  props: {
    citiesList: {
      type: Object,
      default: () => { }
    }
  },
  data () {
    return {
      keyword: '',
      timer: null,
      searchList: []
    }
  },
  computed: {
    hasNoData () {
      return !this.searchList.length
    }
  },
  watch: {
    keyword () {
      if (this.timer) {
        clearTimeout(this.timer)
      }
      this.timer = setTimeout(() => {
        const result = []
        if (this.keyword.trim().length) {
          for (const i in this.citiesList) {
            this.citiesList[i].forEach(item => {
              if (item.spell.indexOf(this.keyword) > -1 || item.name.indexOf(this.keyword) > -1) {
                result.push(item)
              }
            })
          }
        }
        this.searchList = result
      }, 200)
    }
  },
  mounted () {
    this.scroll = new BScroll(this.$refs.search, {
      click: true
    })
  },
  methods: {
    handleCityClick (city) {
      this.keyword = ''
      this.changeCity(city)
    },
    ...mapActions(['changeCity'])
  }
}
</script>

</style>
<style lang="stylus" scoped>
// 样式省略...
</style>

城市列表组件 List.vue:

  1. 首次加载显示默认的或者上次选择的当前城市;
  2. 根据用户点击或者滑动右侧字母导航栏的字母,使用 watch 监听 letter 变化,然后滚动到指定字母的城市列表;
  3. 用户在列表上点击某个城市,更新当前城市并滚动返回页面最上方。
<template>
  <div>
    <div class="current-city">
      <div class="title">当前城市</div>
      <div class="button-wrapper">
        <div class="button">{{ currentCity }}</div>
      </div>
    </div>
    <div class="city-list" ref="wrapper">
      <div>
        <div
          class="area"
          v-for="(items, key) of citiesList"
          :key="key"
          :ref="key"
        >
          <div class="title border-topbottom">{{ key }}</div>
          <ul class="item-list">
            <li
              class="item border-bottom"
              v-for="item of items"
              :key="item.id"
              @click="handleCityClick(item.name)"
            >
              {{ item.name }}
            </li>
          </ul>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { mapState, mapActions } from 'vuex'
import BScroll from 'better-scroll'
export default {
  name: 'List',
  props: {
    letter: {
      type: String
    },
    citiesList: {
      type: Object,
      default: () => { }
    }
  },
  mounted () {
    this.scroll = new BScroll(this.$refs.wrapper, {
      click: true
    })
  },
  computed: {
    ...mapState({
      currentCity: 'city'
    })
  },
  watch: {
    letter () {
      if (this.letter) {
        const element = this.$refs[this.letter] && this.$refs[this.letter][0]
        this.scroll.scrollToElement(element)
      }
    }
  },
  methods: {
    handleCityClick (city) {
      this.changeCity(city)
      this.scroll.scrollToElement(this.$refs.wrapper)
    },
    ...mapActions(['changeCity'])
  }
}
</script>

<style lang="stylus" scoped>
// 样式省略...
</style>

首字母导航栏组件 Alphabet.vue:

  1. 因为是固定的26个英文大写字母,所以没有必要从父组件中引入列表数据再解析,直接手写就好了:'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')
  2. 当户点击导航栏字母,通过 $emit 向外触发一个 change 事件,告诉父组件当前选中的是哪个字母,然后父组件再通知 List 组件滚动到指定字母的城市列表。
  3. 当户滑动导航栏,需要知道当前滑动到哪个字母,然后才能执行第2步操作,这里我们使用了三个触摸事件:touchstarttouchmovetouchend,它们不像 click 事件,可以实时从事件对象中的 target 属性获取到当前字母,而是返回当前触摸屏幕区域的位置信息,所以需要变通下,以字母 A 为基点,通过滑动点距离字母 A 的相对高度 除以 每个字母的固定高度(这里是18),再向下取整得到此时触摸点对应的 字母下标值 了,最后有了下标值,就可以从字母数组列表中拿到对应的字母了。
  4. 至于圆形气泡,它相对于导航栏绝对定位的,所以通过字母 A 距离导航栏顶部的高度,再 加上 每个字母的固定高度 乘以 当前滑动到的字母下标值,就得到触摸点对应当前字母距离 A 在垂直方向上的相对偏移量,最后把这个偏移量赋予给圆形气泡,从而实现跟随字母上下跟随联动显示。
  5. 这里需要做个容错处理,就是只有在用户滑动时我们才会进行第3、4步的计算,所以在data中增加一个 touchActive 属性,用于判断用户是否是在滑动状态。
  6. 同样,对于这种滑动或者滚动事件,必定会频繁触发事件,所以需要加入 节流 处理,减少频繁触发带来的资源消耗。
<template>
  <ul class="alphabet-list" ref="bar">
    <li
      class="item"
      v-for="item of letters"
      :key="item"
      :ref="item"
      @click="handleLetterClick"
      @touchstart="handleTouchStart"
      @touchmove="handleTouchMove"
      @touchend="handleTouchEnd"
    >
      {{ item }}
    </li>
	<!-- 圆形跟随气泡 -->
    <div
      class="active-letter"
      :style="{ top: activeLetterTop + 'px' }"
      v-show="activeLetter"
    >
      <span>{{ activeLetter }}</span>
    </div>
  </ul>
</template>

<script>
export default {
  name: 'Alphabet',
  data () {
    return {
      letters: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''),
      activeLetter: '',
      activeLetterTop: 0,
      startY: 0,
      touchActive: false,
      timer: null
    }
  },
  mounted () {
    this.startY = this.$refs.bar.offsetTop + this.$refs.A[0].offsetTop || 0 // 字母A距离文档边框顶部的高度
  },
  methods: {
    handleLetterClick (e) {
      this.$emit('change', e.target.innerText)
    },
    handleTouchStart () {
      this.touchActive = true
    },
    handleTouchMove (e) {
      if (this.touchActive) {
        // 节流,减少频繁触发带来的资源消耗
        if (this.timer) return
        this.timer = setTimeout(() => {
          const touchY = e.touches[0].clientY || e.touches[0].pageY
          const index = Math.floor((touchY - this.startY) / 18) // 18 为字母的高度,通过此计算出字母的下标值
          if (index >= 0 && index <= this.letters.length) {
            this.activeLetter = this.letters[index]
            this.activeLetterTop = this.$refs.A[0].offsetTop + index * 18 // 字母A偏离导航栏顶部相对高度作为初始值,加上滑动到其它字母相对于A的偏移量,实现圆形气泡跟随
            this.$emit('change', this.activeLetter)
          }
          this.timer = null
        }, 30)
      }
    },
    handleTouchEnd () {
      this.touchActive = false
      this.activeLetter = ''
    }
  }
}
</script>

<style lang="stylus" scoped>
// 样式省略...
</style>

后记

这样一来,我们就实现了一个侧边栏城市联动功能的页面,功能虽然不复杂,但是编写文章和组织语言却花了点时间,相当于重新整理下思路,算是温故而知新吧。

源码在这里

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Vue 移动端城市联动控件是一种用于移动端网页开发的工具,可以实现省市区三级联动选择功能。该控件基于Vue框架,通过Vue的双向数据绑定和组件化开发思想,简化了城市选择页面的开发过程。 使用该控件首先需要安装相关依赖,并在Vue组件中引入并注册该控件。然后在模板中使用该控件的标签,并绑定相关的数据。控件会根据绑定的数据动态生成市、区的选择项。当用户选择省份时,控件会根据省份生成对应的市级选择项;当用户选择市级时,控件会根据市级生成对应的区级选择项。用户可以通过控件的事件监听机制来获取用户选择城市信息。 该控件通过接口调用获取城市数据,可以根据实际需求进行配置。可以自定义城市数据的接口地址,以及省市区数据的字段名,方便适配各种数据源。 在移动端的应用中,城市选择是一个常见的需求。通过使用该控件,可以提高开发效率,减少重复劳动,提升用户体验。用户可以方便地根据自己的需要在手机选择所需要的城市信息,而无需手动输入。同时,该控件还具有良好的兼容性和自定义性,可以根据项目需求进行灵活的配置和样式定制。 总之,Vue 移动端城市联动控件是一个方便实用的工具,可以在移动端开发中快速实现城市选择功能,提高开发效率,提升用户体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值