index.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. import baseComponent from '../helpers/baseComponent'
  2. import classNames from '../helpers/libs/classNames'
  3. import { getSystemInfoSync } from '../helpers/hooks/useNativeAPI'
  4. import { useRect } from '../helpers/hooks/useDOM'
  5. const defaultStyle = 'transition: transform .4s; transform: translate3d(0px, 0px, 0px) scale(1);'
  6. baseComponent({
  7. properties: {
  8. prefixCls: {
  9. type: String,
  10. value: 'wux-refresher',
  11. },
  12. pullingIcon: {
  13. type: String,
  14. value: '',
  15. },
  16. pullingText: {
  17. type: String,
  18. value: '下拉刷新',
  19. },
  20. refreshingIcon: {
  21. type: String,
  22. value: '',
  23. },
  24. refreshingText: {
  25. type: String,
  26. value: '正在刷新',
  27. },
  28. disablePullingRotation: {
  29. type: Boolean,
  30. value: false,
  31. },
  32. distance: {
  33. type: Number,
  34. value: 30,
  35. },
  36. prefixLCls: {
  37. type: String,
  38. value: 'wux-loader',
  39. },
  40. isShowLoadingText: {
  41. type: Boolean,
  42. value: false,
  43. },
  44. loadingText: {
  45. type: String,
  46. value: '正在加载',
  47. },
  48. loadNoDataText: {
  49. type: String,
  50. value: '没有更多数据',
  51. },
  52. scrollTop: {
  53. type: Number,
  54. value: 0,
  55. observer: 'onScroll',
  56. },
  57. },
  58. data: {
  59. style: defaultStyle,
  60. visible: false,
  61. active: false,
  62. refreshing: false,
  63. tail: false,
  64. lVisible: false,
  65. noData: false, // 是否没有更多数据
  66. windowHeight: 0, // 窗口高度
  67. newContentHeight: 0, // 新节点内容高度
  68. oldContentHeight: 0, // 旧节点内容高度
  69. loading: false, // 判断是否正在加载
  70. },
  71. computed: {
  72. classes: ['prefixCls, pullingText, pullingIcon, disablePullingRotation, refreshingText, refreshingIcon, visible, active, refreshing, tail, prefixLCls, loading, noData', function(prefixCls, pullingText, pullingIcon, disablePullingRotation, refreshingText, refreshingIcon, visible, active, refreshing, tail, prefixLCls, loading, noData) {
  73. const wrap = classNames(prefixCls, {
  74. [`${prefixCls}--hidden`]: !visible,
  75. [`${prefixCls}--visible`]: visible,
  76. [`${prefixCls}--active`]: active,
  77. [`${prefixCls}--refreshing`]: refreshing,
  78. [`${prefixCls}--refreshing-tail`]: tail,
  79. })
  80. const cover = `${prefixCls}__cover`
  81. const content = classNames(`${prefixCls}__content`, {
  82. [`${prefixCls}__content--text`]: pullingText || refreshingText,
  83. })
  84. const iconPulling = classNames(`${prefixCls}__icon-pulling`, {
  85. [`${prefixCls}__icon-pulling--disabled`]: disablePullingRotation,
  86. })
  87. const textPulling = `${prefixCls}__text-pulling`
  88. const iconRefreshing = `${prefixCls}__icon-refreshing`
  89. const textRefreshing = `${prefixCls}__text-refreshing`
  90. const pIcon = pullingIcon || `${prefixCls}__icon--arrow-down`
  91. const rIcon = refreshingIcon || `${prefixCls}__icon--refresher`
  92. const lWrap = classNames(prefixLCls, {
  93. [`${prefixLCls}--hidden`]: !loading,
  94. [`${prefixLCls}--visible`]: loading,
  95. [`${prefixLCls}--end`]: noData,
  96. })
  97. const lContent = `${prefixLCls}__content`
  98. const loadingText = `${prefixLCls}__text-loading`
  99. return {
  100. wrap,
  101. cover,
  102. content,
  103. iconPulling,
  104. textPulling,
  105. iconRefreshing,
  106. textRefreshing,
  107. pIcon,
  108. rIcon,
  109. lWrap,
  110. lContent,
  111. loadingText,
  112. }
  113. }],
  114. },
  115. methods: {
  116. /**
  117. * 显示
  118. */
  119. activate() {
  120. this.setData({
  121. style: defaultStyle,
  122. visible: true,
  123. })
  124. },
  125. /**
  126. * 隐藏
  127. */
  128. deactivate() {
  129. if (this.activated) this.activated = false
  130. this.setData({
  131. style: defaultStyle,
  132. visible: false,
  133. active: false,
  134. refreshing: false,
  135. tail: false,
  136. })
  137. },
  138. /**
  139. * 正在刷新
  140. */
  141. refreshing() {
  142. this.setData({
  143. style: 'transition: transform .4s; transform: translate3d(0, 50px, 0) scale(1);',
  144. visible: true,
  145. active: true,
  146. refreshing: true,
  147. // 刷新时重新初始化加载状态
  148. loading: false,
  149. noData: false,
  150. newContentHeight: 0,
  151. oldContentHeight: 0,
  152. lVisible: false,
  153. })
  154. },
  155. /**
  156. * 刷新后隐藏动画
  157. */
  158. tail() {
  159. this.setData({
  160. visible: true,
  161. active: true,
  162. refreshing: true,
  163. tail: true,
  164. })
  165. },
  166. /**
  167. * 加载后隐藏动画
  168. */
  169. hide() {
  170. this.setData({
  171. lVisible: false,
  172. })
  173. },
  174. /**
  175. * 正在下拉
  176. * @param {Number} diffY 距离
  177. */
  178. translate(diffY) {
  179. const style = `transition-duration: 0s; transform: translate3d(0, ${diffY}px, 0) scale(1);`
  180. const className = diffY < this.data.distance ? 'visible' : 'active'
  181. this.setData({
  182. style,
  183. [className]: true,
  184. })
  185. },
  186. /**
  187. * 判断是否正在刷新
  188. */
  189. isRefreshing() {
  190. return this.data.refreshing
  191. },
  192. /**
  193. * 判断是否正在加载
  194. */
  195. isLoading() {
  196. return this.data.loading
  197. },
  198. /**
  199. * 获取触摸点坐标
  200. */
  201. getTouchPoints(e, index = 0) {
  202. const { pageX: x, pageY: y } = e.touches[index] || e.changedTouches[index]
  203. return {
  204. x,
  205. y,
  206. }
  207. },
  208. /**
  209. * 获取触摸移动方向
  210. */
  211. getSwipeDirection(x1, x2, y1, y2) {
  212. return Math.abs(x1 - x2) >= Math.abs(y1 - y2) ? (x1 - x2 > 0 ? 'Left' : 'Right') : (y1 - y2 > 0 ? 'Up' : 'Down')
  213. },
  214. /**
  215. * 创建定时器
  216. */
  217. requestAnimationFrame(callback) {
  218. let currTime = new Date().getTime()
  219. let timeToCall = Math.max(0, 16 - (currTime - this.lastTime))
  220. let timeout = setTimeout(() => {
  221. callback.bind(this)(currTime + timeToCall)
  222. }, timeToCall)
  223. this.lastTime = currTime + timeToCall
  224. return timeout
  225. },
  226. /**
  227. * 清空定时器
  228. */
  229. cancelAnimationFrame(timeout) {
  230. clearTimeout(timeout)
  231. },
  232. /**
  233. * 下拉刷新完成后的函数
  234. */
  235. finishPullToRefresh() {
  236. setTimeout(() => {
  237. this.requestAnimationFrame(this.tail)
  238. setTimeout(() => this.deactivate(), 200)
  239. }, 200)
  240. },
  241. /**
  242. * 上拉加载完成后的函数
  243. */
  244. finishLoadmore(isEnd) {
  245. if (isEnd === true) {
  246. setTimeout(() => {
  247. this.setData({
  248. noData: true,
  249. loading: false,
  250. })
  251. }, 200)
  252. } else {
  253. setTimeout(() => {
  254. this.setData({
  255. noData: false,
  256. loading: false,
  257. })
  258. this.requestAnimationFrame(this.hide)
  259. setTimeout(() => this.deactivate(), 200)
  260. }, 200)
  261. }
  262. },
  263. /**
  264. * 手指触摸动作开始
  265. */
  266. onTouchStart(e) {
  267. if (this.isRefreshing() || this.isLoading()) return
  268. this.start = this.getTouchPoints(e)
  269. this.diffX = this.diffY = 0
  270. this.isMoved = false
  271. this.direction = false
  272. this.activate()
  273. },
  274. /**
  275. * 手指触摸后移动
  276. */
  277. onTouchMove(e) {
  278. if (!this.start || this.isRefreshing() || this.isLoading()) return
  279. if (!this.isMoved) {
  280. this.isMoved = true
  281. }
  282. this.move = this.getTouchPoints(e)
  283. this.diffX = this.move.x - this.start.x
  284. this.diffY = this.move.y - this.start.y
  285. this.direction = this.getSwipeDirection(this.start.x, this.move.x, this.start.y, this.move.y)
  286. if (this.diffY < 0 || this.direction !== 'Down') return
  287. this.diffY = Math.pow(this.diffY, 0.8)
  288. this.triggerPull(this.diffY)
  289. },
  290. /**
  291. * 手指触摸动作结束
  292. */
  293. onTouchEnd(e) {
  294. if (!this.isMoved) return
  295. this.start = false
  296. this.isMoved = false
  297. if (this.diffY <= 0 || this.direction !== 'Down' || this.isRefreshing() || this.isLoading()) return
  298. this.triggerRefresh(this.diffY)
  299. },
  300. /**
  301. * Pulling
  302. */
  303. triggerPull(diffY) {
  304. const { distance } = this.data
  305. if (!this.activated && diffY > distance) {
  306. this.activated = true
  307. this.triggerEvent('pulling')
  308. } else if (this.activated && diffY < distance) {
  309. this.activated = false
  310. }
  311. this.translate(diffY)
  312. },
  313. /**
  314. * Refresh
  315. */
  316. triggerRefresh(diffY = this.data.distance) {
  317. this.triggerPull(diffY)
  318. this.deactivate()
  319. if (Math.abs(diffY) >= this.data.distance) {
  320. this.refreshing()
  321. this.triggerEvent('refresh')
  322. }
  323. },
  324. /**
  325. * Scrool
  326. */
  327. onScroll(n) {
  328. // disabled scroll func
  329. if (this.isMoved) return
  330. // 获取节点高度
  331. useRect(`#${this.id}`)
  332. .then((res) => {
  333. const newContentHeight = res.height
  334. if (this.data.newContentHeight !== newContentHeight) {
  335. this.setData({ newContentHeight })
  336. }
  337. const {
  338. oldContentHeight,
  339. windowHeight,
  340. distance,
  341. loading,
  342. noData,
  343. } = this.data
  344. if (windowHeight && !this.isRefreshing()) {
  345. // 到临界点时触发上拉加载
  346. // 防止节点高度一致时引发重复加载
  347. if (
  348. n > newContentHeight - windowHeight - (distance * 1.5) &&
  349. loading === false &&
  350. noData === false && newContentHeight !== oldContentHeight
  351. ) {
  352. this.setData({
  353. loading: true,
  354. refreshing: false,
  355. oldContentHeight: newContentHeight,
  356. })
  357. this.triggerEvent('loadmore')
  358. } else if (
  359. loading === false &&
  360. noData === false
  361. ) {
  362. // 隐藏上拉加载动画
  363. this.hide()
  364. } else if (loading === true) {
  365. // 如果在加载中,保持内容的高度一致,以此来防止临界点重复加载
  366. this.setData({
  367. oldContentHeight: newContentHeight,
  368. })
  369. }
  370. this.deactivate()
  371. }
  372. })
  373. },
  374. noop() {},
  375. },
  376. created() {
  377. this.lastTime = 0
  378. this.activated = false
  379. },
  380. attached() {
  381. const windowHeight = getSystemInfoSync(['window']).windowHeight
  382. if (this.data.windowHeight !== windowHeight) {
  383. this.setData({
  384. windowHeight,
  385. })
  386. }
  387. },
  388. })