floating-button.vue 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. <template>
  2. <view
  3. class="floating-container"
  4. :style="{ top: position.y + 'px', left: position.x + 'px' }"
  5. @touchstart="onTouchStart"
  6. @click="onClick"
  7. >
  8. <view class="floating-icon" :class="{ expanded: isExpanded }">
  9. <view class="icon-content">
  10. <slot name="icon">
  11. <text class="default-icon">+</text>
  12. </slot>
  13. </view>
  14. <view v-if="isExpanded" class="expanded-content">
  15. <slot name="expanded">
  16. </slot>
  17. </view>
  18. </view>
  19. </view>
  20. </template>
  21. <script>
  22. export default {
  23. name: 'FloatingButton',
  24. props: {
  25. // 初始位置距离右边的距离
  26. right: {
  27. type: Number,
  28. default: 20
  29. },
  30. // 初始位置距离底部的距离
  31. bottom: {
  32. type: Number,
  33. default: 100
  34. },
  35. // 按钮大小
  36. size: {
  37. type: Number,
  38. default: 50
  39. },
  40. // 是否可拖拽
  41. draggable: {
  42. type: Boolean,
  43. default: true
  44. }
  45. },
  46. data() {
  47. return {
  48. position: {
  49. x: 0,
  50. y: 0
  51. },
  52. isDragging: false,
  53. isExpanded: false,
  54. startX: 0,
  55. startY: 0,
  56. lastX: 0,
  57. lastY: 0,
  58. clickStartTime: 0,
  59. hasMoved: false
  60. };
  61. },
  62. mounted() {
  63. this.initPosition();
  64. },
  65. methods: {
  66. // 初始化位置
  67. initPosition() {
  68. const systemInfo = uni.getSystemInfoSync();
  69. const screenWidth = systemInfo.windowWidth;
  70. const screenHeight = systemInfo.windowHeight;
  71. this.position = {
  72. x: screenWidth - this.right - this.size,
  73. y: screenHeight - this.bottom - this.size
  74. };
  75. },
  76. // 触摸开始
  77. onTouchStart(e) {
  78. if (!this.draggable) return;
  79. const touch = e.touches[0];
  80. this.startX = touch.clientX;
  81. this.startY = touch.clientY;
  82. this.lastX = this.position.x;
  83. this.lastY = this.position.y;
  84. this.isDragging = true;
  85. this.hasMoved = false;
  86. this.clickStartTime = Date.now();
  87. },
  88. // 触摸移动
  89. onTouchMove(e) {
  90. if (!this.draggable || !this.isDragging) return;
  91. e.preventDefault();
  92. const touch = e.touches[0];
  93. const deltaX = touch.clientX - this.startX;
  94. const deltaY = touch.clientY - this.startY;
  95. // 如果移动超过5像素,认为是拖拽操作
  96. if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
  97. this.hasMoved = true;
  98. }
  99. const systemInfo = uni.getSystemInfoSync();
  100. const screenWidth = systemInfo.windowWidth;
  101. const screenHeight = systemInfo.windowHeight;
  102. // 计算新位置,限制在屏幕范围内
  103. let newX = this.lastX + deltaX;
  104. let newY = this.lastY + deltaY;
  105. // 边界限制
  106. newX = Math.max(0, Math.min(newX, screenWidth - this.size));
  107. newY = Math.max(0, Math.min(newY, screenHeight - this.size));
  108. this.position = {
  109. x: newX,
  110. y: newY
  111. };
  112. },
  113. // 触摸结束
  114. onTouchEnd(e) {
  115. if (!this.draggable) return;
  116. this.isDragging = false;
  117. // 自动吸附到屏幕边缘
  118. this.snapToEdge();
  119. },
  120. // 吸附到屏幕左右边缘
  121. snapToEdge() {
  122. const systemInfo = uni.getSystemInfoSync();
  123. const screenWidth = systemInfo.windowWidth;
  124. // 判断当前在左半屏还是右半屏
  125. const centerX = this.position.x + this.size / 2;
  126. if (centerX < screenWidth / 2) {
  127. // 吸附到左边
  128. this.position.x = 10;
  129. } else {
  130. // 吸附到右边
  131. this.position.x = screenWidth - this.size - 10;
  132. }
  133. },
  134. // 点击事件
  135. onClick(e) {
  136. // 如果发生了拖拽移动,不触发点击事件
  137. const clickDuration = Date.now() - this.clickStartTime;
  138. if (this.hasMoved || clickDuration > 300) {
  139. return;
  140. }
  141. // 切换展开/收起状态
  142. this.isExpanded = !this.isExpanded;
  143. // 触发父组件事件
  144. this.$emit(this.isExpanded ? 'expand' : 'collapse', {
  145. expanded: this.isExpanded
  146. });
  147. },
  148. // 手动展开
  149. expand() {
  150. this.isExpanded = true;
  151. },
  152. // 手动收起
  153. collapse() {
  154. this.isExpanded = false;
  155. }
  156. }
  157. };
  158. </script>
  159. <style scoped>
  160. .floating-container {
  161. position: fixed;
  162. z-index: 9999;
  163. width: 50px;
  164. height: 50px;
  165. }
  166. .expanded-content{
  167. position: absolute;
  168. }
  169. .floating-icon {
  170. width: 100%;
  171. height: 100%;
  172. display: flex;
  173. align-items: center;
  174. justify-content: center;
  175. transition: all 0.3s ease;
  176. }
  177. .icon-content {
  178. width: 100rpx;
  179. height: 100rpx;
  180. border-radius: 50%;
  181. display: flex;
  182. align-items: center;
  183. justify-content: center;
  184. transition: all 0.3s ease;
  185. }
  186. .default-icon {
  187. font-size: 24px;
  188. color: #ffffff;
  189. font-weight: bold;
  190. }
  191. </style>