likes
comments
collection
share

【前端进阶开发日记 ❤】那就来封装一个城市选择器吧

作者站长头像
站长
· 阅读数 21

写在前面

哎呀呀,每次都是要沉淀好久好久才能更一篇文章...基本上半个月都很难出一篇,但还是想把这个系列做起来,主要是为了记录自己学习和开发的过程,以便在面试的时候讲项目,能说得头头是道(◍•ᴗ•◍)

马上就要开始准备面试啦~想冲冲冲去北京!!我也会努力更文哒!! 💪 💪

简介

今天来讲讲怎么封装一个城市选择器吧~

大概介绍一下它的功能,能够按照城市或省份进行分类选择,其中每个tab下都有按照字母拼音开头的选择,点击后能跳转到相应的区域去选择具体城市,右上角还有一个下拉框选择器,可以直接选择对应的城市,默认按照拼音顺序排列,效果如下

【前端进阶开发日记 ❤】那就来封装一个城市选择器吧

大体框架先搭建起来,捋一捋这个小组件的封装逻辑~

  1. 首先需要行文字来显示城市,然后还有上下箭头以标识是否显示弹出框

  2. 弹出框以及里面元素的布局

  3. 处理数据并渲染到对应DOM元素上

  4. 最后是书写跳转和选择的逻辑

  5. 完善右上角下拉选择框逻辑

好~大概的思路我们已经建立了,接下来就让我们开始实战吧ꉂꉂ꒰•̤▿•̤*ૢ꒱

第一步

虽然效果图上看起来是只有两个元素,但其实有三个呢,其中一个被隐藏了

【前端进阶开发日记 ❤】那就来封装一个城市选择器吧

这个是用v-if和v-else,加上给这俩图标一个visible属性,设置Boolean值来控制显示和隐藏的

但其实有更好的解决方案,确实是只有两个元素来做这个。我们知道,v-if的切换开销是相对来说比较大的,因为它操作的是DOM元素的存在与否,这样容易引起浏览器的Layout回流,渲染的性能相对来说比较低

所以取而代之的是,通过操作样式来实现这个功能

<template #reference>
  <div class="result">
    <div>{{ result }}</div>
    <div>
      <el-icon-arrowdown :class="{ 'rotate': visible }"></el-icon-arrowdown>
    </div>
  </div>
</template>


<style lang='scss' scoped>
.rotate {
  transform: rotate(180deg);
}
</style>

我们给这个图标组件动态绑定样式,通过rotate来控制它的标识性,也摒弃了两个图标来做转化的思路,而是用css3内置的属性来做这个功能,点击后把图标进行180°旋转

第二步

这边主要就是考验基本功了,布局走起~

你以为那些字母会是好多好多个span?吼吼!才不是呢

首先后台给的数据格式是这样的

【前端进阶开发日记 ❤】那就来封装一个城市选择器吧 citis对象里面包含了多个数组对象,字母拼音是键,数组对象是值,每一个数组对象中又包含了多个对象,每一个对象代表一个城市,有对应的id

值得学习的一点是,这边是动态渲染DOM然后进行布局,代码真的很优雅好吧!!

<div class="city">
  <!--  <div v-for="(value, key) in cities">{{key}}</div> -->
  <!-- 字母区域 -->
  <div
      class="city-item"
      @click="clickChat(item)"
      v-for="(item, index) in Object.keys(cities)"
  >{{ item }}</div>
</div>

这边有渲染字母,也就是object的key值两种方法,一种是直接遍历这个cities对象,另一种调用Object.keys()方法遍历cities的key本身,大家取贴合自己学习习惯的方法就好~

第三步

数据格式如上所述,需要渲染成效果图那样,左边显示的是key,右边显示的是城市

<el-scrollbar max-height="300px">
  <template v-for="(value, key) in cities" :key="key">
    <el-row style="margin-bottom: 10px;" :id="key">
      <el-col :span="2">{{ key }}:</el-col>
      <el-col :span="22" class="city-name">
        <div
            @click="clickItem(item)"
            class="city-name-item"
            v-for="(item, index) in value"
            :key="item.id"
        >
          <div>{{ item.name }}</div>
        </div>
      </el-col>
    </el-row>
  </template>
</el-scrollbar>

直接在template里面处理数据,也是同样的遍历操作,用el-row和el-col来分区域,左边占2份的是拼音(或省份)部分,右边占22份的是城市部分

我们来审查一下元素以验证,对视觉要求比较高的小伙伴也可以根据实际情况进行微调

【前端进阶开发日记 ❤】那就来封装一个城市选择器吧

第四步

这里是最最最难写的部分啦 🤣 🤣也是最容易出bug的部分,我们一步一步来仔细分析吧~

