customScrollList.vue 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667
  1. <template>
  2. <view class="scroll-list-wrap" :style="[scrollListWrapStyle]">
  3. <scroll-view
  4. class="scroll-view"
  5. :class="[elClass]"
  6. :style="[listWrapStyle]"
  7. scroll-y
  8. scroll-anchoring
  9. enable-back-to-top
  10. :scroll-top="scrollTop"
  11. :lower-threshold="defaultOption.lowerThreshold"
  12. @scroll="handleScroll"
  13. @touchend="handleTouchEnd"
  14. @touchmove.prevent.stop="handleTouchMove"
  15. @touchstart="handleTouchStart"
  16. @scrolltolower="handleScrolltolower"
  17. >
  18. <view class="scroll-content" :style="[scrollContentStyle]">
  19. <view class="pull-down-wrap">
  20. <slot name="pulldown" v-if="$slots.pulldown"></slot>
  21. <view class="refresh-view" :style="[refreshViewStyle]" v-else>
  22. <view class="pull-down-animation" :class="{ refreshing: refreshing }" :style="[pullDownAnimationStyle]"></view>
  23. <text class="pull-down-text" :style="[pullDownTextStyle]">{{ refreshStateText }}</text>
  24. </view>
  25. </view>
  26. <view class="empty-wrap" v-if="showEmpty">
  27. <slot name="empty" v-if="$slots.empty"></slot>
  28. <view class="empty-view" v-else>
  29. <image class="empty-image" :src="defaultOption.emptyImage || images.empty" mode="aspectFit"></image>
  30. <text class="empty-text" :style="[emptyTextStyle]">{{ emptyText }}</text>
  31. </view>
  32. </view>
  33. <view class="list-content"><slot></slot></view>
  34. <view class="pull-up-wrap" v-if="showPullUp">
  35. <slot name="pullup" v-if="$slots.pullup"></slot>
  36. <view class="load-view" v-else>
  37. <view class="pull-up-animation" v-if="loading" :style="[pullUpAnimationStyle]"></view>
  38. <text class="pull-up-text" :style="[pullUpTextStyle]">{{ loadStateText }}</text>
  39. </view>
  40. </view>
  41. </view>
  42. </scroll-view>
  43. </view>
  44. </template>
  45. <script>
  46. import images from './images.js';
  47. export default {
  48. name: 'scroll-list',
  49. props: {
  50. // 配置信息
  51. option: {
  52. type: Object,
  53. default: () => ({})
  54. }
  55. },
  56. data() {
  57. return {
  58. defaultOption: {
  59. page: 1, // 分页
  60. size: 15, // 分页大小
  61. auto: true, // 自动加载
  62. height: null, // 组件高度
  63. disabled: false, // 禁用
  64. background: '', // 背景颜色属性
  65. emptyImage: '', // 空数据提示图片
  66. offsetBottom: 0, // 底部高度补偿
  67. pullDownSpeed: 0.5, // 下拉速率
  68. lowerThreshold: 40, // 距离底部上拉加载距离
  69. refresherThreshold: 80, // 距离顶部下拉刷新距离
  70. refreshDelayed: 800, // 刷新延迟
  71. refreshFinishDelayed: 800, // 刷新完成后的延迟
  72. safeArea: false, // 是否开启安全区域适配
  73. emptyTextColor: '#82848a', // 空提示文字颜色
  74. loadTextColor: '#82848a', // 上拉加载文字颜色
  75. loadIconColor: '#82848a', // 上拉加载图标颜色
  76. refresherTextColor: '#82848a', // 下拉刷新文字颜色
  77. refresherIconColor: '#82848a', // 下拉刷新图标颜色
  78. emptyText: '暂无列表~', // 空数据提示文字
  79. loadingText: '正在加载中~', // 加载中文字
  80. loadFailText: '加载失败啦~', // 加载失败文字
  81. noMoreText: '没有更多啦~', // 没有更多文字
  82. refreshingText: '正在刷新~', // 正在刷新文字
  83. refreshFailText: '刷新失败~', // 刷新失败文字
  84. refreshSuccessText: '刷新成功~', // 刷新成功文字
  85. pulldownText: '下拉刷新~', // 下拉中的文字
  86. pulldownFinishText: '松开刷新~' // 下拉完成的文字
  87. },
  88. images, // 内置图片
  89. elClass: '', // 组件动态class
  90. windowInfo: {}, // 窗口信息
  91. scrollTop: 0, // 距离顶部滚动高度
  92. scrollViewTop: -1, // 滚动视图顶部位置
  93. scrollViewHeight: 0, // 滚动视图高度
  94. currentPage: 1, // 当前分页页码
  95. currentSize: 15, // 当前分页大小
  96. currentScrollTop: 0, // 当前滚动高度
  97. emptyText: '暂无列表~',
  98. loadStateText: '正在加载中~', // 加载状态文字
  99. refreshStateText: '下拉刷新~', // 刷新状态文字
  100. loadDisabled: false, // 是否禁用上拉加载
  101. loading: false, // 是否加载中
  102. refreshing: false, // 是否刷新中
  103. refreshFinish: false, // 是否刷新完成
  104. pulldowning: false, // 是否正在下拉
  105. pullDownHeight: 0, // 下拉高度
  106. showEmpty: false, // 是否显示空数据提示
  107. showPullUp: false, // 是否显示上拉加载
  108. showPullDown: false // 是否显示下拉刷新
  109. };
  110. },
  111. methods: {
  112. // 组件初始化
  113. handleInit() {
  114. // 合并配置
  115. this.defaultOption = Object.assign(this.defaultOption, this.option);
  116. this.showEmpty = !this.defaultOption.auto;
  117. this.currentPage = this.defaultOption.page;
  118. this.currentSize = this.defaultOption.size;
  119. this.emptyText = this.defaultOption.emptyText;
  120. this.loadStateText = this.defaultOption.loadingText;
  121. this.refreshStateText = this.defaultOption.pulldownText;
  122. // 计算高度
  123. this.queryRect('.' + this.elClass).then(rect => {
  124. // 设置组件顶部位置
  125. this.scrollViewTop = rect.top;
  126. // 判断是否自动加载
  127. if (this.defaultOption.auto) this.load();
  128. });
  129. },
  130. // 加载数据
  131. load() {
  132. if (this.defaultOption.disabled || this.loading || this.loadDisabled) return;
  133. // 开启正在加载
  134. this.loading = true;
  135. // 设置正在加载状态文字
  136. this.loadStateText = this.defaultOption.loadingText;
  137. // 显示上拉加载
  138. this.showPullUp = true;
  139. // 分页参数
  140. let paging = { page: this.currentPage, size: this.currentSize };
  141. // 触发load事件
  142. this.$emit('load', paging);
  143. },
  144. // 加载成功
  145. loadSuccess(data = {}) {
  146. // 解构数据
  147. const { list, total } = data;
  148. // 判断列表是否是数组
  149. if (Array.isArray(list)) {
  150. // 判断列表长度
  151. if (list.length) {
  152. // 判断列表长度和列表总数是否相同
  153. if (list.length >= total) {
  154. // 设置禁用上拉加载
  155. this.loadDisabled = true;
  156. // 加载状态文字
  157. this.loadStateText = this.defaultOption.noMoreText;
  158. } else {
  159. // 关闭禁用上拉加载
  160. this.loadDisabled = false;
  161. // 设置分页参数
  162. this.currentPage++;
  163. // 加载状态为加载中
  164. this.loadStateText = this.defaultOption.loadingText;
  165. // 加载计算
  166. this.loadCompute();
  167. }
  168. // 显示上拉加载
  169. this.showPullUp = true;
  170. // 隐藏空数据提示
  171. this.showEmpty = false;
  172. } else {
  173. // 设置禁用上拉加载
  174. this.loadDisabled = true;
  175. // 隐藏上拉加载
  176. this.showPullUp = false;
  177. // 隐藏上拉加载
  178. this.showPullUp = false;
  179. // 显示空数据提示
  180. this.showEmpty = true;
  181. }
  182. // 关闭正在加载
  183. this.loading = false;
  184. // 触发加载成功事件
  185. this.$emit('loadSuccess', list);
  186. } else {
  187. // 不是数组类型当作加载失败处理
  188. this.loadFail();
  189. console.error('the list must be a array');
  190. }
  191. },
  192. // 加载失败
  193. loadFail() {
  194. // 关闭正在加载
  195. this.loading = false;
  196. // 关闭空数据提示
  197. this.showEmpty = false;
  198. // 显示上拉加载
  199. this.showPullUp = true;
  200. // 加载状态为加载失败
  201. this.loadStateText = this.defaultOption.loadFailText;
  202. // 触发加载失败事件
  203. this.$emit('loadFail');
  204. },
  205. // 刷新数据
  206. refresh() {
  207. // 如果是下拉刷新
  208. if (this.pullDownHeight == this.defaultOption.refresherThreshold) {
  209. // 关闭正在加载
  210. this.loading = false;
  211. // 隐藏上拉加载
  212. this.showPullUp = false;
  213. } else {
  214. // 开启正在加载
  215. this.loading = true;
  216. // 隐藏空数据提示
  217. this.showEmpty = false;
  218. // 显示上拉加载
  219. this.showPullUp = true;
  220. // 设置正在刷新状态文字
  221. this.loadStateText = this.defaultOption.refreshingText;
  222. }
  223. // 设置刷新未完成
  224. this.refreshFinish = false;
  225. // 开启正在刷新
  226. this.refreshing = true;
  227. // 设置正在刷新状态文字
  228. this.refreshStateText = this.defaultOption.refreshingText;
  229. // 设置分页参数
  230. this.currentPage = 1;
  231. this.currentSize = this.defaultOption.size;
  232. let paging = { page: this.currentPage, size: this.currentSize };
  233. // 触发refresh事件
  234. setTimeout(() => {
  235. this.$emit('refresh', paging);
  236. }, this.defaultOption.refreshDelayed);
  237. },
  238. // 刷新成功
  239. refreshSuccess(data) {
  240. // 解构数据
  241. const { list, total } = data;
  242. // 判断列表是否是数组
  243. if (Array.isArray(list)) {
  244. // 判断列表长度
  245. if (list.length) {
  246. // 判断列表长度和列表总数是否相同
  247. if (list.length >= total) {
  248. // 设置禁用上拉加载
  249. this.loadDisabled = true;
  250. // 设置没有更多状态文字
  251. this.loadStateText = this.defaultOption.noMoreText;
  252. } else {
  253. // 设置分页参数
  254. this.currentPage++;
  255. // 关闭禁用上拉加载
  256. this.loadDisabled = false;
  257. // 设置加载中状态文字
  258. this.loadStateText = this.defaultOption.loadingText;
  259. // 开启自动加载
  260. this.defaultOption.auto = true;
  261. // 加载计算
  262. this.loadCompute();
  263. }
  264. // 关闭空数据提示
  265. this.showEmpty = false;
  266. // 显示上拉加载
  267. this.showPullUp = true;
  268. } else {
  269. // 设置禁用上拉加载
  270. this.loadDisabled = true;
  271. // 隐藏上拉加载
  272. this.showPullUp = false;
  273. // 显示空数据提示
  274. this.showEmpty = true;
  275. // 设置没有更多状态文字
  276. this.loadStateText = this.defaultOption.noMoreText;
  277. }
  278. // 关闭正在加载
  279. this.loading = false;
  280. // 设置刷新成功状态文字
  281. this.refreshStateText = this.defaultOption.refreshSuccessText;
  282. // 关闭正在刷新
  283. this.refreshing = false;
  284. // 关闭正在下拉
  285. this.pulldowning = false;
  286. // 触发刷新成功事件
  287. this.$emit('refreshSuccess', list);
  288. setTimeout(() => {
  289. // 设置刷新完成
  290. this.refreshFinish = true;
  291. // 重置下拉高度
  292. this.pullDownHeight = 0;
  293. // 隐藏下拉刷新
  294. this.showPullDown = false;
  295. this.$emit('refreshSuccess');
  296. }, this.defaultOption.refreshFinishDelayed);
  297. } else {
  298. // 不是数组类型当作刷新失败处理
  299. this.refreshFail();
  300. console.error('the list must be a array');
  301. }
  302. },
  303. // 刷新失败
  304. refreshFail() {
  305. // 设置加载失败状态文字
  306. this.loadStateText = this.defaultOption.refreshFailText;
  307. // 设置刷新失败状态文字
  308. this.refreshStateText = this.defaultOption.refreshFailText;
  309. // 关闭正在加载
  310. this.loading = false;
  311. // 显示下拉加载
  312. this.showPullUp = true;
  313. // 关闭正在刷新
  314. this.refreshing = false;
  315. // 关闭正在下拉
  316. this.pulldowning = false;
  317. // 延迟执行刷新完成后状态
  318. setTimeout(() => {
  319. // 设置刷新完成
  320. this.refreshFinish = true;
  321. // 重置下拉高度
  322. this.pullDownHeight = 0;
  323. // 隐藏下拉刷新
  324. this.showPullDown = false;
  325. // 触发刷新失败事件
  326. this.$emit('refreshError');
  327. }, this.defaultOption.refreshFinishDelayed);
  328. },
  329. // 加载计算
  330. loadCompute() {
  331. // 判断是否自动加载
  332. if (this.defaultOption.auto) {
  333. // 延迟执行下否者可能会高度计算错误
  334. setTimeout(() => {
  335. this.$nextTick(() => {
  336. this.queryRect('.list-content').then(rect => {
  337. if (rect.height <= this.scrollViewHeight) {
  338. this.load();
  339. }
  340. });
  341. });
  342. }, 100);
  343. }
  344. },
  345. // 上拉触底事件
  346. handleScrolltolower(e) {
  347. if (this.loadDisabled) return;
  348. this.$emit('scrolltolower', e);
  349. this.load();
  350. },
  351. // 滚动事件
  352. handleScroll(event) {
  353. this.currentScrollTop = event.detail.scrollTop;
  354. this.$emit('scroll', event.detail);
  355. },
  356. // 触摸按下处理
  357. handleTouchStart(event) {
  358. if (this.defaultOption.disabled) return;
  359. this.currentTouchStartY = event.touches[0].clientY;
  360. this.$emit('touchStart', event);
  361. },
  362. // 触摸按下滑动处理
  363. handleTouchMove(event) {
  364. if (this.defaultOption.disabled || this.currentScrollTop) return;
  365. if (event.touches[0].clientY >= this.currentTouchStartY) {
  366. this.pulldowning = true;
  367. this.showPullDown = true;
  368. let pullDownDistance = (event.touches[0].clientY - this.currentTouchStartY) * this.defaultOption.pullDownSpeed;
  369. this.pullDownHeight = pullDownDistance > this.defaultOption.refresherThreshold ? this.defaultOption.refresherThreshold : pullDownDistance;
  370. this.refreshStateText =
  371. this.pullDownHeight >= this.defaultOption.refresherThreshold ? this.defaultOption.pulldownFinishText : this.defaultOption.pulldownText;
  372. this.$emit('touchMove', event);
  373. }
  374. },
  375. // 触摸松开处理
  376. handleTouchEnd(event) {
  377. if (this.defaultOption.disabled) return;
  378. // 当下拉高度小于下拉阈值
  379. if (this.pullDownHeight < this.defaultOption.refresherThreshold) {
  380. // 关闭正在下拉
  381. this.pulldowning = false;
  382. // 重置下拉高度
  383. this.pullDownHeight = 0;
  384. // 隐藏下拉刷新
  385. this.showPullDown = false;
  386. // 触发下拉中断事件
  387. this.$emit('refreshStop');
  388. } else {
  389. this.refresh();
  390. }
  391. // 触发下拉触摸松开事件
  392. this.$emit('touchEnd', event);
  393. },
  394. // 更新组件
  395. updateScrollView() {
  396. if (this.defaultOption.height) {
  397. this.scrollViewHeight = uni.upx2px(this.defaultOption.height);
  398. } else {
  399. this.scrollViewHeight = this.windowInfo.windowHeight - this.scrollViewTop;
  400. }
  401. this.scrollViewObserve();
  402. },
  403. // 监听列表高度变化
  404. listContentObserve() {
  405. this.disconnectObserve('_listContentObserve');
  406. const listContentObserve = this.createIntersectionObserver({
  407. thresholds: [0, 0.5, 1]
  408. });
  409. listContentObserve.relativeToViewport({
  410. // #ifdef H5
  411. top: -(this.windowInfo.windowTop + rect.top),
  412. // #endif
  413. // #ifndef H5
  414. top: -rect.top
  415. // #endif
  416. });
  417. },
  418. // 监听组件位置变化
  419. scrollViewObserve() {
  420. this.disconnectObserve('_scrollViewObserve');
  421. this.$nextTick(() => {
  422. this.queryRect('.' + this.elClass).then(rect => {
  423. const scrollViewObserve = this.createIntersectionObserver({
  424. thresholds: [0, 0.5, 1]
  425. });
  426. scrollViewObserve.relativeToViewport({
  427. // #ifdef H5
  428. top: -(this.windowInfo.windowTop + rect.top),
  429. // #endif
  430. // #ifndef H5
  431. top: -rect.top
  432. // #endif
  433. });
  434. scrollViewObserve.observe('.' + this.elClass, position => {
  435. // #ifdef H5
  436. this.scrollViewTop = position.boundingClientRect.top - this.windowInfo.windowTop;
  437. // #endif
  438. // #ifndef H5
  439. this.scrollViewTop = position.boundingClientRect.top;
  440. // #endif
  441. });
  442. this._scrollViewObserve = scrollViewObserve;
  443. });
  444. });
  445. },
  446. // 断开监听组件
  447. disconnectObserve(observerName) {
  448. const observer = this[observerName];
  449. observer && observer.disconnect();
  450. },
  451. // 查询dom节点信息
  452. queryRect(selector, all) {
  453. return new Promise(resolve => {
  454. uni.createSelectorQuery()
  455. .in(this)
  456. [all ? 'selectAll' : 'select'](selector)
  457. .boundingClientRect(rect => {
  458. if (all && Array.isArray(rect) && rect.length) {
  459. resolve(rect);
  460. }
  461. if (!all && rect) {
  462. resolve(rect);
  463. }
  464. })
  465. .exec();
  466. });
  467. },
  468. // 16进制转RGB
  469. hexToRgb(hex) {
  470. const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
  471. hex = hex.replace(shorthandRegex, (m, r, g, b) => {
  472. return r + r + g + g + b + b;
  473. });
  474. const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  475. return result
  476. ? {
  477. r: parseInt(result[1], 16),
  478. g: parseInt(result[2], 16),
  479. b: parseInt(result[3], 16)
  480. }
  481. : null;
  482. }
  483. },
  484. computed: {
  485. scrollListWrapStyle(){
  486. let style = {};
  487. style.background = this.defaultOption.background;
  488. return style;
  489. },
  490. // 组件容器样式
  491. listWrapStyle() {
  492. let style = {};
  493. const { offsetBottom } = this.defaultOption;
  494. style.height = this.scrollViewHeight - uni.upx2px(offsetBottom) + 'px';
  495. if (this.defaultOption.safeArea) style.paddingBottom = 'env(safe-area-inset-bottom) !important';
  496. return style;
  497. },
  498. // 滚动内容样式
  499. scrollContentStyle() {
  500. const style = {};
  501. const { pullDownHeight, pulldowning, showPullDown } = this;
  502. style.transform = showPullDown ? `translateY(${pullDownHeight}px)` : `translateY(0px)`;
  503. style.transition = pulldowning ? `transform 100ms ease-out` : `transform 200ms cubic-bezier(0.19,1.64,0.42,0.72)`;
  504. return style;
  505. },
  506. // 下拉刷新样式
  507. refreshViewStyle() {
  508. const style = {};
  509. const { showPullDown } = this;
  510. style.opacity = showPullDown ? 1 : 0;
  511. return style;
  512. },
  513. // 下拉中动画样式
  514. pullDownAnimationStyle() {
  515. const style = {};
  516. const { refresherIconColor, refresherThreshold } = this.defaultOption;
  517. const { refreshing, pullDownHeight } = this;
  518. const { r, g, b } = this.hexToRgb(refresherIconColor);
  519. const rate = pullDownHeight / refresherThreshold;
  520. style.borderColor = `rgba(${r},${g},${b},0.2)`;
  521. style.borderTopColor = refresherIconColor;
  522. if (!refreshing) {
  523. style.transform = `rotate(${360 * rate}deg)`;
  524. style.transition = 'transform 100ms linear';
  525. }
  526. return style;
  527. },
  528. pullDownTextStyle() {
  529. const style = {};
  530. const { refresherTextColor } = this.defaultOption;
  531. style.color = refresherTextColor;
  532. return style;
  533. },
  534. // 上拉中动画样式
  535. pullUpAnimationStyle() {
  536. const style = {};
  537. const { loadIconColor } = this.defaultOption;
  538. const { r, g, b } = this.hexToRgb(loadIconColor);
  539. style.borderColor = `rgba(${r},${g},${b},0.2)`;
  540. style.borderTopColor = loadIconColor;
  541. return style;
  542. },
  543. // 上拉中文字样式
  544. pullUpTextStyle() {
  545. const style = {};
  546. const { loadTextColor } = this.defaultOption;
  547. style.color = loadTextColor;
  548. return style;
  549. },
  550. // 空数据提示文字样式
  551. emptyTextStyle() {
  552. const style = {};
  553. const { emptyTextColor } = this.defaultOption;
  554. style.color = emptyTextColor;
  555. return style;
  556. }
  557. },
  558. watch: {
  559. scrollViewTop(val) {
  560. this.updateScrollView();
  561. }
  562. },
  563. created() {
  564. this.elClass = 'scroll-view-' + this._uid;
  565. this.windowInfo = uni.getSystemInfoSync();
  566. },
  567. mounted() {
  568. this.handleInit();
  569. }
  570. };
  571. </script>
  572. <style scoped lang="scss">
  573. .scroll-list-wrap {
  574. box-sizing: border-box;
  575. .scroll-view {
  576. position: relative;
  577. .scroll-content {
  578. height: 100%;
  579. display: flex;
  580. will-change: transform;
  581. flex-direction: column;
  582. .pull-down-wrap {
  583. left: 0;
  584. width: 100%;
  585. display: flex;
  586. padding: 30rpx 0;
  587. position: absolute;
  588. align-items: flex-end;
  589. justify-content: center;
  590. transform: translateY(-100%);
  591. .refresh-view {
  592. display: flex;
  593. justify-content: center;
  594. .pull-down-animation {
  595. width: 32rpx;
  596. height: 32rpx;
  597. border-width: 4rpx;
  598. border-style: solid;
  599. border-radius: 50%;
  600. &.refreshing {
  601. animation: spin 0.5s linear infinite;
  602. }
  603. @keyframes spin {
  604. to {
  605. transform: rotate(360deg);
  606. }
  607. }
  608. }
  609. .pull-down-text {
  610. margin-left: 10rpx;
  611. }
  612. }
  613. }
  614. .empty-wrap {
  615. top: 0;
  616. left: 0;
  617. width: 100%;
  618. height: 100%;
  619. display: flex;
  620. position: absolute;
  621. align-items: center;
  622. flex-direction: column;
  623. .empty-view {
  624. margin: auto;
  625. display: flex;
  626. align-items: center;
  627. flex-direction: column;
  628. .empty-image {
  629. width: 200rpx;
  630. height: 200rpx;
  631. }
  632. .empty-text {
  633. color: #606266;
  634. margin-top: 20rpx;
  635. }
  636. }
  637. }
  638. .list-content {
  639. }
  640. .pull-up-wrap {
  641. display: flex;
  642. align-items: center;
  643. justify-content: center;
  644. .load-view {
  645. padding: 20rpx 0;
  646. display: flex;
  647. align-items: center;
  648. justify-content: center;
  649. .pull-up-animation {
  650. width: 32rpx;
  651. height: 32rpx;
  652. border-width: 4rpx;
  653. border-style: solid;
  654. border-radius: 50%;
  655. animation: spin 0.5s linear infinite;
  656. }
  657. .pull-up-text {
  658. margin-left: 10rpx;
  659. }
  660. }
  661. }
  662. }
  663. }
  664. }
  665. </style>