IndexBar.svelte 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. <script lang="ts">
  2. import { onMount } from 'svelte';
  3. import { fly } from 'svelte/transition';
  4. import { throttleWithRAF } from '../utils';
  5. import type { IndexBarProps } from '../../types';
  6. let {
  7. data = [],
  8. current = $bindable(0),
  9. top = 0,
  10. height = 100,
  11. radius = 'base',
  12. scrollAlign = true,
  13. titleInjClass = '',
  14. textInjClass = '',
  15. onclickChild,
  16. }: IndexBarProps = $props();
  17. // 用于绑定bar的高度
  18. // Used to bind the height of the bar
  19. let barHeight = $state(0);
  20. // 主体内容高度累计,用于监听滑动
  21. // The cumulative height of the main content is used to listen to the slide
  22. let longSumList = [];
  23. // 当前滑动的索引
  24. // Current sliding index
  25. let currentTouch = $state(-1);
  26. // 主体内容元素
  27. // Main body content element
  28. let bodyDom = $state(null);
  29. // Bar外层距离顶部的距离
  30. // Bar outside the distance from the top
  31. let barToTop = 0;
  32. // bar元素
  33. // bar element
  34. let barDom = $state(null);
  35. let isDown = false; //是否按下 is down
  36. // 每一个的高度
  37. // Height of each
  38. let itemHeight = $derived(barHeight / data.length);
  39. // 圆角风格样式
  40. // Rounded style style
  41. const radiusObj = { none: 'rounded-none', base: 'rounded', full: 'rounded-full' };
  42. onMount(() => {
  43. bodyDom.scrollTop = 0;
  44. for (let t = 0; t < data.length + 1; t++) {
  45. const long = data.slice(0, t).reduce((sum, current) => {
  46. return sum + current.height;
  47. }, 0);
  48. longSumList.push(long);
  49. }
  50. //如果索引内容区域高度不够,中英文报错
  51. //If the index content area height is not enough, Chinese and English errors
  52. if (height < barHeight) {
  53. console.error('[STDF IndexBar error]注意:索引内容区域高度不够。(Note: The height of the index content area is not enough.)');
  54. }
  55. barToTop = top + (height - barHeight) / 2;
  56. });
  57. //bar区域滑动开始
  58. //Bar area sliding start
  59. const touchBoxStart = e => {
  60. isDown = true;
  61. const clientY = e.clientY;
  62. currentTouch = Math.floor((clientY - barToTop) / itemHeight);
  63. current = Math.floor((clientY - barToTop) / itemHeight);
  64. bodyDom.scrollTop = data.slice(0, current).reduce((sum, current) => {
  65. // @ts-ignore
  66. return sum + current.height;
  67. }, 0);
  68. };
  69. // bar区域滑动中
  70. // bar area sliding in the middle
  71. const touchBoxMove = e => {
  72. if (!isDown) {
  73. return;
  74. }
  75. barDom.setPointerCapture(e.pointerId);
  76. const clientY = e.clientY;
  77. currentTouch = Math.floor((clientY - barToTop) / itemHeight);
  78. current = Math.floor((clientY - barToTop) / itemHeight);
  79. if (clientY < barToTop) {
  80. currentTouch = 0;
  81. current = 0;
  82. }
  83. if (clientY > barHeight + barToTop) {
  84. currentTouch = data.length - 1;
  85. current = data.length - 1;
  86. }
  87. bodyDom.scrollTop = data.slice(0, current).reduce((sum, current) => {
  88. // @ts-ignore
  89. return sum + current.height;
  90. }, 0);
  91. };
  92. // bar区域滑动结束
  93. // bar area sliding end
  94. const touchBoxEnd = () => {
  95. currentTouch = -1;
  96. isDown = false;
  97. };
  98. //监听主体内容滚动
  99. //Listen to the main content scroll
  100. const scrollBody = () => {
  101. const scrollTop = bodyDom.scrollTop + top;
  102. for (let w = 0; w < longSumList.length; w++) {
  103. if (scrollTop - top >= longSumList[w] && scrollTop - top < longSumList[w + 1]) {
  104. current = w;
  105. }
  106. }
  107. };
  108. </script>
  109. <div bind:this={bodyDom} class="overflow-y-auto {scrollAlign && 'snap-y'}" onscroll={scrollBody} style="height:{height}px;">
  110. {#each data as group, index}
  111. <div class="px-4 pt-8 snap-start" bind:clientHeight={group.height}>
  112. <div class="text-primary dark:text-dark text-sm {titleInjClass}">{group.title}</div>
  113. {#each group.child as child, childIndex}
  114. <button class="py-2 w-full text-left {textInjClass}" onclick={() => onclickChild && onclickChild(index, group, childIndex, child)}>
  115. {child}
  116. </button>
  117. <div class="h-px bg-black/5 dark:bg-white/5"></div>
  118. {/each}
  119. </div>
  120. {/each}
  121. </div>
  122. <div
  123. onpointerdown={touchBoxStart}
  124. onpointermove={e => throttleWithRAF(touchBoxMove)(e)}
  125. onpointerup={touchBoxEnd}
  126. bind:clientHeight={barHeight}
  127. bind:this={barDom}
  128. class="fixed right-5 bg-black/5 dark:bg-white/5 w-7 p-1 flex flex-col justify-around touch-none cursor-move select-none {radiusObj[
  129. radius
  130. ] || radiusObj.base}"
  131. style="top:{top + (height - barHeight) / 2}px;min-height:{height / 4}px;"
  132. >
  133. {#each data as group, i}
  134. <div class="flex-1 relative flex flex-col justify-center">
  135. <div
  136. class="w-5 h-5 leading-5 text-center text-xs transition-all {current === i
  137. ? 'bg-primary dark:bg-dark text-white dark:text-black'
  138. : 'text-gray-600 dark:text-gray-400'} {radiusObj[radius] || radiusObj.base}"
  139. >
  140. {group.index}
  141. </div>
  142. {#if currentTouch === i}
  143. <div
  144. class="absolute text-center -left-24 top-1/2 -translate-y-2/4 text-3xl border border-primary dark:border-dark w-14 h-14 leading-[3.5rem] text-primary dark:text-dark {radiusObj[
  145. radius
  146. ] || radiusObj.base}"
  147. transition:fly={{ x: 38, duration: 300 }}
  148. >
  149. {group.index}
  150. </div>
  151. {/if}
  152. </div>
  153. {/each}
  154. </div>