首先明确我们的目标,在这一步我们需要实现两个主要功能:

  1. 点击字母拼音能跳转到对应区域

  2. 点击要选择的城市之后渲染在一开始的span上

4.1 功能一

功能一的核心实现在于跳转,怎么实现这个跳转比较好呢?

我们都知道,在原生开发中做这个功能最熟悉最常见的就是用a标签来做这个区域跳转

但是a标签的实现原理是通过改变路由来实现跳转功能的,这就意味着,如果我们用a标签那就要更改路径,但我们这个封装组件的整体是放在vue3项目中的,不便于我们实现功能

好在我们还能调用原生的DOM方法,不知道大家有没有见过这个scrollIntoView方法

// 点击字母区域
let clickChat = (item: string) => {
  let el = document.getElementById(item)
  //操作原生DOM,跳转到指定区域
  if (el) el.scrollIntoView()
}

4.2 功能二

我们首先找到渲染城市数据的DOM元素,然后给它绑定一个clickItem事件

<el-col :span="22" class="city-name">
  <div
      @click="clickItem(item)"
      class="city-name-item"
      v-for="(item, index) in value"
      :key="item.id"
  >
    <div>{{ item.name }}</div>
  </div>
</el-col>

在这一步骤我们需要做的事情有两个: 一个是渲染选择到的城市并且分发事件给父组件,二是关闭弹出框

// 点击每个城市
let clickItem = (item: City) => {
  // 给结果赋值
  result.value = item.name
  // 关闭弹出层
  visible.value = false
  emits('changeCity', item)
}

往往我们自己做项目的时候想不到细节处,比如选择完毕之后关闭弹出框。实际上逻辑并不复杂,但真正自己动手敲起来还真没那么容易哈,不敲不知道,一敲bug一堆( Ꙭ)

第五步

还有最后一个功能就是右上角的下拉选择框,这边我们也细分为两个功能要实现

  1. 自定义搜索城市,可以根据中文也可以根据拼音搜索

  2. 下拉框选择城市

5.1 功能一

自定义搜索,我们需要在setup中新建一个filterMethod方法用来过滤数据

并且对数据先进行一个简单的处理方便后续进行过滤,取到cities.value是一个数组对象,我们用flat方法将它解析出来

// 自定义搜索过滤
let filterMethod = (val: string) => {
  console.log(cities.value)
  let values = Object.values(cities.value).flat(2)
}

打印出的cities.value如下

【前端进阶开发日记 ❤】那就来封装一个城市选择器吧

根据MDN文档介绍,flat()  方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回

处理过后的values如下图所示 【前端进阶开发日记 ❤】那就来封装一个城市选择器吧

经过了这些数据处理之后,再来定义过滤搜索的内容

分为三种情况,一种是val为空,二是按照中文过滤,三是按照拼音过滤,过滤的核心方法是通过filter和includes去判断是否遍历的数据中是否包含val,然后返回对应的结果

// 自定义搜索过滤
let filterMethod = (val: string) => {
  let values = Object.values(cities.value).flat(2)
  if (val === '') {
    options.value = values
  } else {
    if (radioValue.value === '按城市') {
      // 中文和拼音一起过滤
      options.value = values.filter(item => {
        return item.name.includes(val) || item.spell.includes(val)
      })
    } else {
      // 中文过滤
      options.value = values.filter(item => {
        return item.name.includes(val)
      })
    }
  }
}

5.2 功能二

下拉框选择城市也需要进行数据匹配,这里涉及到一个很实用的数组方法Array.prototype.find()

find()  方法返回数组中满足提供的测试函数的第一个元素的值,否则返回 undefined

// 所有的城市数据
let allCity = ref<City[]>([])
// 下拉框显示城市的数据
let options = ref<City[]>([])

// 下拉框选择
let changeSelect = (val: number) => {
  let city = allCity.value.find(item => item.id === val)!
  result.value = city.name
  if (radioValue.value === '按城市') {
    emits('changeCity', city)
  } else {
    emits('changeProvince', city.name)
  }
}

这里用数组的find方法去查找符合id匹配的数据,找得到就将返回值赋值给city

然后再取city的name赋值到城市选择器最终结果的span上

总结

到这里整个封装城市选择器这个组件的工作差不多就结束啦~

个人最大的感触就是,乍一看这个组件还是挺复杂的,但是一步步把它拆解开来之后发现,每一个细小处的工作其实并不难,难的是如何将这些拆分的步骤组织成一个完整的组件架构

也希望这篇文章能给大家一些思路和启发~ 🥰 🥰

如果大家想要这个组件的完整源码,可以在评论区@我~ 感谢看到这里的每一个小伙伴๐•ᴗ•๐