sliding-container.vue 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. <template>
  2. <view
  3. class="sliding-container"
  4. :style="containerStyle"
  5. @touchstart="touchStart"
  6. @touchmove="touchMove"
  7. @touchend="touchEnd"
  8. >
  9. <!-- 上一页内容 -->
  10. <view class="page prev-page" :style="prevPageStyle">
  11. <slot name="prev"></slot>
  12. </view>
  13. <!-- 当前页内容 -->
  14. <view class="page current-page" :style="currentPageStyle">
  15. <slot name="current"></slot>
  16. </view>
  17. <!-- 下一页内容 -->
  18. <view class="page next-page" :style="nextPageStyle">
  19. <slot name="next"></slot>
  20. </view>
  21. </view>
  22. </template>
  23. <script>
  24. const SWIPE_THRESHOLD = 50; // 滑动阈值
  25. const ANIMATION_DURATION = 300; // 动画持续时间(毫秒)
  26. export default {
  27. name: 'SlidingContainer',
  28. data() {
  29. return {
  30. touchStartX: 0,
  31. touchStartY: 0,
  32. deltaX: 0,
  33. deltaY: 0,
  34. isAnimating: false,
  35. containerWidth: 0,
  36. }
  37. },
  38. props: {
  39. // 是否禁用滑动
  40. disabled: {
  41. type: Boolean,
  42. default: false
  43. }
  44. },
  45. computed: {
  46. containerStyle() {
  47. return {
  48. transform: `translateX(${this.deltaX}px)`,
  49. transition: this.isAnimating ? `transform ${ANIMATION_DURATION}ms ease-out` : 'none'
  50. }
  51. },
  52. prevPageStyle() {
  53. return {
  54. transform: 'translateX(-100%)',
  55. }
  56. },
  57. currentPageStyle() {
  58. return {
  59. transform: 'translateX(0)',
  60. }
  61. },
  62. nextPageStyle() {
  63. return {
  64. transform: 'translateX(100%)',
  65. }
  66. }
  67. },
  68. mounted() {
  69. // 获取容器宽度
  70. const query = uni.createSelectorQuery().in(this);
  71. query.select('.sliding-container').boundingClientRect(data => {
  72. if (data) {
  73. this.containerWidth = data.width;
  74. }
  75. }).exec();
  76. },
  77. methods: {
  78. touchStart(e) {
  79. if (this.disabled || this.isAnimating) return;
  80. const touch = e.touches[0];
  81. this.touchStartX = touch.clientX;
  82. this.touchStartY = touch.clientY;
  83. this.deltaX = 0;
  84. this.deltaY = 0;
  85. },
  86. touchMove(e) {
  87. if (this.disabled || this.isAnimating) return;
  88. const touch = e.touches[0];
  89. this.deltaX = touch.clientX - this.touchStartX;
  90. this.deltaY = touch.clientY - this.touchStartY;
  91. // 如果垂直滑动距离大于水平滑动距离,则不处理
  92. if (Math.abs(this.deltaY) > Math.abs(this.deltaX)) {
  93. return;
  94. }
  95. // 阻止默认行为
  96. e.preventDefault();
  97. // 添加阻尼效果
  98. const damping = 0.5;
  99. this.deltaX = this.deltaX * damping;
  100. },
  101. touchEnd() {
  102. if (this.disabled || this.isAnimating) return;
  103. // 判断滑动方向和距离
  104. if (Math.abs(this.deltaX) > SWIPE_THRESHOLD) {
  105. if (this.deltaX > 0) {
  106. this.slideToPrev();
  107. } else {
  108. this.slideToNext();
  109. }
  110. } else {
  111. this.resetPosition();
  112. }
  113. },
  114. slideToPrev() {
  115. this.isAnimating = true;
  116. this.deltaX = this.containerWidth;
  117. setTimeout(() => {
  118. this.isAnimating = false;
  119. this.deltaX = 0;
  120. this.$emit('slide-prev');
  121. }, ANIMATION_DURATION);
  122. },
  123. slideToNext() {
  124. this.isAnimating = true;
  125. this.deltaX = -this.containerWidth;
  126. setTimeout(() => {
  127. this.isAnimating = false;
  128. this.deltaX = 0;
  129. this.$emit('slide-next');
  130. }, ANIMATION_DURATION);
  131. },
  132. resetPosition() {
  133. this.isAnimating = true;
  134. this.deltaX = 0;
  135. setTimeout(() => {
  136. this.isAnimating = false;
  137. }, ANIMATION_DURATION);
  138. }
  139. }
  140. }
  141. </script>
  142. <style>
  143. .sliding-container {
  144. position: relative;
  145. width: 100%;
  146. height: 100%;
  147. min-height: 100vh;
  148. overflow: hidden;
  149. touch-action: pan-y pinch-zoom;
  150. background-color: inherit;
  151. }
  152. .page {
  153. position: absolute;
  154. top: 0;
  155. left: 0;
  156. width: 100%;
  157. height: 100%;
  158. min-height: 100vh;
  159. will-change: transform;
  160. background-color: inherit;
  161. overflow-y: auto;
  162. }
  163. .prev-page {
  164. z-index: 1;
  165. }
  166. .current-page {
  167. z-index: 2;
  168. }
  169. .next-page {
  170. z-index: 1;
  171. }
  172. </style>