zhangmeng
2024-04-19 e3ba120cb766a17e098e58d11c39ffc600a3070c
commit | author | age
e3ba12 1 <template>
Z 2     <view class="u-index-list">
3         <!-- #ifdef APP-NVUE -->
4         <list
5             :scrollTop="scrollTop"
6             enable-back-to-top
7             :offset-accuracy="1"
8             :style="{
9                 maxHeight: $u.addUnit(scrollViewHeight)
10             }"
11             @scroll="scrollHandler"
12             ref="uList"
13         >
14             <cell
15                 v-if="$slots.header"
16                 ref="header"
17             >
18                 <slot name="header" />
19             </cell>
20             <slot />
21             <cell v-if="$slots.footer">
22                 <slot name="footer" />
23             </cell>
24         </list>
25         <!-- #endif -->
26         <!-- #ifndef APP-NVUE -->
27         <scroll-view
28             :scrollTop="scrollTop"
29             :scrollIntoView="scrollIntoView"
30             :offset-accuracy="1"
31             :style="{
32                 maxHeight: $u.addUnit(scrollViewHeight)
33             }"
34             scroll-y
35             @scroll="scrollHandler"
36             ref="uList"
37         >
38             <view v-if="$slots.header">
39                 <slot name="header" />
40             </view>
41             <slot />
42             <view v-if="$slots.footer">
43                 <slot name="footer" />
44             </view>
45         </scroll-view>
46         <!-- #endif -->
47         <view
48             class="u-index-list__letter"
49             ref="u-index-list__letter"
50             :style="{ top: $u.addUnit(letterInfo.top || 100) }"
51             @touchstart="touchStart"
52             @touchmove.stop.prevent="touchMove"
53             @touchend.stop.prevent="touchEnd"
54             @touchcancel.stop.prevent="touchEnd"
55         >
56             <view
57                 class="u-index-list__letter__item"
58                 v-for="(item, index) in uIndexList"
59                 :key="index"
60                 :style="{
61                     backgroundColor: activeIndex === index ? activeColor : 'transparent'
62                 }"
63             >
64                 <text
65                     class="u-index-list__letter__item__index"
66                     :style="{color: activeIndex === index ? '#fff' : inactiveColor}"
67                 >{{ item }}</text>
68             </view>
69         </view>
70         <u-transition
71             mode="fade"
72             :show="touching"
73             :customStyle="{
74                 position: 'fixed',
75                 right: '50px',
76                 top: $u.addUnit(indicatorTop),
77                 zIndex: 2
78             }"
79         >
80             <view
81                 class="u-index-list__indicator"
82                 :class="['u-index-list__indicator--show']"
83                 :style="{
84                     height: $u.addUnit(indicatorHeight),
85                     width: $u.addUnit(indicatorHeight)
86                 }"
87             >
88                 <text class="u-index-list__indicator__text">{{ uIndexList[activeIndex] }}</text>
89             </view>
90         </u-transition>
91     </view>
92 </template>
93
94 <script>
95     const indexList = () => {
96         const indexList = [];
97         const charCodeOfA = 'A'.charCodeAt(0);
98         for (let i = 0; i < 26; i++) {
99             indexList.push(String.fromCharCode(charCodeOfA + i));
100         }
101         return indexList;
102     }
103     import props from './props.js';
104     // #ifdef APP-NVUE
105     // 由于weex为阿里的KPI业绩考核的产物,所以不支持百分比单位,这里需要通过dom查询组件的宽度
106     const dom = uni.requireNativePlugin('dom')
107     // #endif
108     /**
109      * IndexList 索引列表
110      * @description  通过折叠面板收纳内容区域
111      * @tutorial https://uviewui.com/components/indexList.html
112      * @property {String}            inactiveColor    右边锚点非激活的颜色 ( 默认 '#606266' )
113      * @property {String}            activeColor        右边锚点激活的颜色 ( 默认 '#5677fc' )
114      * @property {Array}            indexList        索引字符列表,数组形式
115      * @property {Boolean}            sticky            是否开启锚点自动吸顶 ( 默认 true )
116      * @property {String | Number}    customNavHeight    自定义导航栏的高度 ( 默认 0 )
117      * */ 
118     export default {
119         name: 'u-index-list',
120         mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
121         // #ifdef MP-WEIXIN
122         // 将自定义节点设置成虚拟的,更加接近Vue组件的表现,能更好的使用flex属性
123         options: {
124             virtualHost: true
125         },
126         // #endif
127         data() {
128             return {
129                 // 当前正在被选中的字母索引
130                 activeIndex: -1,
131                 touchmoveIndex: 1,
132                 // 索引字母的信息
133                 letterInfo: {
134                     height: 0,
135                     itemHeight: 0,
136                     top: 0
137                 },
138                 // 设置字母指示器的高度,后面为了让指示器跟随字母,并将尖角部分指向字母的中部,需要依赖此值
139                 indicatorHeight: 50,
140                 // 字母放大指示器的top值,为了让其指向当前激活的字母
141                 // indicatorTop: 0
142                 // 当前是否正在被触摸状态
143                 touching: false,
144                 // 滚动条顶部top值
145                 scrollTop: 0,
146                 // scroll-view的高度
147                 scrollViewHeight: 0,
148                 // 系统信息
149                 sys: uni.$u.sys(),
150                 scrolling: false,
151                 scrollIntoView: '',
152             }
153         },
154         computed: {
155             // 如果有传入外部的indexList锚点数组则使用,否则使用内部生成A-Z字母
156             uIndexList() {
157                 return this.indexList.length ? this.indexList : indexList()
158             },
159             // 字母放大指示器的top值,为了让其指向当前激活的字母
160             indicatorTop() {
161                 const {
162                     top,
163                     itemHeight
164                 } = this.letterInfo
165                 return Math.floor(top + itemHeight * this.activeIndex + itemHeight / 2 - this.indicatorHeight / 2)
166             }
167         },
168         watch: {
169             // 监听字母索引的变化,重新设置尺寸
170             uIndexList: {
171                 immediate: true,
172                 handler() {
173                     uni.$u.sleep().then(() => {
174                         this.setIndexListLetterInfo()
175                     })
176                 }
177             }
178         },
179         created() {
180             this.children = []
181             this.anchors = []
182             this.init()
183         },
184         mounted() {
185             this.setIndexListLetterInfo()
186         },
187         methods: {
188             init() {
189                 // 设置列表的高度为整个屏幕的高度
190                 //减去this.customNavHeight,并将this.scrollViewHeight设置为maxHeight
191                 //解决当u-index-list组件放在tabbar页面时,scroll-view内容较少时,还能滚动
192                 this.scrollViewHeight = this.sys.windowHeight - this.customNavHeight
193             },
194             // 索引列表被触摸
195             touchStart(e) {
196                 // 获取触摸点信息
197                 const touchStart = e.changedTouches[0]
198                 if (!touchStart) return
199                 this.touching = true
200                 const {
201                     pageY
202                 } = touchStart
203                 // 根据当前触摸点的坐标,获取当前触摸的为第几个字母
204                 const currentIndex = this.getIndexListLetter(pageY)
205                 this.setValueForTouch(currentIndex)
206             },
207             // 索引字母列表被触摸滑动中
208             touchMove(e) {
209                 // 获取触摸点信息
210                 let touchMove = e.changedTouches[0]
211                 if (!touchMove) return;
212
213                 // 滑动结束后迅速开始第二次滑动时候 touching 为 false 造成不显示 indicator 问题
214                 if (!this.touching) {
215                     this.touching = true
216                 }
217                 const {
218                     pageY
219                 } = touchMove
220                 const currentIndex = this.getIndexListLetter(pageY)
221                 this.setValueForTouch(currentIndex)
222             },
223             // 触摸结束
224             touchEnd(e) {
225                 // 延时一定时间后再隐藏指示器,为了让用户看的更直观,同时也是为了消除快速切换u-transition的show带来的影响
226                 uni.$u.sleep(300).then(() => {
227                     this.touching = false
228                 })
229             },
230             // 获取索引列表的尺寸以及单个字符的尺寸信息
231             getIndexListLetterRect() {
232                 return new Promise(resolve => {
233                     // 延时一定时间,以获取dom尺寸
234                     // #ifndef APP-NVUE
235                     this.$uGetRect('.u-index-list__letter').then(size => {
236                         resolve(size)
237                     })
238                     // #endif
239
240                     // #ifdef APP-NVUE
241                     const ref = this.$refs['u-index-list__letter']
242                     dom.getComponentRect(ref, res => {
243                         resolve(res.size)
244                     })
245                     // #endif
246                 })
247             },
248             // 设置indexList索引的尺寸信息
249             setIndexListLetterInfo() {
250                 this.getIndexListLetterRect().then(size => {
251                     const {
252                         height
253                     } = size
254                     const sys = uni.$u.sys()
255                     const windowHeight = sys.windowHeight
256                     let customNavHeight = 0
257                     // 消除各端导航栏非原生和原生导致的差异,让索引列表字母对屏幕垂直居中
258                     if (this.customNavHeight == 0) {
259                         // #ifdef H5
260                         customNavHeight = sys.windowTop
261                         // #endif
262                         // #ifndef H5
263                         // 在非H5中,为原生导航栏,其高度不算在windowHeight内,这里设置为负值,后面相加时变成减去其高度的一半
264                         customNavHeight = -(sys.statusBarHeight + 44)
265                         // #endif
266                     } else {
267                         customNavHeight = uni.$u.getPx(this.customNavHeight)
268                     }
269                     this.letterInfo = {
270                         height,
271                         // 为了让字母列表对屏幕绝对居中,让其对导航栏进行修正,也即往上偏移导航栏的一半高度
272                         top: (windowHeight - height) / 2 + customNavHeight / 2,
273                         itemHeight: Math.floor(height / this.uIndexList.length)
274                     }
275                 })
276             },
277             // 获取当前被触摸的索引字母
278             getIndexListLetter(pageY) {
279                 const {
280                     top,
281                     height,
282                     itemHeight
283                 } = this.letterInfo
284                 // 对H5的pageY进行修正,这是由于uni-app自作多情在H5中将触摸点的坐标跟H5的导航栏结合导致的问题
285                 // #ifdef H5
286                 pageY += uni.$u.sys().windowTop
287                 // #endif
288                 // 对第一和最后一个字母做边界处理,因为用户可能在字母列表上触摸到两端的尽头后依然继续滑动
289                 if (pageY < top) {
290                     return 0
291                 } else if (pageY >= top + height) {
292                     // 如果超出了,取最后一个字母
293                     return this.uIndexList.length - 1
294                 } else {
295                     // 将触摸点的Y轴偏移值,减去索引字母的top值,除以每个字母的高度,即可得到当前触摸点落在哪个字母上
296                     return Math.floor((pageY - top) / itemHeight);
297                 }
298             },
299             // 设置各项由触摸而导致变化的值
300             setValueForTouch(currentIndex) {
301                 // 如果偏移量太小,前后得出的会是同一个索引字母,为了防抖,进行返回
302                 if (currentIndex === this.activeIndex) return
303                 this.activeIndex = currentIndex
304                 // #ifndef APP-NVUE || MP-WEIXIN
305                 // 在非nvue中,由于anchor和item都在u-index-item中,所以需要对index-item进行偏移
306                 this.scrollIntoView = `u-index-item-${this.uIndexList[currentIndex].charCodeAt(0)}`
307                 // #endif
308                 // #ifdef MP-WEIXIN
309                 // 微信小程序下,scroll-view的scroll-into-view属性无法对slot中的内容的id生效,只能通过设置scrollTop的形式去移动滚动条
310                 this.scrollTop = this.children[currentIndex].top
311                 // #endif
312                 // #ifdef APP-NVUE
313                 // 在nvue中,由于cell和header为同级元素,所以实际是需要对header(anchor)进行偏移
314                 const anchor = `u-index-anchor-${this.uIndexList[currentIndex]}`
315                 dom.scrollToElement(this.anchors[currentIndex].$refs[anchor], {
316                     offset: 0,
317                     animated: false
318                 })
319                 // #endif
320             },
321             getHeaderRect() {
322                 // 获取header slot的高度,因为list组件中获取元素的尺寸是没有top值的
323                 return new Promise(resolve => {
324                     dom.getComponentRect(this.$refs.header, res => {
325                         resolve(res.size)
326                     })
327                 })
328             },
329             // scroll-view的滚动事件
330             async scrollHandler(e) {
331                 if (this.touching || this.scrolling) return
332                 // 每过一定时间取样一次,减少资源损耗以及可能带来的卡顿
333                 this.scrolling = true
334                 uni.$u.sleep(10).then(() => {
335                     this.scrolling = false
336                 })
337                 let scrollTop = 0
338                 const len = this.children.length
339                 let children = this.children
340                 const anchors = this.anchors
341                 // #ifdef APP-NVUE
342                 // nvue下获取的滚动条偏移为负数,需要转为正数
343                 scrollTop = Math.abs(e.contentOffset.y)
344                 // 获取header slot的尺寸信息
345                 const header = await this.getHeaderRect()
346                 // item的top值,在nvue下,模拟出的anchor的top,类似非nvue下的index-item的top
347                 let top = header.height
348                 // 由于list组件无法获取cell的top值,这里通过header slot和各个item之间的height,模拟出类似非nvue下的位置信息
349                 children = this.children.map((item, index) => {
350                     const child = {
351                         height: item.height,
352                         top
353                     }
354                     // 进行累加,给下一个item提供计算依据
355                     top += item.height + anchors[index].height
356                     return child
357                 })
358                 // #endif
359                 // #ifndef APP-NVUE
360                 // 非nvue通过detail获取滚动条位移
361                 scrollTop = e.detail.scrollTop
362                 // #endif
363                 for (let i = 0; i < len; i++) {
364                     const item = children[i],
365                         nextItem = children[i + 1]
366                     // 如果滚动条高度小于第一个item的top值,此时无需设置任意字母为高亮
367                     if (scrollTop <= children[0].top || scrollTop >= children[len - 1].top + children[len -
368                             1].height) {
369                         this.activeIndex = -1
370                         break
371                     } else if (!nextItem) { 
372                         // 当不存在下一个item时,意味着历遍到了最后一个
373                         this.activeIndex = len - 1
374                         break
375                     } else if (scrollTop > item.top && scrollTop < nextItem.top) {
376                         this.activeIndex = i
377                         break
378                     }
379                 }
380             },
381         },
382     }
383 </script>
384
385 <style lang="scss" scoped>
386     @import "../../libs/css/components.scss";
387
388     .u-index-list {
389
390         &__letter {
391             position: fixed;
392             right: 0;
393             text-align: center;
394             z-index: 3;
395             padding: 0 6px;
396
397             &__item {
398                 width: 16px;
399                 height: 16px;
400                 border-radius: 100px;
401                 margin: 1px 0;
402                 @include flex;
403                 align-items: center;
404                 justify-content: center;
405
406                 &--active {
407                     background-color: $u-primary;
408                 }
409
410                 &__index {
411                     font-size: 12px;
412                     text-align: center;
413                     line-height: 12px;
414                 }
415             }
416         }
417
418         &__indicator {
419             width: 50px;
420             height: 50px;
421             border-radius: 100px 100px 0 100px;
422             text-align: center;
423             color: #ffffff;
424             background-color: #c9c9c9;
425             transform: rotate(-45deg);
426             @include flex;
427             justify-content: center;
428             align-items: center;
429
430             &__text {
431                 font-size: 28px;
432                 line-height: 28px;
433                 font-weight: bold;
434                 color: #fff;
435                 transform: rotate(45deg);
436                 text-align: center;
437             }
438         }
439     }
440 </style>