floating-button.vue 4.2 KB

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