index.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. <template>
  2. <view
  3. class="nut-pullrefresh"
  4. ref="scroller"
  5. :style="getStyle"
  6. @touchstart="touchStart"
  7. @touchmove="touchMove"
  8. @touchend="touchEnd"
  9. >
  10. <view
  11. class="pullrefresh-top"
  12. :class="
  13. direction == 'horizontal' ? 'pullrefresh-top-h' : 'pullrefresh-top-v'
  14. "
  15. >
  16. {{ refreshTem }}
  17. <!-- <template
  18. v-if="status == 'loading' && (reachTop || reachLeft) && distance > 0"
  19. >{{ loadingText.top }}</template
  20. >
  21. <template
  22. v-if="status == 'pulling' && (reachTop || reachLeft) && distance > 0"
  23. >{{ pullingText.top }}</template
  24. >
  25. <template
  26. v-if="status == 'loosing' && (reachTop || reachLeft) && distance > 0"
  27. >{{ loosingText.top }}</template
  28. > -->
  29. </view>
  30. <view class="pullrefresh-content" ref="pull">
  31. <slot></slot>
  32. </view>
  33. <view
  34. class="pullrefresh-bottom"
  35. :class="
  36. direction == 'horizontal'
  37. ? 'pullrefresh-bottom-h'
  38. : 'pullrefresh-bottom-v'
  39. "
  40. :style="getBottomStyle"
  41. >
  42. <template
  43. v-if="
  44. status == 'loading' && (reachBottom || reachRight) && distance < 0
  45. "
  46. >{{ loadingText.bottom }}</template
  47. >
  48. <template
  49. v-if="
  50. status == 'pulling' && (reachBottom || reachRight) && distance < 0
  51. "
  52. >{{ pullingText.bottom }}</template
  53. >
  54. <template
  55. v-if="
  56. status == 'loosing' && (reachBottom || reachRight) && distance < 0
  57. "
  58. >{{ loosingText.bottom }}</template
  59. >
  60. </view>
  61. </view>
  62. </template>
  63. <script lang="ts">
  64. import { ref, toRefs, reactive, onMounted, computed, CSSProperties } from 'vue';
  65. import { createComponent } from '@/utils/create';
  66. import { useTouch } from './use-touch';
  67. import { preventDefault } from './util';
  68. const { componentName, create } = createComponent('pullrefresh');
  69. export default create({
  70. props: {
  71. useWindow: {
  72. type: Boolean,
  73. default: true
  74. },
  75. containerId: {
  76. type: String,
  77. default: ''
  78. },
  79. disabled: {
  80. type: Boolean,
  81. default: false
  82. },
  83. /** 方向 Horizontal */
  84. direction: {
  85. type: String,
  86. default: 'vertical'
  87. },
  88. pullingText: {
  89. type: Object,
  90. default: {
  91. top: '下拉刷新',
  92. bottom: '上拉加载',
  93. left: '左滑刷新',
  94. right: '右滑加载'
  95. }
  96. },
  97. loosingText: {
  98. type: Object,
  99. default: {
  100. top: '松手释放刷新',
  101. bottom: '松手释放刷新',
  102. left: '松手释放刷新',
  103. right: '松手释放刷新'
  104. }
  105. },
  106. loadingText: {
  107. type: Object,
  108. default: {
  109. top: '加载中...',
  110. bottom: '加载中...',
  111. left: '加载中...',
  112. right: '加载中...'
  113. }
  114. }
  115. },
  116. components: {},
  117. emits: ['refresh'],
  118. setup(props, { emit }) {
  119. console.log('componentName', componentName);
  120. const { containerId, useWindow, direction, disabled } = toRefs(props);
  121. const reachTop = ref(false);
  122. const reachBottom = ref(false);
  123. const reachLeft = ref(false);
  124. const reachRight = ref(false);
  125. const state = reactive({
  126. status: 'normal',
  127. distance: 0,
  128. duration: 0
  129. });
  130. let scrollEl: HTMLElement = document.documentElement || document.body;
  131. const scroller = ref<null | HTMLElement>(null);
  132. const touch = useTouch();
  133. const getStyle = computed(() => {
  134. let style: CSSProperties = {};
  135. const { deltaY, deltaX } = touch;
  136. if (
  137. direction.value == 'vertical' &&
  138. ((reachTop.value && deltaY.value > 0) ||
  139. (reachBottom.value && deltaY.value < 0)) &&
  140. touch.isVertical()
  141. ) {
  142. style = {
  143. transitionDuration: `${state.duration}ms`,
  144. transform: state.distance
  145. ? `translate3d(0,${state.distance}px, 0)`
  146. : `translate3d(0,0,0)`
  147. };
  148. }
  149. if (
  150. direction.value == 'horizontal' &&
  151. ((reachLeft.value && deltaX.value > 0) ||
  152. (reachRight.value && deltaX.value < 0)) &&
  153. touch.isHorizontal()
  154. ) {
  155. style = {
  156. transitionDuration: `${state.duration}ms`,
  157. transform: state.distance
  158. ? `translate3d(${state.distance}px, 0,0)`
  159. : `translate3d(0,0,0)`
  160. };
  161. }
  162. return style;
  163. });
  164. const getBottomStyle = computed(() => {
  165. let style: CSSProperties = {};
  166. if (
  167. direction.value == 'vertical' &&
  168. reachBottom.value &&
  169. touch.deltaY.value < 0 &&
  170. touch.isVertical()
  171. ) {
  172. const dis = Math.abs(state.distance) < 50 ? -state.distance : 50;
  173. style = {
  174. height: dis + 'px'
  175. };
  176. }
  177. if (
  178. direction.value == 'horizontal' &&
  179. reachRight.value &&
  180. touch.deltaX.value < 0 &&
  181. touch.isVertical()
  182. ) {
  183. const dis = Math.abs(state.distance) < 50 ? -state.distance : 50;
  184. style = {
  185. width: dis + 'px'
  186. };
  187. }
  188. return style;
  189. });
  190. const refreshTem = computed(() => {
  191. const { status, distance } = state;
  192. /** 刷新 顶部或左侧 */
  193. if (
  194. status == 'loading' &&
  195. (reachTop.value || reachLeft.value) &&
  196. distance > 0
  197. ) {
  198. return props.loadingText.top;
  199. }
  200. if (
  201. status == 'pulling' &&
  202. (reachTop.value || reachLeft.value) &&
  203. distance > 0
  204. ) {
  205. return props.pullingText.top;
  206. }
  207. if (
  208. status == 'loosing' &&
  209. (reachTop.value || reachLeft.value) &&
  210. distance > 0
  211. ) {
  212. return props.loosingText.top;
  213. }
  214. /** 刷新 底部或右侧 */
  215. return '';
  216. });
  217. const isTouchable = () => state.status !== 'loading' && !disabled.value;
  218. const setStatus = (distance: number, isLoading?: boolean) => {
  219. state.distance = distance;
  220. if (isLoading) {
  221. state.status = 'loading';
  222. } else if (distance === 0) {
  223. state.status = 'normal';
  224. } else if (Math.abs(distance) < 50) {
  225. state.status = 'pulling';
  226. } else {
  227. state.status = 'loosing';
  228. }
  229. };
  230. /** 获取监听自定义滚动节点 */
  231. const getParentElement = el => {
  232. if (containerId.value != '') {
  233. return document.querySelector(`#${containerId.value}`);
  234. }
  235. return el && el.parentNode;
  236. };
  237. /** 生命周期 首次加载 */
  238. onMounted(() => {
  239. const parentElement = getParentElement(scroller);
  240. let scrollElCopy = document.documentElement || document.body;
  241. if (useWindow.value === false) {
  242. scrollElCopy = parentElement;
  243. }
  244. scrollEl = scrollElCopy;
  245. });
  246. const ease = (distance: number) => {
  247. const headHeight = 50;
  248. if (distance > headHeight) {
  249. if (distance < headHeight * 2) {
  250. distance = headHeight + (distance - headHeight) / 2;
  251. } else {
  252. distance = headHeight * 1.5 + (distance - headHeight * 2) / 4;
  253. }
  254. }
  255. return Math.round(distance);
  256. };
  257. const refreshDone = () => {
  258. setStatus(0);
  259. };
  260. const touchStart = event => {
  261. if (isTouchable()) {
  262. if (direction.value == 'vertical') {
  263. /** 判断滚动条是否在顶部 */
  264. const top = 'scrollTop' in scrollEl ? scrollEl.scrollTop : 0;
  265. reachTop.value = Math.max(top, 0) == 0 ? true : false;
  266. /** 判断滚动条是否在底部*/
  267. const { scrollHeight, clientHeight, scrollTop } = scrollEl;
  268. reachBottom.value =
  269. clientHeight + scrollTop == scrollHeight ? true : false;
  270. if (reachTop.value || reachBottom.value) {
  271. state.duration = 0;
  272. touch.start(event);
  273. }
  274. } else {
  275. const { scrollWidth, clientWidth, scrollLeft } = scrollEl;
  276. /** 判断滚动条是否在左边 */
  277. const left = 'scrollLeft' in scrollEl ? scrollEl.scrollLeft : 0;
  278. reachLeft.value = Math.max(left, 0) == 0 ? true : false;
  279. /** 判断滚动条是否在右边 */
  280. reachRight.value =
  281. clientWidth + scrollLeft == scrollWidth ? true : false;
  282. if (reachLeft.value || reachRight.value) {
  283. state.duration = 0;
  284. touch.start(event);
  285. }
  286. }
  287. }
  288. };
  289. const touchMove = event => {
  290. if (isTouchable()) {
  291. const { deltaY, deltaX } = touch;
  292. touch.move(event);
  293. if (
  294. direction.value == 'vertical' &&
  295. ((reachBottom.value && deltaY.value < 0) ||
  296. (reachTop.value && deltaY.value >= 0)) &&
  297. touch.isVertical()
  298. ) {
  299. preventDefault(event);
  300. setStatus(ease(deltaY.value));
  301. }
  302. if (
  303. direction.value == 'horizontal' &&
  304. ((reachLeft.value && deltaX.value >= 0) ||
  305. (reachRight.value && deltaX.value < 0)) &&
  306. touch.isHorizontal()
  307. ) {
  308. preventDefault(event);
  309. setStatus(ease(deltaX.value));
  310. }
  311. }
  312. };
  313. const touchEnd = () => {
  314. if (isTouchable()) {
  315. const { deltaY, deltaX } = touch;
  316. if (state.status === 'loosing') {
  317. let dis = 0;
  318. if (
  319. direction.value == 'vertical' &&
  320. reachTop.value &&
  321. deltaY.value > 0
  322. ) {
  323. dis = 50;
  324. }
  325. if (
  326. direction.value == 'vertical' &&
  327. reachBottom.value &&
  328. deltaY.value < 0
  329. ) {
  330. dis = -50;
  331. }
  332. if (
  333. direction.value == 'horizontal' &&
  334. reachLeft.value &&
  335. deltaX.value > 0
  336. ) {
  337. dis = 50;
  338. }
  339. if (
  340. direction.value == 'horizontal' &&
  341. reachRight.value &&
  342. deltaX.value < 0
  343. ) {
  344. dis = -50;
  345. }
  346. setStatus(dis, true);
  347. emit('refresh', refreshDone);
  348. } else {
  349. setStatus(0);
  350. }
  351. }
  352. };
  353. return {
  354. scroller,
  355. touchStart,
  356. touchMove,
  357. touchEnd,
  358. getStyle,
  359. reachBottom,
  360. reachTop,
  361. reachRight,
  362. reachLeft,
  363. getBottomStyle,
  364. refreshTem,
  365. ...toRefs(state)
  366. };
  367. }
  368. });
  369. </script>
  370. <style lang="scss">
  371. @import 'index.scss';
  372. </style>