preface
The city linkage function is common in businesses. It is generally used for users to search or select their location by themselves, and then obtain some local recommendation information based on the location, such as recommendations of nearby businesses, scenic spots and entertainment, to guide users’ travel and consumption behaviors.
Results the preview
In fact, as long as the data structure is changed, it can also become a similar to the phone contacts list, the circular bubble to follow, is imitationmeizu
Indicates the effect of the current letter when you swipe up or down the sidebar in the phone address book.
Function introduction
As you can see, the following functions are not very complicated, mainly including:
- City search, according to the city name or pinyin fuzzy matching search
- City list, from top to bottom by city pinyin initials block display
- In the navigation bar of the first letter of the city on the right, click and slide the letters up and down to display the corresponding city list on the left, plus the circular bubble of the current letter to follow the display
- After selecting the city, the city is cached in the browser localStorage cache, keeping the current city display state
The project structure
Here, for convenience, I’m usingVue-cli4
Create a project with the following structure:
mock
Save a list of city json data, page data depends on it, the format is as follows:
{
"A": [{"id": 56."spell": "aba"."name": "Aba"
},
{
"id": 57."spell": "akesu"."name": aksu},... ] ."B": [{"id": 1."spell": "beijing"."name": "Beijing"
},
{
"id": 66."spell": "baicheng"."name": "Baicheng"},... ] . }Copy the code
As an aside, the data you get isn’t necessarily in this format, and it’s not alphabetically sorted, but rather a flattened list of cities. You can install pinyinjs to convert city names into pinyin, then take the first letters of pinyin, and finally group them by letter.
router
Save the route configuration. Because there is only one page, the simple configuration is as follows:
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')}]})Copy the code
Instead of routing, you can simply import the city-vue component written in app. vue.
store
State management configuration, which defines only the currentCity property currentCity for the List and Search components to share operations, or you can use this.$emit to notify the parent component of updates.utils
It encapsulates the localStorage operation, which is referenced in the Store configuration.views
Vue is used to store pages and components, each page corresponds to a self-named folder, create the city. Vue page and its components directory in the city folder, according to the function of the classification, we can divide into three sub-components:- Search.vue City Search component
- LIst. Vue City LIst component
- Alphabet. Vue city initials navigation component
Development depends on
vue-router
vuex
better-scroll
First of all, a single page can not display all the city list at one time, root users need to search, click or slide to select letters to display the corresponding partial list, here uses the Better-Scroll plug-in, it is a mobile terminal scrolling solution, relatively easy to use.
axios
Simulate back-end request data.
stylus
和stylus-loader
CSS styling and CSS preprocessing, so that you can use a modular approach to writing CSS code.
Function implementation
After simple configuration, we can start to implement the ‘coding’ function, but first we need to analyze what functions and data each component needs to implement:
- for
Serach
For the component, search matches and search result lists are implemented on it, so you only need to pass in the city list data. When a city is selected, you need to update currentCity; List
Component, because it wants to display the list of cities and follow the letter selected in the navigation bar on the right, so it needs to pass in the list of cities and the first letter of the currently selected city, and then when a city is selected, it also needs to update currentCity;Alphabet
Component that needs to be notified when clicked or swipedList
Component currently selected city initials, display and update the circular bubble display letters and position in the vertical direction to achieve linkage to follow, then the list of cities also scroll to the specified position.
After the above analysis, the City. Vue page file can be written: the three components of Search, List and Alphabet are introduced and their corresponding data is passed in respectively.
<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>
Copy the code
Search component search.vue:
- Watch is used to monitor the user’s input changes, and according to the input value, the results of fuzzy matching are returned from the city list data, and then displayed in the form of a drop-down list.
- The user clicks on a city in the list, asynchronously updates the value currentCity in the store via vuex’s mapActions helper function, then hides the search list and updates the currentCity;
- When there was no match, a prompt was added to tell the user that the data he was looking for was not found.
- Note that handling this type of user input listening operation frequently triggers requests, so it is necessary to add a
Image stabilization
To reduce the number of requests.
<template>
<div>
<div class="search">
<input
v-model="keyword"
class="search-input"
type="text"
placeholder="Enter city name or pinyin"
/>
</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">There is no match for what you entered</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 omit...</style>
Copy the code
City List component list.vue:
- The first load displays the default or last selected current city.
- According to the user clicks or slides the letter in the right letter navigation bar, use Watch to monitor letter changes, and then scroll to the list of cities with the specified letter;
- The user clicks on a city on the list, updates the current city and scrolls back to the top of the page.
<template>
<div>
<div class="current-city">
<div class="title">The current city</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 omit...</style>
Copy the code
Alphabet navigation bar component Alphabet. Vue:
- There is no need to import the list data from the parent component and then parse it, just write it by hand:
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')
. - When the user clicks the navigation bar letter, pass
$emit
Fires a change event to tell the parent which letter is currently selected, which then tells the List component to scroll to the List of cities with the specified letter. - When you slide the navigation bar, you need to know which letter you are sliding to before you can perform step 2. Here we use three touch events:
touchstart
.touchmove
.touchend
They don’t look likeclick
Event, you can get the current letter from the target property of the event object in real time, but return the location information of the current touch screen area, so it needs to be modified to the letterA
For base point, slide through the point distance letterA
Relative height ofDivided by the
The fixed height of each letter (here is 18), and then round down to get the corresponding touch point at this timeLetter subscript value
So, finally, we have the subscript value, and we can get the corresponding letter from the alphabetic arraylist. - As for the circular bubble, it is absolutely positioned relative to the navigation bar, so by letter
A
Height from the top of the navigation bar, andadd
The fixed height of each letterMultiplied by the
The touch point corresponds to the current letter distanceA
The relative offset in the vertical direction, and finally the offset is given to the circular bubble, so as to realize the linkage display of following letters up and down. - We need to make a fault tolerant processing here, that is, we will only carry out the calculation of step 3 and step 4 when the user slides, so we add a touchActive attribute in data to determine whether the user is sliding.
- Also, for this kind of slide or scroll event, events must be triggered frequently, so it needs to be added
The throttle
Processing to reduce resource consumption caused by frequent triggers.
<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>
<! -- Circle follows bubble -->
<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 // Height of the letter A from the top of the document border
},
methods: {
handleLetterClick (e) {
this.$emit('change', e.target.innerText)
},
handleTouchStart () {
this.touchActive = true
},
handleTouchMove (e) {
if (this.touchActive) {
// Throttling reduces resource consumption caused by frequent triggers
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 is the height of the letter, which is used to calculate the subscript value of the letter
if (index >= 0 && index <= this.letters.length) {
this.activeLetter = this.letters[index]
this.activeLetterTop = this.$refs.A[0].offsetTop + index * 18 // Letter A deviates from the relative height of the top of the navigation bar as the initial value, and slides to the offset of other letters relative to A to achieve circular bubble following
this.$emit('change'.this.activeLetter)
}
this.timer = null
}, 30)
}
},
handleTouchEnd () {
this.touchActive = false
this.activeLetter = ' '}}}</script>
<style lang="stylus" scoped>// style omit...</style>
Copy the code
Afterword.
As a result, we have achieved a page with a sidebar city linkage function. Although the function is not complicated, it took some time to write the article and organize the language.
The code word is not easy. Please give it a thumbs up
The source code is here