<template>
|
<view ref="u-index-list" class="u-index-list">
|
<!-- #ifdef APP-NVUE -->
|
<list
|
:scrollTop="scrollTop"
|
enable-back-to-top
|
:offset-accuracy="1"
|
:style="{
|
maxHeight: addUnit(scrollViewHeight)
|
}"
|
@scroll="scrollHandler"
|
ref="u-index-list__scroll-view"
|
class="u-index-list__scroll-view"
|
>
|
<cell
|
v-if="$slots.header"
|
ref="header"
|
>
|
<slot name="header" />
|
</cell>
|
<slot />
|
<cell v-if="$slots.footer">
|
<slot name="footer" />
|
</cell>
|
</list>
|
<!-- #endif -->
|
<!-- #ifndef APP-NVUE -->
|
<scroll-view
|
:scrollTop="scrollTop"
|
:scrollIntoView="scrollIntoView"
|
:offset-accuracy="1"
|
:style="{
|
maxHeight: addUnit(scrollViewHeight)
|
}"
|
scroll-y
|
@scroll="scrollHandler"
|
ref="u-index-list__scroll-view"
|
class="u-index-list__scroll-view"
|
>
|
<view class="u-index-list__header" v-if="$slots.header">
|
<slot name="header" />
|
</view>
|
<slot />
|
<view class="u-index-list__footer" v-if="$slots.footer">
|
<slot name="footer" />
|
</view>
|
</scroll-view>
|
<!-- #endif -->
|
<view
|
class="u-index-list__letter"
|
ref="u-index-list__letter"
|
:style="{top: addUnit(letterInfo.top) , transform: 'translateY(-50%)'}"
|
@touchstart.prevent="touchStart"
|
@touchmove.prevent="touchMove"
|
@touchend.prevent="touchEnd"
|
@touchcancel.prevent="touchEnd"
|
>
|
<view
|
class="u-index-list__letter__item"
|
v-for="(item, index) in uIndexList"
|
:key="index"
|
:style="{
|
backgroundColor: activeIndex === index ? activeColor : 'transparent'
|
}"
|
>
|
<text
|
class="u-index-list__letter__item__index"
|
:style="{color: activeIndex === index ? '#fff' : inactiveColor}"
|
>{{ item }}</text>
|
</view>
|
</view>
|
<u-transition
|
mode="fade"
|
:show="touching"
|
:customStyle="{
|
position: 'absolute',
|
right: '50px',
|
top: addUnit(indicatorTop, 'px'),
|
zIndex: 3
|
}"
|
>
|
<view
|
class="u-index-list__indicator"
|
:class="['u-index-list__indicator--show']"
|
:style="{
|
height: addUnit(indicatorHeight),
|
width: addUnit(indicatorHeight)
|
}"
|
>
|
<text class="u-index-list__indicator__text">{{ uIndexList[activeIndex] }}</text>
|
</view>
|
</u-transition>
|
</view>
|
</template>
|
|
<script>
|
const indexList = () => {
|
const indexList = [];
|
const charCodeOfA = 'A'.charCodeAt(0);
|
for (let i = 0; i < 26; i++) {
|
indexList.push(String.fromCharCode(charCodeOfA + i));
|
}
|
return indexList;
|
}
|
import { props } from './props';
|
import { mpMixin } from '../../libs/mixin/mpMixin';
|
import { mixin } from '../../libs/mixin/mixin';
|
import { addUnit, sys, sleep, getPx } from '../../libs/function/index';
|
|
// #ifdef APP-NVUE
|
// 由于weex为阿里的KPI业绩考核的产物,所以不支持百分比单位,这里需要通过dom查询组件的宽度
|
const dom = uni.requireNativePlugin('dom')
|
// #endif
|
/**
|
* IndexList 索引列表
|
* @description 通过折叠面板收纳内容区域
|
* @tutorial https://uview-plus.jiangruyi.com/components/indexList.html
|
* @property {String} inactiveColor 右边锚点非激活的颜色 ( 默认 '#606266' )
|
* @property {String} activeColor 右边锚点激活的颜色 ( 默认 '#5677fc' )
|
* @property {Array} indexList 索引字符列表,数组形式
|
* @property {Boolean} sticky 是否开启锚点自动吸顶 ( 默认 true )
|
* @property {String | Number} customNavHeight 自定义导航栏的高度 ( 默认 0 )
|
* */
|
export default {
|
name: 'u-index-list',
|
mixins: [mpMixin, mixin, props],
|
// #ifdef MP-WEIXIN
|
// 将自定义节点设置成虚拟的,更加接近Vue组件的表现,能更好的使用flex属性
|
options: {
|
virtualHost: true
|
},
|
// #endif
|
data() {
|
return {
|
// 当前正在被选中的字母索引
|
activeIndex: -1,
|
touchmoveIndex: 1,
|
// 索引字母的信息
|
letterInfo: {
|
height: 0,
|
itemHeight: 0,
|
top: 0
|
},
|
// 设置字母指示器的高度,后面为了让指示器跟随字母,并将尖角部分指向字母的中部,需要依赖此值
|
indicatorHeight: 50,
|
// 字母放大指示器的top值,为了让其指向当前激活的字母
|
// indicatorTop: 0
|
// 当前是否正在被触摸状态
|
touching: false,
|
// 滚动条顶部top值
|
scrollTop: 0,
|
// scroll-view的高度
|
scrollViewHeight: 0,
|
// 系统信息
|
sys: sys(),
|
scrolling: false,
|
scrollIntoView: '',
|
pageY: 0,
|
topOffset: 0
|
}
|
},
|
computed: {
|
// 如果有传入外部的indexList锚点数组则使用,否则使用内部生成A-Z字母
|
uIndexList() {
|
return this.indexList.length ? this.indexList : indexList()
|
},
|
// 字母放大指示器的top值,为了让其指向当前激活的字母
|
indicatorTop() {
|
const {
|
top,
|
height,
|
itemHeight
|
} = this.letterInfo
|
return Math.floor(top - (height / 2) + itemHeight * this.activeIndex + itemHeight - 70 / 2)
|
}
|
},
|
watch: {
|
// 监听字母索引的变化,重新设置尺寸
|
uIndexList: {
|
immediate: false,
|
handler() {
|
sleep(30).then(() => {
|
this.setIndexListLetterInfo()
|
})
|
}
|
}
|
},
|
created() {
|
this.children = []
|
this.anchors = []
|
},
|
mounted() {
|
this.init()
|
sleep(50).then(() => {
|
this.setIndexListLetterInfo()
|
})
|
},
|
methods: {
|
addUnit,
|
init() {
|
// 设置列表的高度为整个屏幕的高度
|
//减去this.customNavHeight,并将this.scrollViewHeight设置为maxHeight
|
//解决当u-index-list组件放在tabbar页面时,scroll-view内容较少时,还能滚动
|
let customNavHeight = getPx(this.customNavHeight)
|
// this.scrollViewHeight = this.sys.windowHeight - customNavHeight
|
this.getIndexListRect().then(async sizeScroll => {
|
this.scrollViewHeight = sizeScroll.height ? sizeScroll.height : this.sys.windowHeight - customNavHeight
|
this.topOffset = this.sys.windowHeight - this.scrollViewHeight
|
// console.log('scrollViewHeight', this.scrollViewHeight)
|
// console.log('topOffset', this.topOffset)
|
})
|
},
|
// 索引列表被触摸
|
touchStart(e) {
|
// 获取触摸点信息
|
const touchStartData = e.changedTouches[0]
|
if (!touchStartData) return
|
this.touching = true
|
const {
|
pageY,
|
screenY
|
} = touchStartData
|
// 根据当前触摸点的坐标,获取当前触摸的为第几个字母
|
// #ifdef APP-NVUE
|
// 使用screenY要减去导航栏44和状态栏24高度
|
const currentIndex = this.getIndexListLetter(screenY - 68)
|
// #endif
|
// #ifndef APP-NVUE
|
const currentIndex = this.getIndexListLetter(pageY)
|
// #endif
|
this.setValueForTouch(currentIndex)
|
},
|
// 索引字母列表被触摸滑动中
|
touchMove(e) {
|
// 获取触摸点信息
|
let touchMove = e.changedTouches[0]
|
if (!touchMove) return;
|
|
// 滑动结束后迅速开始第二次滑动时候 touching 为 false 造成不显示 indicator 问题
|
if (!this.touching) {
|
this.touching = true
|
}
|
const {
|
pageY,
|
screenY
|
} = touchMove
|
// #ifdef APP-NVUE
|
// 使用screenY要减去导航栏44和状态栏24高度
|
const currentIndex = this.getIndexListLetter(screenY - 68)
|
// #endif
|
// #ifndef APP-NVUE
|
const currentIndex = this.getIndexListLetter(pageY)
|
// #endif
|
|
this.setValueForTouch(currentIndex)
|
},
|
// 触摸结束
|
touchEnd(e) {
|
// 延时一定时间后再隐藏指示器,为了让用户看的更直观,同时也是为了消除快速切换u-transition的show带来的影响
|
sleep(300).then(() => {
|
this.touching = false
|
})
|
},
|
// 获取索引列表的尺寸以及单个字符的尺寸信息
|
getIndexListLetterRect() {
|
return new Promise(resolve => {
|
// 延时一定时间,以获取dom尺寸
|
// #ifndef APP-NVUE
|
this.$uGetRect('.u-index-list__letter').then(size => {
|
resolve(size)
|
})
|
// #endif
|
|
// #ifdef APP-NVUE
|
const ref = this.$refs['u-index-list__letter']
|
dom.getComponentRect(ref, res => {
|
resolve(res.size)
|
})
|
// #endif
|
})
|
},
|
getIndexListScrollViewRect() {
|
return new Promise(resolve => {
|
// 延时一定时间,以获取dom尺寸
|
// #ifndef APP-NVUE
|
this.$uGetRect('.u-index-list__scroll-view').then(size => {
|
resolve(size)
|
})
|
// #endif
|
|
// #ifdef APP-NVUE
|
const ref = this.$refs['u-index-list__scroll-view']
|
dom.getComponentRect(ref, res => {
|
resolve(res.size)
|
})
|
// #endif
|
})
|
},
|
getIndexListRect() {
|
return new Promise(resolve => {
|
// 延时一定时间,以获取dom尺寸
|
// #ifndef APP-NVUE
|
this.$uGetRect('.u-index-list').then(size => {
|
resolve(size)
|
})
|
// #endif
|
|
// #ifdef APP-NVUE
|
const ref = this.$refs['u-index-list']
|
dom.getComponentRect(ref, res => {
|
resolve(res.size)
|
})
|
// #endif
|
})
|
},
|
// 设置indexList索引的尺寸信息
|
setIndexListLetterInfo() {
|
this.getIndexListLetterRect().then(size => {
|
// console.log('getIndexListLetterRect', size)
|
const {
|
height
|
} = size
|
const sysData = sys()
|
const windowHeight = sysData.windowHeight
|
let customNavHeight = 0
|
// 消除各端导航栏非原生和原生导致的差异,让索引列表字母对屏幕垂直居中
|
if (this.customNavHeight == 0) {
|
// #ifdef H5
|
customNavHeight = sysData.windowTop
|
// #endif
|
// #ifndef H5
|
// 在非H5中,为原生导航栏,其高度不算在windowHeight内,这里设置为负值,后面相加时变成减去其高度的一半
|
customNavHeight = -(sysData.statusBarHeight + 44)
|
// #endif
|
} else {
|
customNavHeight = getPx(this.customNavHeight)
|
}
|
this.getIndexListScrollViewRect().then(sizeScroll => {
|
// console.log('sizeScroll', sizeScroll)
|
this.letterInfo = {
|
height,
|
// 为了让字母列表对屏幕绝对居中,让其对导航栏进行修正,也即往上偏移导航栏的一半高度
|
top: sizeScroll.height / 2,
|
// top: (this.scrollViewHeight - height) / 2 + customNavHeight / 2,
|
itemHeight: Math.floor(height / this.uIndexList.length)
|
}
|
})
|
})
|
},
|
// 获取当前被触摸的索引字母
|
getIndexListLetter(pageY) {
|
this.pageY = pageY
|
let {
|
top,
|
height,
|
itemHeight
|
} = this.letterInfo
|
let index = this.currentIndex;
|
// 对H5的pageY进行修正,这是由于uni-app自作多情在H5中将触摸点的坐标跟H5的导航栏结合导致的问题
|
// #ifdef H5
|
// pageY += sys().windowTop
|
// #endif
|
// 对第一和最后一个字母做边界处理,因为用户可能在字母列表上触摸到两端的尽头后依然继续滑动
|
console.log('top1', top)
|
console.log('height', height)
|
top = top - (height / 2) // 减去transfrom的translateY值导致的高度
|
pageY = pageY - this.topOffset
|
// if (this.safeBottomFix) {
|
// pageY = pageY + 34
|
// }
|
console.log('topOffset', this.topOffset)
|
console.log('pageY', pageY)
|
console.log('top2', top)
|
if (pageY < top) {
|
index = 0
|
} else if (pageY >= top + height) {
|
// 如果超出了,取最后一个字母
|
index = this.uIndexList.length - 1
|
} else {
|
// 将触摸点的Y轴偏移值,减去索引字母的top值,除以每个字母的高度,即可得到当前触摸点落在哪个字母上
|
index = Math.floor((pageY - top) / itemHeight)
|
}
|
// console.log(index)
|
return index
|
},
|
// 设置各项由触摸而导致变化的值
|
async setValueForTouch(currentIndex) {
|
// 如果偏移量太小,前后得出的会是同一个索引字母,为了防抖,进行返回
|
if (currentIndex === this.activeIndex) return
|
this.activeIndex = currentIndex
|
// #ifndef APP-NVUE || MP-WEIXIN
|
// 在非nvue中,由于anchor和item都在u-index-item中,所以需要对index-item进行偏移
|
this.scrollIntoView = `u-index-item-${this.uIndexList[currentIndex].charCodeAt(0)}`
|
// #endif
|
|
// #ifdef MP-WEIXIN
|
// 微信小程序下,scroll-view的scroll-into-view属性无法对slot中的内容的id生效,只能通过设置scrollTop的形式去移动滚动条
|
const customNavHeight = this.customNavHeight
|
|
// 获取header slot的尺寸信息
|
const header = await this.getHeaderRect()
|
// item的top值,在nvue下,模拟出的anchor的top,类似非nvue下的index-item的top
|
let top = header.height
|
console.log(top)
|
const anchors = this.anchors
|
// 由于list组件无法获取cell的top值,这里通过header slot和各个item之间的height,模拟出类似非nvue下的位置信息
|
let children = this.children.map((item, index) => {
|
const child = {
|
height: item.height,
|
top: top
|
}
|
// 进行累加,给下一个item提供计算依据
|
top = top + item.height
|
// #ifdef APP-NVUE
|
// 只有nvue下,需要将锚点的高度也累加,非nvue下锚点高度是包含在index-item中的。
|
top = top + anchors[index].height
|
// #endif
|
return child
|
})
|
// console.log('this.children[currentIndex].top', children[currentIndex].top)
|
if (children[currentIndex]?.top) {
|
this.scrollTop = children[currentIndex].top - getPx(customNavHeight)
|
}
|
// #endif
|
|
// #ifdef APP-NVUE
|
// 在nvue中,由于cell和header为同级元素,所以实际是需要对header(anchor)进行偏移
|
const anchor = `u-index-anchor-${this.uIndexList[currentIndex]}`
|
console.log(anchor)
|
dom.scrollToElement(this.anchors[currentIndex].$refs[anchor], {
|
offset: 0,
|
animated: false
|
})
|
// #endif
|
},
|
getHeaderRect() {
|
// 获取header slot的高度,因为list组件中获取元素的尺寸是没有top值的
|
return new Promise(resolve => {
|
if (!this.$slots.header) {
|
resolve({
|
width: 0,
|
height: 0
|
})
|
}
|
|
// #ifndef APP-NVUE
|
this.$uGetRect('.u-index-list__header').then(size => {
|
resolve(size)
|
})
|
// #endif
|
|
// #ifdef APP-NVUE
|
let headerRef = this.$refs.header
|
if (!headerRef) {
|
resolve({
|
width: 0,
|
height: 0
|
})
|
}
|
dom.getComponentRect(headerRef, res => {
|
resolve(res.size)
|
})
|
// #endif
|
})
|
},
|
// scroll-view的滚动事件
|
async scrollHandler(e) {
|
if (this.touching || this.scrolling) return
|
// 每过一定时间取样一次,减少资源损耗以及可能带来的卡顿
|
this.scrolling = true
|
sleep(10).then(() => {
|
this.scrolling = false
|
})
|
let scrollTop = 0
|
const len = this.children.length
|
let children = this.children
|
// #ifdef APP-NVUE
|
// nvue下获取的滚动条偏移为负数,需要转为正数
|
let sys = uni.getSystemInfoSync()
|
scrollTop = Math.abs(e.contentOffset.y) / 10
|
// console.log('native', e)
|
// #endif
|
|
// 获取header slot的尺寸信息
|
const header = await this.getHeaderRect()
|
// item的top值,在nvue下,模拟出的anchor的top,类似非nvue下的index-item的top
|
let top = header.height
|
const anchors = this.anchors
|
// 由于list组件无法获取cell的top值,这里通过header slot和各个item之间的height,模拟出类似非nvue下的位置信息
|
children = this.children.map((item, index) => {
|
const child = {
|
height: item.height,
|
top: top
|
}
|
// 进行累加,给下一个item提供计算依据
|
top = top + item.height
|
// #ifdef APP-NVUE
|
// 只有nvue下,需要将锚点的高度也累加,非nvue下锚点高度是包含在index-item中的。
|
top = top + anchors[index].height
|
// #endif
|
return child
|
})
|
|
// #ifndef APP-NVUE
|
// 非nvue通过detail获取滚动条位移
|
scrollTop = e.detail.scrollTop
|
// console.log('scrollTop', scrollTop, this.customNavHeight)
|
// #endif
|
// 在弹窗中需要加上弹窗距离顶部的高度topOffset
|
scrollTop = scrollTop + getPx(this.customNavHeight)
|
for (let i = 0; i < len; i++) {
|
const item = children[i],
|
nextItem = children[i + 1]
|
// 如果滚动条高度小于第一个item的top值,此时无需设置任意字母为高亮
|
if (scrollTop <= children[0].top || scrollTop >= children[len - 1].top + children[len - 1].height) {
|
this.activeIndex = -1
|
break
|
} else if (!nextItem) {
|
// 当不存在下一个item时,意味着历遍到了最后一个
|
this.activeIndex = len - 1
|
break
|
} else if (scrollTop > item.top && scrollTop < nextItem.top) {
|
this.activeIndex = i
|
break
|
}
|
}
|
},
|
},
|
}
|
</script>
|
|
<style lang="scss" scoped>
|
@import "../../libs/css/components.scss";
|
|
.u-index-list {
|
|
&__letter {
|
position: absolute;
|
right: 0;
|
text-align: center;
|
z-index: 3;
|
padding: 0 6px;
|
width: 30px;
|
|
&__item {
|
width: 16px;
|
height: 16px;
|
border-radius: 100px;
|
margin: 1px 0;
|
@include flex;
|
align-items: center;
|
justify-content: center;
|
|
&--active {
|
background-color: $u-primary;
|
}
|
|
&__index {
|
font-size: 12px;
|
text-align: center;
|
line-height: 12px;
|
}
|
}
|
}
|
|
&__indicator {
|
width: 50px;
|
height: 50px;
|
border-radius: 100px 100px 0 100px;
|
text-align: center;
|
color: #ffffff;
|
background-color: #c9c9c9;
|
transform: rotate(-45deg);
|
@include flex;
|
justify-content: center;
|
align-items: center;
|
|
&__text {
|
font-size: 28px;
|
line-height: 28px;
|
font-weight: bold;
|
color: #fff;
|
transform: rotate(45deg);
|
text-align: center;
|
}
|
}
|
}
|
</style>
|