floating-drag.vue 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. <template>
  2. <view
  3. class="floating-drag"
  4. :style="{
  5. left: buttonPosition.left + 'px',
  6. right: buttonPosition.right + 'px',
  7. bottom: buttonPosition.bottom + 'px',
  8. width: width + 'rpx',
  9. height: height + 'rpx',
  10. display: visible ? 'block' : 'none'
  11. }"
  12. @touchstart="touchStart"
  13. @touchmove="touchMove"
  14. @touchend="touchEnd"
  15. @click="handleClick"
  16. v-if="visible"
  17. >
  18. <slot>
  19. <!-- Default content if no slot is provided -->
  20. <view class="default-btn">
  21. <text>按钮</text>
  22. </view>
  23. </slot>
  24. </view>
  25. </template>
  26. <script>
  27. export default {
  28. name: 'FloatingDrag',
  29. props: {
  30. // 初始位置
  31. initialPosition: {
  32. type: Object,
  33. default: () => ({
  34. left: 'auto',
  35. right: 0,
  36. bottom: '20%'
  37. })
  38. },
  39. // 组件尺寸
  40. width: {
  41. type: [Number, String],
  42. default: 120
  43. },
  44. height: {
  45. type: [Number, String],
  46. default: 120
  47. },
  48. // 是否显示
  49. visible: {
  50. type: Boolean,
  51. default: true
  52. },
  53. // 点击事件
  54. onClick: {
  55. type: Function,
  56. default: null
  57. }
  58. },
  59. data() {
  60. return {
  61. // 悬浮按钮位置
  62. buttonPosition: {
  63. left: 'auto',
  64. right: 0,
  65. bottom: '20%',
  66. },
  67. // 触摸开始位置
  68. startX: 0,
  69. startY: 0,
  70. // 屏幕宽度和高度
  71. screenWidth: 0,
  72. screenHeight: 0,
  73. // 初始位置记录,用于计算拖动
  74. initialLeft: 0,
  75. initialBottom: 0,
  76. // 是否正在更新位置,用于防止频繁更新
  77. isUpdatingPosition: false,
  78. };
  79. },
  80. created() {
  81. // 使用传入的初始位置
  82. this.buttonPosition = { ...this.initialPosition };
  83. },
  84. watch: {
  85. initialPosition: {
  86. handler(newVal) {
  87. this.buttonPosition = { ...newVal };
  88. },
  89. immediate: true,
  90. },
  91. },
  92. mounted() {
  93. // 获取屏幕宽度和高度
  94. uni.getSystemInfo({
  95. success: (res) => {
  96. this.screenWidth = res.windowWidth;
  97. this.screenHeight = res.windowHeight;
  98. },
  99. });
  100. },
  101. methods: {
  102. // 触摸开始
  103. touchStart(e) {
  104. const touch = e.touches[0];
  105. this.startX = touch.clientX;
  106. this.startY = touch.clientY;
  107. // 记录初始位置,用于计算移动距离
  108. if (this.buttonPosition.right !== 'auto') {
  109. // 如果是靠右定位,记录当前位置但不立即改变显示位置
  110. this.initialLeft = this.screenWidth - parseFloat(this.width) / 2;
  111. } else {
  112. this.initialLeft = parseFloat(this.buttonPosition.left);
  113. }
  114. // 如果bottom是百分比,转换为具体像素值
  115. if (typeof this.buttonPosition.bottom === 'string' && this.buttonPosition.bottom.includes('%')) {
  116. const percentage = parseFloat(this.buttonPosition.bottom) / 100;
  117. this.initialBottom = this.screenHeight * percentage;
  118. } else {
  119. this.initialBottom = parseFloat(this.buttonPosition.bottom);
  120. }
  121. },
  122. // 触摸移动
  123. touchMove(e) {
  124. // 阻止默认行为,防止页面滚动
  125. e.preventDefault && e.preventDefault();
  126. e.stopPropagation && e.stopPropagation();
  127. const touch = e.touches[0];
  128. // 计算移动距离
  129. const deltaX = touch.clientX - this.startX;
  130. const deltaY = touch.clientY - this.startY;
  131. // 使用初始位置计算新位置,避免累积误差
  132. let newLeft = this.initialLeft + deltaX;
  133. let newBottom = this.initialBottom - deltaY; // 注意:y轴方向是相反的
  134. // 获取按钮实际宽度(将rpx转换为px)
  135. const buttonWidthPx = parseFloat(this.width) / 750 * this.screenWidth;
  136. // 确保按钮不超出屏幕边界
  137. if (newLeft < 0) {
  138. newLeft = 0;
  139. } else if (newLeft > this.screenWidth - buttonWidthPx) {
  140. newLeft = this.screenWidth - buttonWidthPx;
  141. }
  142. // 确保按钮不超出屏幕垂直边界
  143. if (newBottom < 20) {
  144. newBottom = 20;
  145. } else if (newBottom > this.screenHeight - buttonWidthPx) {
  146. newBottom = this.screenHeight - buttonWidthPx;
  147. }
  148. // 使用节流方式更新位置,避免过于频繁的更新
  149. if (!this.isUpdatingPosition) {
  150. this.isUpdatingPosition = true;
  151. // 更新位置 - 第一次移动时才真正改变right为auto
  152. this.buttonPosition = {
  153. left: newLeft,
  154. right: 'auto',
  155. bottom: newBottom,
  156. };
  157. // 使用setTimeout代替requestAnimationFrame,在微信小程序中更兼容
  158. setTimeout(() => {
  159. this.isUpdatingPosition = false;
  160. }, 16); // 约等于60fps的刷新率
  161. }
  162. },
  163. // 触摸结束,实现吸附效果
  164. touchEnd() {
  165. // 确保不再有待处理的更新
  166. this.isUpdatingPosition = false;
  167. // 获取按钮实际宽度(将rpx转换为px)
  168. const buttonWidthPx = parseFloat(this.width) / 750 * this.screenWidth;
  169. const buttonCenter = this.buttonPosition.left + buttonWidthPx / 2; // 按钮中心位置
  170. const halfScreen = this.screenWidth / 2;
  171. // 判断是吸附到左边还是右边
  172. if (buttonCenter < halfScreen) {
  173. // 吸附到左边
  174. this.buttonPosition = {
  175. left: 0,
  176. right: 'auto',
  177. bottom: this.buttonPosition.bottom,
  178. };
  179. } else {
  180. // 吸附到右边
  181. this.buttonPosition = {
  182. left: 'auto',
  183. right: 0,
  184. bottom: this.buttonPosition.bottom,
  185. };
  186. }
  187. // 触发位置变更事件
  188. this.$emit('position-change', { ...this.buttonPosition });
  189. },
  190. // 处理点击事件
  191. handleClick() {
  192. this.$emit('click');
  193. if (this.onClick) {
  194. this.onClick();
  195. }
  196. }
  197. }
  198. };
  199. </script>
  200. <style lang="scss" scoped>
  201. .floating-drag {
  202. position: fixed;
  203. z-index: 999;
  204. transition: all 0.3s ease;
  205. .default-btn {
  206. width: 100%;
  207. height: 100%;
  208. background-color: #4cd964;
  209. border-radius: 50%;
  210. display: flex;
  211. flex-direction: column;
  212. align-items: center;
  213. justify-content: center;
  214. border: 4rpx solid #fff;
  215. box-shadow: 0 0 10rpx rgba(0, 0, 0, 0.2);
  216. text {
  217. color: #ffffff;
  218. font-size: 34rpx;
  219. text-align: center;
  220. font-weight: 500;
  221. padding: 0 10rpx;
  222. line-height: 1.2;
  223. }
  224. }
  225. }
  226. </style>