load-more.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. // [z-paging]滚动到底部加载更多模块
  2. import u from '.././z-paging-utils'
  3. import Enum from '.././z-paging-enum'
  4. export default {
  5. props: {
  6. // 自定义底部加载更多样式
  7. loadingMoreCustomStyle: {
  8. type: Object,
  9. default: u.gc('loadingMoreCustomStyle', {})
  10. },
  11. // 自定义底部加载更多文字样式
  12. loadingMoreTitleCustomStyle: {
  13. type: Object,
  14. default: u.gc('loadingMoreTitleCustomStyle', {})
  15. },
  16. // 自定义底部加载更多加载中动画样式
  17. loadingMoreLoadingIconCustomStyle: {
  18. type: Object,
  19. default: u.gc('loadingMoreLoadingIconCustomStyle', {})
  20. },
  21. // 自定义底部加载更多加载中动画图标类型,可选flower或circle,默认为flower
  22. loadingMoreLoadingIconType: {
  23. type: String,
  24. default: u.gc('loadingMoreLoadingIconType', 'flower')
  25. },
  26. // 自定义底部加载更多加载中动画图标图片
  27. loadingMoreLoadingIconCustomImage: {
  28. type: String,
  29. default: u.gc('loadingMoreLoadingIconCustomImage', '')
  30. },
  31. // 底部加载更多加载中view是否展示旋转动画,默认为是
  32. loadingMoreLoadingAnimated: {
  33. type: Boolean,
  34. default: u.gc('loadingMoreLoadingAnimated', true)
  35. },
  36. // 是否启用加载更多数据(含滑动到底部加载更多数据和点击加载更多数据),默认为是
  37. loadingMoreEnabled: {
  38. type: Boolean,
  39. default: u.gc('loadingMoreEnabled', true)
  40. },
  41. // 是否启用滑动到底部加载更多数据,默认为是
  42. toBottomLoadingMoreEnabled: {
  43. type: Boolean,
  44. default: u.gc('toBottomLoadingMoreEnabled', true)
  45. },
  46. // 滑动到底部状态为默认状态时,以加载中的状态展示,默认为否。若设置为是,可避免滚动到底部看到默认状态然后立刻变为加载中状态的问题,但分页数量未超过一屏时,不会显示【点击加载更多】
  47. loadingMoreDefaultAsLoading: {
  48. type: Boolean,
  49. default: u.gc('loadingMoreDefaultAsLoading', false)
  50. },
  51. // 滑动到底部"默认"文字,默认为【点击加载更多】
  52. loadingMoreDefaultText: {
  53. type: [String, Object],
  54. default: u.gc('loadingMoreDefaultText', null)
  55. },
  56. // 滑动到底部"加载中"文字,默认为【正在加载...】
  57. loadingMoreLoadingText: {
  58. type: [String, Object],
  59. default: u.gc('loadingMoreLoadingText', null)
  60. },
  61. // 滑动到底部"没有更多"文字,默认为【没有更多了】
  62. loadingMoreNoMoreText: {
  63. type: [String, Object],
  64. default: u.gc('loadingMoreNoMoreText', null)
  65. },
  66. // 滑动到底部"加载失败"文字,默认为【加载失败,点击重新加载】
  67. loadingMoreFailText: {
  68. type: [String, Object],
  69. default: u.gc('loadingMoreFailText', null)
  70. },
  71. // 当没有更多数据且分页内容未超出z-paging时是否隐藏没有更多数据的view,默认为否
  72. hideNoMoreInside: {
  73. type: Boolean,
  74. default: u.gc('hideNoMoreInside', false)
  75. },
  76. // 当没有更多数据且分页数组长度少于这个值时,隐藏没有更多数据的view,默认为0,代表不限制。
  77. hideNoMoreByLimit: {
  78. type: Number,
  79. default: u.gc('hideNoMoreByLimit', 0)
  80. },
  81. // 是否显示默认的加载更多text,默认为是
  82. showDefaultLoadingMoreText: {
  83. type: Boolean,
  84. default: u.gc('showDefaultLoadingMoreText', true)
  85. },
  86. // 是否显示没有更多数据的view
  87. showLoadingMoreNoMoreView: {
  88. type: Boolean,
  89. default: u.gc('showLoadingMoreNoMoreView', true)
  90. },
  91. // 是否显示没有更多数据的分割线,默认为是
  92. showLoadingMoreNoMoreLine: {
  93. type: Boolean,
  94. default: u.gc('showLoadingMoreNoMoreLine', true)
  95. },
  96. // 自定义底部没有更多数据的分割线样式
  97. loadingMoreNoMoreLineCustomStyle: {
  98. type: Object,
  99. default: u.gc('loadingMoreNoMoreLineCustomStyle', {})
  100. },
  101. // 当分页未满一屏时,是否自动加载更多,默认为否(nvue无效)
  102. insideMore: {
  103. type: Boolean,
  104. default: u.gc('insideMore', false)
  105. },
  106. // 距底部/右边多远时(单位px),触发 scrolltolower 事件,默认为100rpx
  107. lowerThreshold: {
  108. type: [Number, String],
  109. default: u.gc('lowerThreshold', '100rpx')
  110. },
  111. },
  112. data() {
  113. return {
  114. M: Enum.More,
  115. // 底部加载更多状态
  116. loadingStatus: Enum.More.Default,
  117. // 在渲染之后的底部加载更多状态
  118. loadingStatusAfterRender: Enum.More.Default,
  119. // 底部加载更多时间戳
  120. loadingMoreTimeStamp: 0,
  121. // 底部加载更多slot
  122. loadingMoreDefaultSlot: null,
  123. // 是否展示底部加载更多
  124. showLoadingMore: false,
  125. // 是否是开发者自定义的加载更多,-1代表交由z-paging自行判断;1代表没有更多了;0代表还有更多数据
  126. customNoMore: -1,
  127. }
  128. },
  129. computed: {
  130. // 底部加载更多配置
  131. zLoadMoreConfig() {
  132. return {
  133. status: this.loadingStatusAfterRender,
  134. defaultAsLoading: this.loadingMoreDefaultAsLoading || (this.useChatRecordMode && this.chatLoadingMoreDefaultAsLoading),
  135. defaultThemeStyle: this.finalLoadingMoreThemeStyle,
  136. customStyle: this.loadingMoreCustomStyle,
  137. titleCustomStyle: this.loadingMoreTitleCustomStyle,
  138. iconCustomStyle: this.loadingMoreLoadingIconCustomStyle,
  139. loadingIconType: this.loadingMoreLoadingIconType,
  140. loadingIconCustomImage: this.loadingMoreLoadingIconCustomImage,
  141. loadingAnimated: this.loadingMoreLoadingAnimated,
  142. showNoMoreLine: this.showLoadingMoreNoMoreLine,
  143. noMoreLineCustomStyle: this.loadingMoreNoMoreLineCustomStyle,
  144. defaultText: this.finalLoadingMoreDefaultText,
  145. loadingText: this.finalLoadingMoreLoadingText,
  146. noMoreText: this.finalLoadingMoreNoMoreText,
  147. failText: this.finalLoadingMoreFailText,
  148. hideContent: !this.loadingMoreDefaultAsLoading && this.listRendering,
  149. unit: this.unit,
  150. isChat: this.useChatRecordMode,
  151. chatDefaultAsLoading: this.chatLoadingMoreDefaultAsLoading
  152. };
  153. },
  154. // 最终的底部加载更多主题
  155. finalLoadingMoreThemeStyle() {
  156. return this.loadingMoreThemeStyle.length ? this.loadingMoreThemeStyle : this.defaultThemeStyle;
  157. },
  158. // 最终的底部加载更多触发阈值
  159. finalLowerThreshold() {
  160. return u.convertToPx(this.lowerThreshold);
  161. },
  162. // 是否显示默认状态下的底部加载更多
  163. showLoadingMoreDefault() {
  164. return this._showLoadingMore('Default');
  165. },
  166. // 是否显示加载中状态下的底部加载更多
  167. showLoadingMoreLoading() {
  168. return this._showLoadingMore('Loading');
  169. },
  170. // 是否显示没有更多了状态下的底部加载更多
  171. showLoadingMoreNoMore() {
  172. return this._showLoadingMore('NoMore');
  173. },
  174. // 是否显示加载失败状态下的底部加载更多
  175. showLoadingMoreFail() {
  176. return this._showLoadingMore('Fail');
  177. },
  178. // 是否显示自定义状态下的底部加载更多
  179. showLoadingMoreCustom() {
  180. return this._showLoadingMore('Custom');
  181. }
  182. },
  183. methods: {
  184. // 页面滚动到底部时通知z-paging进行进一步处理
  185. pageReachBottom() {
  186. !this.useChatRecordMode && this._onLoadingMore('toBottom');
  187. },
  188. // 手动触发上拉加载更多(非必须,可依据具体需求使用)
  189. doLoadMore(type) {
  190. this._onLoadingMore(type);
  191. },
  192. // 通过@scroll事件检测是否滚动到了底部(顺带检测下是否滚动到了顶部)
  193. _checkScrolledToBottom(scrollDiff, checked = false) {
  194. // 如果当前scroll-view高度未获取,则获取其高度
  195. if (this.cacheScrollNodeHeight === -1) {
  196. // 获取当前scroll-view高度
  197. this._getNodeClientRect('.zp-scroll-view').then((res) => {
  198. if (res) {
  199. const scrollNodeHeight = res[0].height;
  200. // 缓存当前scroll-view高度,如果获取过了不再获取
  201. this.cacheScrollNodeHeight = scrollNodeHeight;
  202. // // scrollDiff - this.cacheScrollNodeHeight = 当前滚动区域的顶部与内容底部的距离 - scroll-view高度 = 当前滚动区域的底部与内容底部的距离(也就是最终的与底部的距离)
  203. if (scrollDiff - scrollNodeHeight <= this.finalLowerThreshold) {
  204. // 如果与底部的距离小于阈值,则判断为滚动到了底部,触发滚动到底部事件
  205. this._onLoadingMore('toBottom');
  206. }
  207. }
  208. });
  209. } else {
  210. // scrollDiff - this.cacheScrollNodeHeight = 当前滚动区域的顶部与内容底部的距离 - scroll-view高度 = 当前滚动区域的底部与内容底部的距离(也就是最终的与底部的距离)
  211. if (scrollDiff - this.cacheScrollNodeHeight <= this.finalLowerThreshold) {
  212. // 如果与底部的距离小于阈值,则判断为滚动到了底部,触发滚动到底部事件
  213. this._onLoadingMore('toBottom');
  214. } else if (scrollDiff - this.cacheScrollNodeHeight <= 500 && !checked) {
  215. // 如果与底部的距离小于500px,则获取当前滚动的位置,延迟150毫秒重复上述步骤再次检测(避免@scroll触发时获取的scrollTop不正确导致的其他问题,此时获取的scrollTop不一定可信)。防止因为部分性能较差安卓设备@scroll采样率过低导致的滚动到底部但是依然没有触发的问题
  216. u.delay(() => {
  217. this._getNodeClientRect('.zp-scroll-view', true, true).then((res) => {
  218. if (res) {
  219. this.oldScrollTop = res[0].scrollTop;
  220. const newScrollDiff = res[0].scrollHeight - this.oldScrollTop;
  221. this._checkScrolledToBottom(newScrollDiff, true);
  222. }
  223. })
  224. }, 150, 'checkScrolledToBottomDelay')
  225. }
  226. // 检测一下是否已经滚动到了顶部了,因为在安卓中滚动到顶部时scrollTop不一定为0(和滚动到底部一样的原因),所以需要在scrollTop小于150px时,通过获取.zp-scroll-view的scrollTop再判断一下
  227. if (this.oldScrollTop <= 150 && this.oldScrollTop !== 0) {
  228. u.delay(() => {
  229. // 这里再判断一下是否确实已经滚动到顶部了,如果已经滚动到顶部了,则不用再判断了,再次判断的原因是可能150毫秒之后oldScrollTop才是0
  230. if (this.oldScrollTop !== 0) {
  231. this._getNodeClientRect('.zp-scroll-view', true, true).then((res) => {
  232. // 如果150毫秒后.zp-scroll-view的scrollTop为0,则认为已经滚动到了顶部了
  233. if (res && res[0].scrollTop === 0 && this.oldScrollTop !== 0) {
  234. this._onScrollToUpper();
  235. }
  236. })
  237. }
  238. }, 150, 'checkScrolledToTopDelay')
  239. }
  240. }
  241. },
  242. // 触发加载更多时调用,from:toBottom-滑动到底部触发;1、click-点击加载更多触发
  243. _onLoadingMore(from = 'click') {
  244. // 如果是ios并且是滚动到底部的,则在滚动到底部时候尝试将列表设置为禁止滚动然后设置为允许滚动,以禁止底部bounce的效果
  245. if (this.isIos && from === 'toBottom' && !this.scrollToBottomBounceEnabled && this.scrollEnable) {
  246. this.scrollEnable = false;
  247. this.$nextTick(() => {
  248. this.scrollEnable = true;
  249. })
  250. }
  251. // emit scrolltolower
  252. this.$emit('scrolltolower', from);
  253. // 如果是只使用下拉刷新 或者 禁用底部加载更多 或者 底部加载更多不是默认状态或加载失败状态 或者 是加载中状态 或者 空数据图已经展示了,则return,不触发内部加载更多逻辑
  254. if (this.refresherOnly || !this.loadingMoreEnabled || !(this.loadingStatus === Enum.More.Default || this.loadingStatus === Enum.More.Fail) || this.loading || this.showEmpty) return;
  255. // #ifdef MP-WEIXIN
  256. if (!this.isIos && !this.refresherOnly && !this.usePageScroll) {
  257. const currentTimestamp = u.getTime();
  258. // 在非ios平台+scroll-view中节流处理
  259. if (this.loadingMoreTimeStamp > 0 && currentTimestamp - this.loadingMoreTimeStamp < 100) {
  260. this.loadingMoreTimeStamp = 0;
  261. return;
  262. }
  263. }
  264. // #endif
  265. // 处理加载更多数据
  266. this._doLoadingMore();
  267. },
  268. // 处理开始加载更多
  269. _doLoadingMore() {
  270. if (this.pageNo >= this.defaultPageNo && this.loadingStatus !== Enum.More.NoMore) {
  271. this.pageNo ++;
  272. this._startLoading(false);
  273. if (this.isLocalPaging) {
  274. // 如果是本地分页,则在组件内部对数据进行分页处理,不触发@query事件
  275. this._localPagingQueryList(this.pageNo, this.defaultPageSize, this.localPagingLoadingTime, res => {
  276. this.completeByTotal(res, this.totalLocalPagingList.length);
  277. this.queryFrom = Enum.QueryFrom.LoadingMore;
  278. })
  279. } else {
  280. // emit @query相关加载更多事件
  281. this._emitQuery(this.pageNo, this.defaultPageSize, Enum.QueryFrom.LoadingMore);
  282. this._callMyParentQuery();
  283. }
  284. // 设置当前加载状态为底部加载更多状态
  285. this.loadingType = Enum.LoadingType.LoadingMore;
  286. }
  287. },
  288. // (预处理)判断当没有更多数据且分页内容未超出z-paging时是否显示没有更多数据的view
  289. _preCheckShowNoMoreInside(newVal, scrollViewNode, pagingContainerNode) {
  290. if (this.loadingStatus === Enum.More.NoMore && this.hideNoMoreByLimit > 0 && newVal.length) {
  291. this.showLoadingMore = newVal.length > this.hideNoMoreByLimit;
  292. } else if ((this.loadingStatus === Enum.More.NoMore && this.hideNoMoreInside && newVal.length) || (this.insideMore && this.insideOfPaging !== false && newVal.length)) {
  293. this.$nextTick(() => {
  294. this._checkShowNoMoreInside(newVal, scrollViewNode, pagingContainerNode);
  295. })
  296. if (this.insideMore && this.insideOfPaging !== false && newVal.length) {
  297. this.showLoadingMore = newVal.length;
  298. }
  299. } else {
  300. this.showLoadingMore = newVal.length;
  301. }
  302. },
  303. // 判断当没有更多数据且分页内容未超出z-paging时是否显示没有更多数据的view
  304. async _checkShowNoMoreInside(totalData, oldScrollViewNode, oldPagingContainerNode) {
  305. try {
  306. const scrollViewNode = oldScrollViewNode || await this._getNodeClientRect('.zp-scroll-view');
  307. // 在页面滚动模式下
  308. if (this.usePageScroll) {
  309. if (scrollViewNode) {
  310. // 获取滚动内容总高度
  311. const scrollViewTotalH = scrollViewNode[0].top + scrollViewNode[0].height;
  312. // 如果滚动内容总高度小于窗口高度,则认为内容未超出z-paging
  313. this.insideOfPaging = scrollViewTotalH < this.windowHeight;
  314. // 如果需要没有更多数据时,隐藏底部加载更多view,并且内容未超过z-paging,则隐藏底部加载更多
  315. if (this.hideNoMoreInside) {
  316. this.showLoadingMore = !this.insideOfPaging;
  317. }
  318. // 如果需要内容未超过z-paging时自动加载更多,则触发加载更多
  319. this._updateInsideOfPaging();
  320. }
  321. } else {
  322. // 在scroll-view滚动模式下
  323. const pagingContainerNode = oldPagingContainerNode || await this._getNodeClientRect('.zp-paging-container-content');
  324. // 获取滚动内容总高度
  325. const pagingContainerH = pagingContainerNode ? pagingContainerNode[0].height : 0;
  326. // 获取z-paging内置scroll-view高度
  327. const scrollViewH = scrollViewNode ? scrollViewNode[0].height : 0;
  328. // 如果滚动内容总高度小于z-paging内置scroll-view高度,则认为内容未超出z-paging
  329. this.insideOfPaging = pagingContainerH < scrollViewH;
  330. if (this.hideNoMoreInside) {
  331. this.showLoadingMore = !this.insideOfPaging;
  332. }
  333. // 如果需要内容未超过z-paging时自动加载更多,则触发加载更多
  334. this._updateInsideOfPaging();
  335. }
  336. } catch (e) {
  337. // 如果发生了异常,判断totalData数组长度为0,则认为内容未超出z-paging
  338. this.insideOfPaging = !totalData.length;
  339. if (this.hideNoMoreInside) {
  340. this.showLoadingMore = !this.insideOfPaging;
  341. }
  342. // 如果需要内容未超过z-paging时自动加载更多,则触发加载更多
  343. this._updateInsideOfPaging();
  344. }
  345. },
  346. // 是否要展示上拉加载更多view
  347. _showLoadingMore(type) {
  348. if (!this.showLoadingMoreWhenReload && (!(this.loadingStatus === Enum.More.Default ? this.nShowBottom : true) || !this.realTotalData.length)) return false;
  349. if (((!this.showLoadingMoreWhenReload || this.isUserPullDown || this.loadingStatus !== Enum.More.Loading) && !this.showLoadingMore) ||
  350. (!this.loadingMoreEnabled && (!this.showLoadingMoreWhenReload || this.isUserPullDown || this.loadingStatus !== Enum.More.Loading)) || this.refresherOnly) {
  351. return false;
  352. }
  353. if (this.useChatRecordMode && type !== 'Loading') return false;
  354. if (!this.zSlots) return false;
  355. if (type === 'Custom') {
  356. return this.showDefaultLoadingMoreText && !(this.loadingStatus === Enum.More.NoMore && !this.showLoadingMoreNoMoreView);
  357. }
  358. const res = this.loadingStatus === Enum.More[type] && this.zSlots[`loadingMore${type}`] && (type === 'NoMore' ? this.showLoadingMoreNoMoreView : true);
  359. if (res) {
  360. // #ifdef APP-NVUE
  361. if (!this.isIos) {
  362. this.nLoadingMoreFixedHeight = false;
  363. }
  364. // #endif
  365. }
  366. return res;
  367. },
  368. }
  369. }