记一次indexList组件的开发
- Published on
- 发布于·预估阅读9分钟
- Authors
- Name
- willson-wang
indexList组件开发只要捋清楚了核心逻辑那么实现起来就会比较简单了,于是总结一下,便于下次查阅;
indexList的两个基本需求
列表滚动的时候,右侧的导航标识也需要跟着变化;
点击or滑动右边导航标识的时候,左侧的列表可以滚动到对应的位置;
列表滚动的时候,右侧的导航标识怎么样去跟随滚动?
实现思路,监听列表的滚动事件,事实获取滚动的scrollY值,根据scrollY值,来计算当前滚动到的index索引;计算方式如下,首先通过getBoundingClientRect()获取每个滚动小块的height高度,并保存到一个数组中;然后判断,如果scrollY小于height[0],那么index = 0 or 负数;如果scrollY > height [i] && scrollY < height[i + 1],那么index = i + 1(因为0没有计算在内); 如果scrollY 不符合上面两个条件则等于height.length - 2,这个2自己根据情况定,目的是防止导航标识跟随到最底部;
scrollY(newY) {
const listHeight = this.listHeight;
if (-newY < this.menuHeaderElHeight) {
this.currentIndex = -1;
return;
}
newY = newY + this.menuHeaderElHeight;
if (-newY < listHeight[0]) {
this.currentIndex = 0;
return;
}
for (let i = 0; i < listHeight.length; i++) {
let height1 = listHeight[i];
let height2 = listHeight[i + 1];
if (-newY >=height1 && -newY < height2) {
// 如果修改结构,需要修改currentIndex的计算方式
this.currentIndex = i + 1;
this.diff = height2 + newY;
return;
}
}
this.currentIndex = listHeight.length - 2;
}
点击or滑动右边导航标识的时候,左侧的列表可以滚动到对应的位置
实现思路,绑定touchstart及touchmove事件,通过当前点击的索引来设置当前需要滚动到的元素,如果当前的索引小于0,则置0 or 其它;如果大于height.length - 2则都置为height.length - 2;而move得时候是通过计算(move时获取的touches[0].pageY - 点击时的touches[0].pageY)/ 一个导航标识的高度 | 0;
_scrollTo(index, flag) {
if (index < 0) {
this.$refs.scroll.scrollTo(0, 0);
this.scrollY = 0;
return;
} else if (index > this.listHeight.length - 2) {
index = this.listHeight.length - 2;
}
console.log(index);
this.$refs.scroll.scrollToElement(this.groudList[+index], 0);
this.scrollY = this.$refs.scroll.scroll.y;
},
一种效果的实现方式
<template>
<div class="city-map relative">
<div class="city-map__header">
<span class="city-map__back" @click="$router.back()"><i class="brokericon icon-arrow-left"></i></span>
<div>
<span><i class="brokericon icon-search"></i></span>
<input type="text" placeholder="请输入城市名称" v-model="searchCity">
</div>
</div>
<div class="city-map__content" v-show="!searchResult.length && !searchCity && list.menu.length">
<vue-better-scroll class="scroller-container"
:class="designTime && 'design'"
ref="scroll"
:probeType="3"
:listenScroll="listenScroll"
:pullDownRefresh="pullDownRefreshObj"
:pullUpLoad="pullUpLoadObj"
@pullingDown="$emit('pullingDown')"
@scroll="scrollHandler"
@pullingUp="$emit('pullingUp')">
<div class="city-map__menu">
<slot name="header"></slot>
<ul class="city-map__group" v-for="(group, index) in list.menu" :key="index">
<li class="city-map__group__item city-map__group__item--scroll">
<h2 class="city-map__group__anchor">{{group.name}}</h2>
<ul class="city-map__group__list">
<li v-for="(item, index) in group.items" :key="index" @click="click(item)">
<div>{{item.city_name}}</div>
</li>
</ul>
</li>
</ul>
</div>
</vue-better-scroll>
<ul class="city-map__nav" @touchstart="onTouchstartHandler" @touchmove.stop.prevent="onTouchmoveHandler" @touchend="onTouchendHandler">
<li :data-index="-1" :class="fixedCurrentIndex === -1 ? 'activted' : ''" v-if="this.$slots['header']">#</li>
<li v-for="(item, index) in list.nav" :key="index" :data-index="index" :class="fixedCurrentIndex === index ? 'activted' : ''">
{{item}}
</li>
</ul>
<div ref="fixed"
v-show="isFixedTitle && fixedTitle"
v-html="fixedTitle"
class="city-map__group__anchor--fixed city-map__group__anchor">
</div>
</div>
<div class="city-map__search" v-show="searchResult.length || searchCity">
<ul v-if="searchResult.length">
<li v-for="(city, index) in searchResult" class="city-map__search__item" :key="index" @click="click(city, true)">{{city.city_name}}</li>
</ul>
<div v-else class="no-data">
<span class="no-data-img" />
<p>暂无数据哦~</p>
</div>
</div>
<div v-if="!list.menu.length" class="loading">
<inline-loading></inline-loading>加载中
</div>
</div>
</template>
<script>
import VueBetterScroll from "vue2-better-scroll";
import { mapState, mapActions } from "vuex";
import { InlineLoading } from 'vux';
const cities = require('./cities.json').data.cities;
export default {
name: 'cityMap',
components: {
VueBetterScroll,
InlineLoading
},
props: {
city: Object,
isFixedTitle: {
type: Boolean,
default: false
}
},
data() {
return {
pullDownRefreshObj: false,
pullUpLoadObj: false,
listenScroll: true,
currentIndex: -1,
diff: -1,
scrollY: -1,
subTitleEl: null,
searchResult: [],
searchCity: '',
touchmoving: false,
fixedCurrentIndex: -1,
touchIndex: -1,
locCity: {},
cities: {},
}
},
computed: {
designTime() {
return this.isDesignTime
},
fixedTitle() {
return this.list.menu[this.currentIndex] ? this.list.menu[this.currentIndex].name : ''
},
list() {
let nav = [];
let menu = [];
const cities = this.cities && this.cities.nested && this.cities.nested.by_pinyin || {};
for (let prop in cities) {
if (cities.hasOwnProperty(prop)) {
nav.push(prop);
menu.push({
name: prop,
items: cities[prop]
})
}
}
return {nav, menu}
},
},
methods: {
getRect(el) {
if (!el) return;
return el.getBoundingClientRect && el.getBoundingClientRect()
},
scrollHandler(e) {
this.scrollY = e.y;
},
_calculateHeight() {
// 这里是获取了包含固定项的,如果换个类名就需要同步,修改下scrollY内的currentIndex的计算方式
this.groudList = this.$el.getElementsByClassName('city-map__group__item--scroll');
this.subTitleEl = this.$el.getElementsByClassName('city-map__group__anchor--fixed')[0];
this.subTitleHeight = this.subTitleEl ? this.getRect(this.subTitleEl).height : 0;
this.navItemEles = this.$el.getElementsByClassName('city-map__nav')[0].children;
this.navItemEl = this.navItemEles[0];
this.navItemHeight = this.navItemEl ? this.getRect(this.navItemEl).height : 0;
this.menuHeaderElHeight = (this.$slots['header'] && this.getRect(this.$slots['header'][0].elm).height) || 0;
this.listHeight = [];
if (!this.groudList) return;
let height = 0;
for (let i = 0; i < this.groudList.length; i++) {
let item = this.groudList[i];
height += item.clientHeight;
this.listHeight.push(height);
}
console.log(this.listHeight);
},
showToast(ele) {
this.$vux.toast.show({
text: ele.innerHTML,
position: 'middle',
type: 'text',
width: '12%'
})
},
onTouchstartHandler(e) {
const firstTouch = e.touches[0]
const target = e.targetTouches[0].target.nodeName === 'LI' && e.targetTouches[0].target;
if (!target) return;
let navIndex = target.dataset && target.dataset.index;
this.touch.y1 = firstTouch.pageY;
this.touch.navIndex = navIndex;
this.touchIndex = navIndex;
this.showToast(target);
this._scrollTo(navIndex, +navIndex < 0);
},
onTouchmoveHandler(e) {
const firstTouch = e.touches[0];
this.touch.y2 = firstTouch.pageY;
let delTa = (this.touch.y2 - this.touch.y1) / this.navItemHeight | 0;
let navIndex = parseInt(this.touch.navIndex) + delTa;
this.touchIndex = navIndex;
this.touchmoving = true;
this._scrollTo(navIndex, +navIndex < 0);
},
onTouchendHandler() {
this.touchmoving = false;
},
_scrollTo(index, flag) {
if (index < 0) {
this.$refs.scroll.scrollTo(0, 0);
this.scrollY = 0;
return;
} else if (index > this.listHeight.length - 2) {
index = this.listHeight.length - 2;
}
console.log(index);
this.$refs.scroll.scrollToElement(this.groudList[+index], 0);
this.scrollY = this.$refs.scroll.scroll.y;
},
click(city, flag) {
this.$emit('checkAddr', city);
}
},
watch: {
currentIndex(val) {
this.fixedCurrentIndex = val;
this.touchmoving && this.showToast(this.navItemEles[val < 0 ? 0 : val]);
},
scrollY(newY) {
const listHeight = this.listHeight;
if (-newY < this.menuHeaderElHeight) {
this.currentIndex = -1;
return;
}
newY = newY + this.menuHeaderElHeight;
if (-newY < listHeight[0]) {
this.currentIndex = 0;
return;
}
for (let i = 0; i < listHeight.length; i++) {
let height1 = listHeight[i];
let height2 = listHeight[i + 1];
if (-newY >=height1 && -newY < height2) {
// 如果修改结构,需要修改currentIndex的计算方式
this.currentIndex = i + 1;
this.diff = height2 + newY;
return;
}
}
this.currentIndex = listHeight.length - 2;
},
diff(newVal) {
this.subTitleHeight = this.subTitleHeight ? this.subTitleHeight : this.getRect(this.subTitleEl).height;
let fixedTop = (newVal > 0 && newVal < this.subTitleHeight) ? newVal - this.subTitleHeight : 0;
if (this.fixedTop === fixedTop) return;
this.fixedTop = fixedTop;
this.$refs.fixed.style['transform'] = `translate3d(0,${fixedTop}px,0)`;
},
searchCity(val) {
if (!val) {
this.searchResult = [];
this.$nextTick(() => {
this._scrollTo(this.currentIndex, true);
})
} else {
this.searchResult = [];
this.list.menu.forEach((item) => {
item.items.forEach((city) => {
if (city.city_name.indexOf(val) > -1) {
this.searchResult.push(city);
}
})
})
// this.searchResult = [...new Set(this.searchResult)];
}
},
cities(val) {
this.$nextTick(() => {
this._calculateHeight();
})
},
touchIndex(val) {
if (val && +val > this.currentIndex) {
this.fixedCurrentIndex = +val;
} else {
this.fixedCurrentIndex = this.currentIndex;
}
}
},
activated() {
this.groudList = [];
this.listHeight = [];
this.subTitleHeight = 0;
this.touch = {};
this.searchCity = '';
this.cities = cities;
this.$nextTick(() => {
this._calculateHeight();
})
}
}
</script>
<style lang="less" scoped>
@import '~vux/src/styles/1px.less';
ul, li {
list-style: none;
padding: 0;
margin: 0;
}
.city-map {
display: flex;
height: 100%;
flex-direction: column;
&__header {
flex: 0 0 45px;
display: flex;
align-items: center;
border-bottom: 1px solid #ddd;
>span {
flex: 0 0 56px;
height: 30px;
text-align: center;
line-height: 30px;
i {
color: #939899;
font-size: 22px;
}
}
>div {
flex: 0 0 274px;
align-items: center;
position: relative;
height: 30px;
span {
position: absolute;
top: 50%;
left: 12px;
transform: translate(0, -50%);
i {
color: #aeaeae;
font-size: 16px;
}
}
input {
color: #000;
background-color: #e3e6e6;
font-size: 14px;
width: 100%;
border: 0;
outline: none;
border-radius: 3px;
text-indent: 34px;
height: 30px;
line-height: 30px;
}
}
}
&__content {
flex: 1;
position: relative;
overflow: hidden;
.scroller-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
bottom: 0;
overflow: hidden;
}
}
&__group {
&__item {
display: flex;
flex-direction: column;
justify-content: center;
}
&__anchor {
flex: 0 0 41px;
width: 100%;
line-height: 41px;
border-bottom: 1px solid #f5f5f5;
background-color: #f5f5f5;
font-size: 12px;
color: #999;
padding-left: 22px;
font-weight: normal;
}
&__list {
display: flex;
flex-direction: column;
justify-content: center;
>li {
flex: 0 0 50px;
>div {
height: 50px;
line-height: 50px;
border-bottom: 1px solid #e2e2e2;
font-size: 16px;
color: #000;
padding-left: 22px;
background-color: #fff;
}
&:last-of-type div {
border-bottom: 1px solid #f5f5f5;
}
}
}
}
&__nav {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 40px;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
li {
flex: 0 0 24px;
width: 100%;
font-size: 14px;
line-height: 24px;
color: #000;
font-weight: 700;
}
.activted {
color: orange;
}
}
&__search {
flex: 1;
position: relative;
overflow: hidden;
&__item {
height: 50px;
line-height: 50px;
border-bottom: 1px solid #ddd;
font-size: 16px;
color: #000;
padding-left: 22px;
background-color: #fff;
}
}
.no-data{
margin-top: 90px;
font-size: 12px;
color: #999;
text-align: center;
.no-data-img{
background: url("./img/noData.jpg") no-repeat center;
display: inline-block;
background-size: 100% 100%;
width: 80px;
height: 70px;
}
}
.loading {
margin-top: 90px;
text-align: center;
}
}
</style>
<style lang="less" scoped>
.relative {
position: relative;
}
html.ios,
html.iphone {
&.is-app,
&[data-runtime="app"] {
.relative {
position: relative;
padding-top: 20px
}
}
}
@iphonex: ~"only screen and (device-width: 375PX) and (device-height: 812PX) and (-webkit-device-pixel-ratio: 3)";
@media @iphonex {
html.ios,
html.iphone {
&.is-app,
&[data-runtime="app"] {
.relative{
padding-top: 44px;
}
}
}
}
</style>