如👇图,实现一个滚动内容区域,右侧字母滚动索引定位;选择和拖动字母索引,对应内容滚动到视窗
image.png

环境准备:

  • 安装better-scroll npm包
  • 安装 mouseWheel 扩展 BetterScroll 鼠标滚轮的能力,开启鼠标滚动(移动端非必须)

    1. npm install @better-scroll/core @better-scroll/mouse-wheel --save

    实现步骤:

    数据结构

  • 内容区和索引按下图数据结构处理

    1. export default {
    2. data() {
    3. return {
    4. entityList: [
    5. {
    6. key: 'A',
    7. list: ['氨基酸代谢病', '广泛性发育障碍']
    8. },
    9. {
    10. key: 'B',
    11. list: ['巴特综合征', '包涵体性结膜炎', '膀胱外翻', '鼻腔结外型NK/T细胞淋巴瘤']
    12. },
    13. {
    14. key: 'C',
    15. list: ['C5功能不全综合征', '肠道蛔虫症', '喘息样支气管炎']
    16. },
    17. {
    18. key: 'D',
    19. list: ['低氯性氮质血症综合征', '石棉状糠疹', 'Dravet综合征']
    20. }
    21. ]
    22. };
    23. }
    24. };

    基本HTML

    1. <!-- 内容区域 -->
    2. <!-- 最外层父容器wrapper,固定高度并且overflow:hidden-->
    3. <div class="h-534px flex-1 wrapper overflow-hidden" ref="wrapper">
    4. <!-- content 注意滚动区域一定是父容器的第一个子元素,当高度超出父容器即可滚动 -->
    5. <ul class="content">
    6. <!-- v-for 循环出列表 -->
    7. <li
    8. v-for="(item, index) in entityList"
    9. :key="index"
    10. class="flex flex-col"
    11. ref="listGroup"
    12. >
    13. <div
    14. class="h-42px leading-42px text-sm font-bold pl-15px w-244px"
    15. >
    16. {{ item.key }}
    17. </div>
    18. <div class="flex flex-col">
    19. <span
    20. class="h-42px leading-42px text-sm pl-15px g-clamp1 w-244px"
    21. v-for="(it, i) in item.list"
    22. :key="i"
    23. >
    24. {{ it }}
    25. </span>
    26. </div>
    27. </li>
    28. </ul>
    29. </div>
    30. <!-- 索引 -->
    31. <ul class="entityList w-15px bg-white">
    32. <!-- v-for 循环出索引 -->
    33. <li
    34. v-for="(item, index) in entityList"
    35. :key="index"
    36. :data-index="index"
    37. class="w-3 text-4 h-3 mb-1 leading-3 text-center text-gray6"
    38. >
    39. {{ item.key }}
    40. </li>
    41. </ul>

    使用better-scroll实现内容区列表的滚动

    1. <script>
    2. //import 引入BScroll
    3. import BScroll from '@better-scroll/core';
    4. import MouseWheel from '@better-scroll/mouse-wheel';
    5. BScroll.use(MouseWheel);
    6. export default {
    7. mounted() {
    8. //dom渲染完毕,初始化better-scroll
    9. this.$nextTick(() => {
    10. this.initBanner();
    11. });
    12. },
    13. methods: {
    14. initBanner() {
    15. if (this.scroll && this.scroll.destroy){
    16. this.scroll.refresh();//当 DOM 结构发生变化的时候需重新计算 BetterScroll
    17. this.scroll.destroy();//销毁 BetterScroll,解绑事件
    18. }
    19. this.scroll = new BScroll('.wrapper', {
    20. scrollY: true,//纵向滚动
    21. click: true,
    22. mouseWheel: true,
    23. disableMouse: false, //启用鼠标拖动
    24. disableTouch: false, //启用手指触摸
    25. probeType: 3 //设置为3,BetterScroll实时派发 scroll 事件
    26. });
    27. }
    28. }
    29. };
    30. </script>

    💥注意:这里我们在mounted时期,在this.$nextTick 的回调函数中初始化 better-scroll 。这时wrapper 的 DOM 已经渲染了,我们可以正确计算它以及它内层 content 的高度,以确保滚动正常。

    给索引添加点击事件和移动事件实现跳转

    1. <ul class="entityList w-15px bg-white">
    2. <li
    3. v-for="(item, index) in entityList"
    4. :key="index"
    5. :data-index="index"
    6. class="w-3 text-4 h-3 mb-1 leading-3 text-center text-gray6"
    7. @touchstart="onShortcutStart" //点击事件
    8. @touchmove.stop.prevent="onShortcutMove" //移动事件
    9. >
    10. {{ item.key }}
    11. </li>
    12. </ul>
    1. created() {
    2. // 添加一个 touch 用于记录移动的属性
    3. this.touch = {};
    4. this.$nextTick(() => {
    5. this.initBanner();
    6. });
    7. },
    8. methods: {
    9. onShortcutStart(e) {
    10. // 获取到绑定的 index
    11. let index = e.target.getAttribute('data-index');
    12. // 使用 better-scroll 的 scrollToElement 方法实现跳转
    13. this.scroll.scrollToElement(this.$refs.listGroup[index]);
    14. // 记录一下点击时候的 Y坐标 和 index
    15. let firstTouch = e.touches[0].pageY;
    16. this.touch.y1 = firstTouch;
    17. this.touch.anchorIndex = index;
    18. },
    19. onShortcutMove(e) {
    20. // 再记录一下移动时候的 Y坐标,然后计算出移动了几个索引
    21. let touchMove = e.touches[0].pageY;
    22. this.touch.y2 = touchMove;
    23. // 这里的 16.7 是索引元素的高度
    24. let delta = Math.floor((this.touch.y2 - this.touch.y1) / 18);
    25. // 计算最后的位置
    26. let index = this.touch.anchorIndex * 1 + delta;
    27. //注意,这里要有边界判断,不然会报错
    28. if (index >= 0 && index <= this.entityList.length - 2) {
    29. this.scroll.scrollToElement(this.$refs.listGroup[index]);
    30. }
    31. }
    32. }

    给索引添加高亮

  • 在data中定义currentIndex用于索引高亮的判断,并在html中绑定class

    1. data() {
    2. return {
    3. currentIndex: 0,
    4. entityList: [
    5. {
    6. key: 'A',
    7. list: ['氨基酸代谢病', '广泛性发育障碍']
    8. }]
    9. }
    10. }
    1. <ul class="entityList w-15px bg-white">
    2. <li
    3. v-for="(item, index) in entityList"
    4. :key="index"
    5. :data-index="index"
    6. class="w-3 text-4 h-3 mb-1 leading-3 text-center text-gray6"
    7. @touchstart="onShortcutStart"
    8. @touchmove.stop.prevent="onShortcutMove"
    9. :class="{ current: currentIndex === index }"
    10. >
    11. {{ item.key }}
    12. </li>
    13. </ul>

    接下来求currentIndex:

  • 先通过better-scroll 的on(type, fn, context)方法,监听当前实例上的scroll,得到内容区y轴的偏移量

    1. initBanner() {
    2. if (this.scroll && this.scroll.destroy) {
    3. this.scroll.refresh();
    4. this.scroll.destroy();
    5. }
    6. this.scroll = new BScroll('.wrapper', {
    7. scrollY: true,
    8. click: true,
    9. mouseWheel: true,
    10. disableMouse: false, //启用鼠标拖动
    11. disableTouch: false, //启用手指触摸
    12. probeType: 3
    13. });
    14. // 监听Y轴偏移的值
    15. this.scroll.on('scroll', pos => {
    16. this.scrollY = pos.y;
    17. });
    18. },
  • data中初始化 listHeight ,添加calculateHeight() 方法计算内容区高度

    1. data() {
    2. return {
    3. listHeight: [],
    4. currentIndex: 0,
    5. entityList: [
    6. {
    7. key: 'A',
    8. list: ['氨基酸代谢病', '广泛性发育障碍']
    9. }
    10. ]
    11. }
    12. }
    1. //计算内容区高度
    2. _calculateHeight() {
    3. this.listHeight = [];
    4. const list = this.$refs.listGroup;
    5. let height = 0;
    6. this.listHeight.push(height);
    7. for (let i = 0; i < list.length; i++) {
    8. let item = list[i];
    9. //累加之前的高度
    10. height += item.clientHeight;
    11. this.listHeight.push(height);
    12. }
    13. }
  • data中初始化scrollY为-1,在 watch 中监听 scrollY

    1. data() {
    2. return {
    3. scrollY: -1
    4. currentIndex: 0,
    5. listHeight: [],
    6. entityList: [
    7. {
    8. key: 'A',
    9. list: ['氨基酸代谢病', '广泛性发育障碍']
    10. }
    11. ]
    12. }
    13. }
    1. watch: {
    2. scrollY(newVal) {
    3. // 向下滑动的时候 newVal 是一个负数,所以当 newVal > 0 时,currentIndex 直接为 0
    4. if (newVal > 0) {
    5. this.currentIndex = 0;
    6. return;
    7. }
    8. // 计算内容区高度判断 对应索引currentIndex 的值
    9. for (let i = 0; i < this.listHeight.length - 1; i++) {
    10. let height1 = this.listHeight[i];
    11. let height2 = this.listHeight[i + 1];
    12. if (-newVal >= height1 && -newVal < height2) {
    13. this.currentIndex = i;
    14. return;
    15. }
    16. }
    17. // 当超 -newVal > 最后一个高度的时候
    18. // 因为 this.listHeight 有头尾,所以需要 - 2
    19. this.currentIndex = this.listHeight.length - 2;
    20. }
    21. }

    这样就得到了currentIndex 实现索引高亮的特效

    全部代码

  1. <template>
  2. <div>
  3. <!-- 内容区域 -->
  4. <div class="h-534px flex-1 wrapper overflow-hidden" ref="listview">
  5. <ul class="content">
  6. <li
  7. v-for="(item, index) in entityList"
  8. :key="index"
  9. class="flex flex-col"
  10. ref="listGroup"
  11. >
  12. <div class="h-42px leading-42px text-sm font-bold pl-15px w-244px">
  13. {{ item.key }}
  14. </div>
  15. <div class="flex flex-col">
  16. <Link
  17. class="h-42px leading-42px text-sm pl-15px g-clamp1 w-244px"
  18. v-for="(it, i) in item.list"
  19. :key="i"
  20. :to="{
  21. name: 'Yidian',
  22. query: {
  23. title: it
  24. }
  25. }"
  26. >
  27. {{ it }}
  28. </Link>
  29. </div>
  30. </li>
  31. </ul>
  32. </div>
  33. <!-- 索引 -->
  34. <ul class="entityList w-15px bg-white">
  35. <li
  36. v-for="(item, index) in entityList"
  37. :key="index"
  38. :data-index="index"
  39. class="w-3 text-4 h-3 mb-1 leading-3 text-center text-gray6"
  40. @touchstart="onShortcutStart"
  41. @touchmove.stop.prevent="onShortcutMove"
  42. :class="{ current: currentIndex === index }"
  43. >
  44. {{ item.key }}
  45. </li>
  46. </ul>
  47. </div>
  48. </template>
  49. <script>
  50. import BScroll from '@better-scroll/core';
  51. import MouseWheel from '@better-scroll/mouse-wheel';
  52. BScroll.use(MouseWheel);
  53. export default {
  54. data() {
  55. return {
  56. currentIndex: 0,
  57. listHeight: [],
  58. entityList: [
  59. {
  60. key: 'A',
  61. list: ['氨基酸代谢病', '广泛性发育障碍']
  62. },
  63. {
  64. key: 'B',
  65. list: ['巴特综合征', '包涵体性结膜炎', '膀胱外翻', '鼻腔结外型NK/T细胞淋巴瘤']
  66. },
  67. {
  68. key: 'C',
  69. list: ['C5功能不全综合征', '肠道蛔虫症', '喘息样支气管炎']
  70. },
  71. {
  72. key: 'D',
  73. list: ['低氯性氮质血症综合征', '石棉状糠疹', 'Dravet综合征']
  74. },
  75. {
  76. key: 'E',
  77. list: ['耳聋', '儿童癫痫', '儿童头痛', '儿童急性中耳炎']
  78. },
  79. {
  80. key: 'F',
  81. list: [
  82. '腹肌缺如综合征',
  83. '肥大性神经病',
  84. ]
  85. }
  86. ],
  87. scrollY: -1
  88. };
  89. },
  90. mounted() {
  91. this.touch = {};
  92. this.$nextTick(() => {
  93. this.initBanner();
  94. });
  95. },
  96. methods: {
  97. //初始化scroll
  98. initBanner() {
  99. if (this.scroll && this.scroll.destroy) {
  100. this.scroll.refresh();
  101. this.scroll.destroy();
  102. }
  103. this.scroll = new BScroll('.wrapper', {
  104. scrollY: true,
  105. click: true,
  106. mouseWheel: true,
  107. disableMouse: false, //启用鼠标拖动
  108. disableTouch: false, //启用手指触摸
  109. probeType: 3
  110. });
  111. this._calculateHeight();
  112. this.scroll.on('scroll', pos => {
  113. this.scrollY = pos.y;
  114. });
  115. },
  116. onShortcutStart(e) {
  117. // 获取到绑定的 index
  118. let index = e.target.getAttribute('data-index');
  119. // 使用 better-scroll 的 scrollToElement 方法实现跳转
  120. this.scroll.scrollToElement(this.$refs.listGroup[index]);
  121. // 记录一下点击时候的 Y坐标 和 index
  122. let firstTouch = e.touches[0].pageY;
  123. this.touch.y1 = firstTouch;
  124. this.touch.anchorIndex = index;
  125. },
  126. onShortcutMove(e) {
  127. // 再记录一下移动时候的 Y坐标,然后计算出移动了几个索引
  128. let touchMove = e.touches[0].pageY;
  129. this.touch.y2 = touchMove;
  130. // 这里的 16.7 是索引元素的高度
  131. let delta = Math.floor((this.touch.y2 - this.touch.y1) / 12);
  132. // 计算最后的位置
  133. let index = this.touch.anchorIndex * 1 + delta;
  134. if (index >= 0 && index <= this.entityList.length - 2) {
  135. this.scroll.scrollToElement(this.$refs.listGroup[index]);
  136. }
  137. },
  138. //计算索引内容高度
  139. _calculateHeight() {
  140. this.listHeight = [];
  141. const list = this.$refs.listGroup;
  142. let height = 0;
  143. this.listHeight.push(height);
  144. for (let i = 0; i < list.length; i++) {
  145. let item = list[i];
  146. height += item.clientHeight;
  147. this.listHeight.push(height);
  148. }
  149. }
  150. },
  151. watch: {
  152. scrollY(newVal) {
  153. // 向下滑动的时候 newVal 是一个负数,所以当 newVal > 0 时,currentIndex 直接为 0
  154. if (newVal > 0) {
  155. this.currentIndex = 0;
  156. return;
  157. }
  158. // 计算 currentIndex 的值
  159. for (let i = 0; i < this.listHeight.length - 1; i++) {
  160. let height1 = this.listHeight[i];
  161. let height2 = this.listHeight[i + 1];
  162. if (-newVal >= height1 && -newVal < height2) {
  163. this.currentIndex = i;
  164. return;
  165. }
  166. }
  167. // 当超 -newVal > 最后一个高度的时候
  168. // 因为 this.listHeight 有头尾,所以需要 - 2
  169. this.currentIndex = this.listHeight.length - 2;
  170. }
  171. }
  172. };
  173. </script>
  174. <style scoped lang="postcss">
  175. .tabActive {
  176. @apply font-bold;
  177. }
  178. .tabActive::after {
  179. content: '';
  180. display: block;
  181. width: 18px;
  182. height: 3px;
  183. background: #00c2b0;
  184. border-radius: 2px;
  185. position: absolute;
  186. bottom: 0;
  187. }
  188. .sortActive {
  189. color: #00c2b0;
  190. }
  191. .select-left {
  192. @apply w-110px;
  193. }
  194. .select-left-item {
  195. @apply pl-15px h-42px text-sm;
  196. }
  197. .entityList {
  198. position: fixed;
  199. right: 8px;
  200. top: 156px;
  201. }
  202. .current {
  203. border-radius: 50%;
  204. background: #00beb0;
  205. color: #fff;
  206. }
  207. .typeAct {
  208. @apply bg-white text-primary;
  209. }
  210. </style>

总结

  • 参考了很多网上的资料,相对于原生实现,better-scroll带来了更大的便利,但是同时也需要我们对better-scroll有一定的了解。

参考文献

better-scroll官方文档
参考博客