floating-drag.vue 8.0 KB

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