floating-drag.vue 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  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. // 阻止默认行为,防止页面滚动
  149. e.preventDefault && e.preventDefault();
  150. e.stopPropagation && e.stopPropagation();
  151. const touch = e.touches[0];
  152. // 计算移动距离
  153. const deltaX = touch.clientX - this.startX;
  154. const deltaY = touch.clientY - this.startY;
  155. // 使用初始位置计算新位置,避免累积误差
  156. let newLeft = this.initialLeft + deltaX;
  157. let newBottom = this.initialBottom - deltaY; // 注意:y轴方向是相反的
  158. // 获取按钮实际宽度(将rpx转换为px)
  159. const buttonWidthPx = (parseFloat(this.width) / 750) * this.screenWidth;
  160. // 确保按钮不超出屏幕边界
  161. if (newLeft < 0) {
  162. newLeft = 0;
  163. } else if (newLeft > this.screenWidth - buttonWidthPx) {
  164. newLeft = this.screenWidth - buttonWidthPx;
  165. }
  166. // 确保按钮不超出屏幕垂直边界
  167. if (newBottom < 20) {
  168. newBottom = 20;
  169. } else if (newBottom > this.screenHeight - buttonWidthPx) {
  170. newBottom = this.screenHeight - buttonWidthPx;
  171. }
  172. // 使用节流方式更新位置,避免过于频繁的更新
  173. if (!this.isUpdatingPosition) {
  174. this.isUpdatingPosition = true;
  175. // 更新位置 - 第一次移动时才真正改变right为auto
  176. this.buttonPosition = {
  177. left: newLeft,
  178. right: "auto",
  179. bottom: newBottom,
  180. };
  181. // 使用setTimeout代替requestAnimationFrame,在微信小程序中更兼容
  182. setTimeout(() => {
  183. this.isUpdatingPosition = false;
  184. }, 16); // 约等于60fps的刷新率
  185. }
  186. },
  187. // 触摸结束,实现吸附效果
  188. touchEnd() {
  189. // 确保不再有待处理的更新
  190. this.isUpdatingPosition = false;
  191. // 获取按钮实际宽度(将rpx转换为px)
  192. const buttonWidthPx = (parseFloat(this.width) / 750) * this.screenWidth;
  193. const buttonCenter = this.buttonPosition.left + buttonWidthPx / 2; // 按钮中心位置
  194. const halfScreen = this.screenWidth / 2;
  195. // 判断是吸附到左边还是右边
  196. if (buttonCenter < halfScreen) {
  197. // 吸附到左边
  198. this.buttonPosition = {
  199. left: 0,
  200. right: "auto",
  201. bottom: this.buttonPosition.bottom,
  202. };
  203. } else {
  204. // 吸附到右边
  205. this.buttonPosition = {
  206. left: "auto",
  207. right: 0,
  208. bottom: this.buttonPosition.bottom,
  209. };
  210. }
  211. // 触发位置变更事件
  212. this.$emit("position-change", { ...this.buttonPosition });
  213. },
  214. // 处理点击事件
  215. handleClick() {
  216. this.$emit("click");
  217. if (this.onClick) {
  218. this.onClick();
  219. }
  220. },
  221. },
  222. };
  223. </script>
  224. <style lang="scss" scoped>
  225. .floating-drag {
  226. position: fixed;
  227. z-index: 999;
  228. transition: all 0.3s ease;
  229. }
  230. </style>