scroll-list.vue 20 KB

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