tn-scroll-view.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. <template>
  2. <view class="tn-scroll-view-class tn-scroll-view">
  3. <scroll-view
  4. class="scroll-view"
  5. :style="[scrollViewStyle]"
  6. scroll-y
  7. scroll-anchoring
  8. enable-back-to-top
  9. :throttle="false"
  10. :scroll-top="scrollTop"
  11. :lower-threshold="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="scroll__pull-down">
  20. <slot name="pulldown">
  21. <view class="scroll__refresh" :style="[refreshStyle]">
  22. <view><tn-loading :animation="refreshing"></tn-loading></view>
  23. <view class="scroll__refresh--text" :style="[refreshTextStyle]">{{ refreshStateText }}</view>
  24. </view>
  25. </slot>
  26. </view>
  27. <view :id="elScrollDataId" class="scroll__data">
  28. <slot></slot>
  29. </view>
  30. </view>
  31. </scroll-view>
  32. </view>
  33. </template>
  34. <script>
  35. import componentsColor from '../../libs/mixin/components_color.js'
  36. export default {
  37. name: 'tn-scroll-view',
  38. mixins: [ componentsColor ],
  39. props: {
  40. // H5顶部导航栏的高度
  41. h5NavHeight: {
  42. type: Number,
  43. default: 45
  44. },
  45. // 自定义顶部导航栏高度
  46. customNavHeight: {
  47. type: Number,
  48. default: 0
  49. },
  50. // 可滚动区域顶部偏移高度
  51. offsetTop: {
  52. type: Number,
  53. default: 0
  54. },
  55. // 可滚动区域底部偏移高度
  56. offsetBottom: {
  57. type: Number,
  58. default: 0
  59. },
  60. // 容器高度 (不设置则自动计算)
  61. height: {
  62. type: Number,
  63. default: null
  64. },
  65. // 是否禁用
  66. disabled: {
  67. type: Boolean,
  68. default: false
  69. },
  70. // 禁用下拉刷新
  71. pullDownDisabled: {
  72. type: Boolean,
  73. default: false
  74. },
  75. // 下拉速率
  76. pullDownSpeed: {
  77. type: Number,
  78. default: 0.5
  79. },
  80. // 刷新延迟
  81. refreshDelayed: {
  82. type: Number,
  83. default: 800
  84. },
  85. // 刷新完成后延迟
  86. refreshFinishDelayed: {
  87. type: Number,
  88. default: 800
  89. },
  90. // 下拉刷新距离
  91. refresherThreshold: {
  92. type: Number,
  93. default: 70
  94. },
  95. // 上拉加载距离
  96. lowerThreshold: {
  97. type: Number,
  98. default: 40
  99. },
  100. // 刷新状态
  101. refreshState: {
  102. type: Boolean,
  103. default: false
  104. },
  105. // 正在刷新文字
  106. refreshingText: {
  107. type: String,
  108. default: '正在刷新'
  109. },
  110. // 刷新成功文字
  111. refreshSuccessText: {
  112. type: String,
  113. default: '刷新成功'
  114. },
  115. // 下拉中的文字
  116. pulldownText: {
  117. type: String,
  118. default: '下拉刷新'
  119. },
  120. // 下拉完成的文字
  121. pulldownFinishText: {
  122. type: String,
  123. default: '松开刷新'
  124. }
  125. },
  126. data() {
  127. return {
  128. // 滚动容器内容id
  129. elScrollDataId: '',
  130. // 系统信息
  131. systemInfo: {
  132. height: 0,
  133. statusBarHeight: 0
  134. },
  135. // 距离顶部滚动高度
  136. scrollTop: 0,
  137. // 滚动内容视图顶部位置
  138. scrollDataTop: -1,
  139. // 滚动内容视图顶部位置偏移
  140. scrollDataOffsetTop: -1,
  141. // 滚动区域的高度
  142. scrollViewHeight: 0,
  143. // 当前滚动高度
  144. currentScrollTop: 0,
  145. // 当前触摸点Y轴开始坐标
  146. currentTouchStartY: 0,
  147. // 刷新状态文字
  148. refreshStateText: '下拉刷新',
  149. // 是否刷新中
  150. refreshing: false,
  151. // 是否刷新完成
  152. refreshFinish: false,
  153. // 是否正在下拉
  154. pulldowning: false,
  155. // 下拉高度
  156. pullDownHeight: 0,
  157. // 是否显示下拉加载
  158. showPullDown: false
  159. }
  160. },
  161. computed: {
  162. scrollViewStyle() {
  163. let style = {}
  164. style.height = this.scrollViewHeight + 'px'
  165. if (!this.backgroundColorClass) {
  166. style.backgroundColor = this.backgroundColorStyle
  167. }
  168. return style
  169. },
  170. scrollContentStyle() {
  171. let style = {}
  172. style.transform = this.showPullDown ? `translateY(${this.pullDownHeight}px)` : `translateY(0px)`
  173. style.transition = this.pulldowning ? `transform 100ms ease-out` : `transform 500ms cubic-bezier(0.19,1.64,0.42,0.72)`
  174. return style
  175. },
  176. refreshStyle() {
  177. let style = {}
  178. style.opacity = this.showPullDown ? 1 : 0
  179. return style
  180. },
  181. refreshTextStyle() {
  182. let style = {}
  183. if (!this.fontColorClass) {
  184. style.color = this.fontColorStyle
  185. }
  186. return style
  187. },
  188. loadTextStyle() {
  189. let style = {}
  190. if (!this.fontColorClass) {
  191. style.color = this.fontColorStyle
  192. }
  193. return style
  194. }
  195. },
  196. watch: {
  197. refreshState(nVal, oVal) {
  198. if (!nVal) {
  199. if (this.showPullDown) {
  200. // 关闭正在下拉
  201. this.pulldowning = false
  202. // 隐藏下拉刷新
  203. this.showPullDown = false
  204. // 关闭正在刷新
  205. this.refreshing = false
  206. }
  207. }
  208. }
  209. },
  210. created() {
  211. this.elScrollDataId = this.$t.uuid()
  212. this.getSystemInfo()
  213. },
  214. mounted() {
  215. this.$nextTick(() => {
  216. this.init()
  217. })
  218. },
  219. methods: {
  220. // 组件初始化
  221. init() {
  222. this.refreshStateText = this.pulldownText
  223. // 初始化scrollView信息
  224. this.updateScrollViewInfo()
  225. },
  226. // 获取系统信息
  227. getSystemInfo() {
  228. const systemInfo = uni.getSystemInfoSync()
  229. this.systemInfo.height = systemInfo.safeArea.height
  230. this.systemInfo.statusBarHeight = systemInfo.statusBarHeight
  231. },
  232. // 更新scrollView信息
  233. updateScrollViewInfo() {
  234. if (this.height) {
  235. this.scrollViewHeight = this.height
  236. } else {
  237. // 设置scrollView的高度和组件顶部位置
  238. // console.log(this.systemInfo, this.offsetTop, this.customNavHeight);
  239. // #ifdef H5
  240. this.scrollViewHeight = this.systemInfo.height - (
  241. this.offsetTop +
  242. (this.customNavHeight ? this.customNavHeight : this.h5NavHeight) +
  243. this.offsetBottom)
  244. this.scrollDataOffsetTop = this.offsetTop + (this.customNavHeight ? this.customNavHeight : this.h5NavHeight)
  245. // #endif
  246. // #ifndef H5
  247. this.scrollViewHeight = this.systemInfo.height - (
  248. this.offsetTop +
  249. this.systemInfo.statusBarHeight +
  250. this.offsetBottom)
  251. this.scrollDataOffsetTop = this.offsetTop + this.systemInfo.statusBarHeight
  252. // #endif
  253. }
  254. },
  255. // 获取scrollView内容信息
  256. async getScrollDataInfo() {
  257. const scrollInfo = await this._tGetRect(`#${this.elScrollDataId}`)
  258. this.scrollDataTop = scrollInfo.top
  259. },
  260. // 上拉触底事件
  261. handleScrollTolower(e) {
  262. if (this.pullUpDisabled) return
  263. this.$emit('scrolltolower', e)
  264. },
  265. // 滚动事件
  266. handleScroll(e) {
  267. this.currentScrollTop = e.detail.scrollTop
  268. this.$emit('scroll', e.detail)
  269. },
  270. // 触摸按下事件
  271. handleTouchStart(e) {
  272. if (this.disabled) return
  273. this.currentTouchStartY = e.touches[0].clientY
  274. this.getScrollDataInfo()
  275. this.$emit('touchStart', e)
  276. },
  277. // 触摸下滑事件
  278. handleTouchMove(e) {
  279. if (this.disabled) return
  280. if (this.currentScrollTop == 0 && e.touches[0].clientY >= this.currentTouchStartY) {
  281. // 容器滑动的偏移
  282. const moveOffset = this.scrollDataTop > 0 ?
  283. (this.scrollDataOffsetTop - this.scrollDataTop) :
  284. (Math.abs(this.scrollDataTop) + this.scrollDataOffsetTop)
  285. this.pulldowning = true
  286. this.showPullDown = true
  287. let pullDownDistance = ((e.touches[0].clientY - this.currentTouchStartY) - moveOffset) * this.pullDownSpeed
  288. this.pullDownHeight = pullDownDistance
  289. // this.pullDownHeight = pullDownDistance > this.refresherThreshold ? this.refresherThreshold : pullDownDistance
  290. this.refreshStateText = this.pullDownHeight >= this.refresherThreshold ? this.pulldownFinishText : this.pulldownText
  291. if (pullDownDistance > this.refresherThreshold) {
  292. this.$emit('refreshReady')
  293. }
  294. }
  295. this.$emit('touchMove', e)
  296. },
  297. // 触摸松开处理
  298. handleTouchEnd(e) {
  299. if (this.disabled) return
  300. // 处理下拉刷新
  301. if (this.showPullDown) {
  302. // 当下拉高度小于下拉阈值
  303. if (this.pullDownHeight < this.refresherThreshold) {
  304. // 关闭正在下拉
  305. this.pulldowning = false
  306. // 重置下拉高度
  307. this.pullDownHeight = 0
  308. // 隐藏下拉刷新
  309. this.showPullDown = false
  310. // 触发下拉中断事件
  311. this.$emit('refreshStop')
  312. } else {
  313. this.pullDownHeight = this.pullDownHeight > this.refresherThreshold ? this.refresherThreshold : this.pullDownHeight
  314. this.refresh()
  315. }
  316. }
  317. // 触发下拉触摸松开事件
  318. this.$emit('touchEnd', e)
  319. },
  320. // 刷新数据
  321. refresh() {
  322. // 设置刷新未完成
  323. this.refreshFinish = false
  324. // 开启正在刷新
  325. this.refreshing = true
  326. // 设置正在刷新状态文字
  327. this.refreshStateText = this.refreshingText
  328. // 触发refresh事件
  329. setTimeout(() => {
  330. this.$emit('refresh')
  331. }, this.refreshDelayed)
  332. },
  333. }
  334. }
  335. </script>
  336. <style lang="scss" scoped>
  337. .tn-scroll-view {
  338. .scroll-view {
  339. position: relative;
  340. touch-action: none;
  341. .scroll__content {
  342. display: flex;
  343. will-change: transform;
  344. flex-direction: column;
  345. .scroll {
  346. &__pull-down {
  347. position: absolute;
  348. left: 0;
  349. width: 100%;
  350. display: flex;
  351. padding: 30rpx 0;
  352. align-items: center;
  353. justify-content: center;
  354. transform: translateY(-100%);
  355. }
  356. &__refresh {
  357. display: flex;
  358. align-items: center;
  359. justify-content: center;
  360. &--text {
  361. margin-left: 10rpx;
  362. }
  363. }
  364. &__data {
  365. }
  366. &__pull-up {
  367. position: absolute;
  368. left: 0;
  369. bottom: 0;
  370. width: 100%;
  371. display: flex;
  372. align-items: center;
  373. justify-content: center;
  374. transform: translateY(100%);
  375. }
  376. &__load {
  377. padding: 20rpx 0;
  378. display: flex;
  379. align-items: center;
  380. justify-content: center;
  381. &--text {
  382. margin-left: 10rpx;
  383. }
  384. }
  385. }
  386. }
  387. }
  388. }
  389. </style>