index.vue 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. <template>
  2. <view :class="classes" ref="scroller" @touchstart="touchStart" @touchmove="touchMove" @touchend="touchEnd">
  3. <view class="nut-infinite-top" ref="refreshTop" :style="getStyle">
  4. <view class="top-box">
  5. <nut-icon class="top-img" :name="pullIcon"></nut-icon>
  6. <view class="top-text">{{ pullTxt || translate('pullTxt') }}</view>
  7. </view>
  8. </view>
  9. <view class="nut-infinite-container">
  10. <slot></slot>
  11. </view>
  12. <view class="nut-infinite-bottom">
  13. <template v-if="isInfiniting">
  14. <view class="bottom-box">
  15. <template v-if="!slots.loading">
  16. <nut-icon class="bottom-img" :name="loadIcon"></nut-icon>
  17. <view class="bottom-text">{{ loadTxt || translate('loading') }}</view>
  18. </template>
  19. <slot name="loading" v-else></slot>
  20. </view>
  21. </template>
  22. <template v-else-if="!hasMore">
  23. <view class="tips" v-if="!slots.finished">{{ loadMoreTxt || translate('loadMoreTxt') }}</view>
  24. <slot name="finished" v-else></slot>
  25. </template>
  26. </view>
  27. </view>
  28. </template>
  29. <script lang="ts">
  30. import {
  31. toRefs,
  32. onMounted,
  33. onUnmounted,
  34. reactive,
  35. computed,
  36. CSSProperties,
  37. onActivated,
  38. onDeactivated,
  39. ref
  40. } from 'vue';
  41. import { createComponent } from '@/packages/utils/create';
  42. const { componentName, create, translate } = createComponent('infiniteloading');
  43. export default create({
  44. props: {
  45. hasMore: {
  46. type: Boolean,
  47. default: true
  48. },
  49. threshold: {
  50. type: Number,
  51. default: 200
  52. },
  53. pullIcon: {
  54. type: String,
  55. default: 'https://img10.360buyimg.com/imagetools/jfs/t1/169863/6/4565/6306/60125948E7e92774e/40b3a0cf42852bcb.png'
  56. },
  57. pullTxt: {
  58. type: String,
  59. default: ''
  60. },
  61. loadIcon: {
  62. type: String,
  63. default: 'https://img10.360buyimg.com/imagetools/jfs/t1/169863/6/4565/6306/60125948E7e92774e/40b3a0cf42852bcb.png'
  64. },
  65. loadTxt: {
  66. type: String,
  67. default: ''
  68. },
  69. loadMoreTxt: {
  70. type: String,
  71. default: ''
  72. },
  73. useWindow: {
  74. type: Boolean,
  75. default: true
  76. },
  77. containerId: {
  78. type: String,
  79. default: ''
  80. },
  81. useCapture: {
  82. type: Boolean,
  83. default: false
  84. },
  85. isOpenRefresh: {
  86. type: Boolean,
  87. default: false
  88. }
  89. },
  90. emits: ['scroll-change', 'load-more', 'refresh'],
  91. setup(props, { emit, slots }) {
  92. const state = reactive({
  93. scrollEl: window as Window | HTMLElement | (Node & ParentNode),
  94. scroller: null as null | HTMLElement,
  95. refreshTop: null as null | HTMLElement,
  96. beforeScrollTop: 0,
  97. isTouching: false,
  98. isInfiniting: false,
  99. refreshMaxH: 0,
  100. y: 0,
  101. x: 0,
  102. distance: 0
  103. });
  104. const classes = computed(() => {
  105. const prefixCls = componentName;
  106. return {
  107. [prefixCls]: true
  108. };
  109. });
  110. const getStyle = computed(() => {
  111. const style: CSSProperties = {};
  112. return {
  113. height: state.distance < 0 ? `0px` : `${state.distance}px`,
  114. transition: state.isTouching
  115. ? `height 0s cubic-bezier(0.25,0.1,0.25,1)`
  116. : `height 0.2s cubic-bezier(0.25,0.1,0.25,1)`
  117. };
  118. });
  119. const requestAniFrame = () => {
  120. return (
  121. window.requestAnimationFrame ||
  122. window.webkitRequestAnimationFrame ||
  123. function (callback) {
  124. window.setTimeout(callback, 1000 / 60);
  125. }
  126. );
  127. };
  128. const getWindowScrollTop = () => {
  129. return window.pageYOffset !== undefined
  130. ? window.pageYOffset
  131. : (document.documentElement || document.body.parentNode || document.body).scrollTop;
  132. };
  133. const calculateTopPosition = (el: HTMLElement): number => {
  134. return !el ? 0 : el.offsetTop + calculateTopPosition(el.offsetParent as HTMLElement);
  135. };
  136. const isScrollAtBottom = () => {
  137. let offsetDistance = 0;
  138. let resScrollTop = 0;
  139. let direction = 'down';
  140. const windowScrollTop = getWindowScrollTop();
  141. if (props.useWindow) {
  142. if (state.scroller) {
  143. offsetDistance =
  144. calculateTopPosition(state.scroller) + state.scroller.offsetHeight - windowScrollTop - window.innerHeight;
  145. }
  146. resScrollTop = windowScrollTop;
  147. } else {
  148. const { scrollHeight, clientHeight, scrollTop } = state.scrollEl as HTMLElement;
  149. offsetDistance = scrollHeight - clientHeight - scrollTop;
  150. resScrollTop = scrollTop;
  151. }
  152. if (state.beforeScrollTop > resScrollTop) {
  153. direction = 'up';
  154. } else {
  155. direction = 'down';
  156. }
  157. state.beforeScrollTop = resScrollTop;
  158. emit('scroll-change', resScrollTop);
  159. return offsetDistance <= props.threshold && direction == 'down';
  160. };
  161. const infiniteDone = () => {
  162. state.isInfiniting = false;
  163. };
  164. const handleScroll = () => {
  165. requestAniFrame()(() => {
  166. if (!isScrollAtBottom() || !props.hasMore || state.isInfiniting) {
  167. return false;
  168. } else {
  169. state.isInfiniting = true;
  170. emit('load-more', infiniteDone);
  171. }
  172. });
  173. };
  174. const scrollListener = () => {
  175. state.scrollEl.addEventListener('scroll', handleScroll, props.useCapture);
  176. };
  177. const refreshDone = () => {
  178. state.distance = 0;
  179. state.isTouching = false;
  180. };
  181. const touchStart = (event: TouchEvent) => {
  182. if (state.beforeScrollTop == 0 && !state.isTouching && props.isOpenRefresh) {
  183. state.y = event.touches[0].pageY;
  184. state.isTouching = true;
  185. const childHeight = ((state.refreshTop as HTMLElement).firstElementChild as HTMLElement).offsetHeight;
  186. state.refreshMaxH = Math.floor(childHeight * 1 + 10);
  187. }
  188. };
  189. const touchMove = (event: TouchEvent) => {
  190. state.distance = event.touches[0].pageY - state.y;
  191. if (state.distance > 0 && state.isTouching) {
  192. event.preventDefault();
  193. if (state.distance >= state.refreshMaxH) state.distance = state.refreshMaxH;
  194. } else {
  195. state.distance = 0;
  196. state.isTouching = false;
  197. }
  198. };
  199. const touchEnd = () => {
  200. if (state.distance) {
  201. if (state.distance < state.refreshMaxH) {
  202. state.distance = 0;
  203. } else {
  204. emit('refresh', refreshDone);
  205. }
  206. }
  207. };
  208. // 滚动监听对象
  209. const getParentElement = (el: HTMLElement) => {
  210. return !!props.containerId ? document.querySelector(`#${props.containerId}`) : el && el.parentNode;
  211. };
  212. onMounted(() => {
  213. const parentElement = getParentElement(state.scroller as HTMLElement) as Node & ParentNode;
  214. state.scrollEl = props.useWindow ? window : parentElement;
  215. scrollListener();
  216. console.log(slots);
  217. });
  218. onUnmounted(() => {
  219. state.scrollEl.removeEventListener('scroll', handleScroll, props.useCapture);
  220. });
  221. const isKeepAlive = ref(false);
  222. onActivated(() => {
  223. if (isKeepAlive.value) {
  224. isKeepAlive.value = false;
  225. scrollListener();
  226. }
  227. });
  228. onDeactivated(() => {
  229. isKeepAlive.value = true;
  230. state.scrollEl.removeEventListener('scroll', handleScroll, props.useCapture);
  231. });
  232. return {
  233. classes,
  234. ...toRefs(state),
  235. touchStart,
  236. touchMove,
  237. touchEnd,
  238. getStyle,
  239. translate,
  240. slots
  241. };
  242. }
  243. });
  244. </script>