123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588 |
- <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>
|