wd-fab.vue 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. <template>
  2. <view
  3. @touchmove.stop.prevent="handleTouchMove"
  4. @touchstart="handleTouchStart"
  5. @touchend="handleTouchEnd"
  6. :class="`wd-fab ${customClass}`"
  7. :style="rootStyle"
  8. @click.stop=""
  9. >
  10. <view @click.stop="" :style="{ visibility: inited ? 'visible' : 'hidden' }" id="trigger">
  11. <slot name="trigger" v-if="$slots.trigger"></slot>
  12. <wd-button v-else @click="handleClick" custom-class="wd-fab__trigger" round :type="type" :disabled="disabled">
  13. <wd-icon custom-class="wd-fab__icon" :name="isActive ? activeIcon : inactiveIcon"></wd-icon>
  14. </wd-button>
  15. </view>
  16. <wd-transition
  17. v-if="expandable"
  18. :enter-class="`wd-fab__transition-enter--${fabDirection}`"
  19. enter-active-class="wd-fab__transition-enter-active"
  20. :leave-to-class="`wd-fab__transition-leave-to--${fabDirection}`"
  21. leave-active-class="wd-fab__transition-leave-active"
  22. :custom-class="`wd-fab__actions wd-fab__actions--${fabDirection}`"
  23. :show="isActive"
  24. :duration="300"
  25. >
  26. <slot></slot>
  27. </wd-transition>
  28. </view>
  29. </template>
  30. <script lang="ts">
  31. export default {
  32. name: 'wd-fab',
  33. options: {
  34. virtualHost: true,
  35. addGlobalClass: true,
  36. styleIsolation: 'shared'
  37. }
  38. }
  39. </script>
  40. <script lang="ts" setup>
  41. import wdButton from '../wd-button/wd-button.vue'
  42. import wdIcon from '../wd-icon/wd-icon.vue'
  43. import wdTransition from '../wd-transition/wd-transition.vue'
  44. import { type CSSProperties, computed, ref, watch, inject, getCurrentInstance, onBeforeUnmount, onMounted, nextTick } from 'vue'
  45. import { getRect, isDef, isH5, objToStyle } from '../common/util'
  46. import { type Queue, queueKey } from '../composables/useQueue'
  47. import { closeOther, pushToQueue, removeFromQueue } from '../common/clickoutside'
  48. import { fabProps, type FabExpose } from './types'
  49. import { reactive } from 'vue'
  50. import { useRaf } from '../composables/useRaf'
  51. const props = defineProps(fabProps)
  52. const emit = defineEmits(['update:active', 'click'])
  53. const inited = ref<boolean>(false) // 是否初始化完成
  54. const isActive = ref<boolean>(false) // 是否激活状态
  55. const queue = inject<Queue | null>(queueKey, null)
  56. const { proxy } = getCurrentInstance() as any
  57. watch(
  58. () => props.active,
  59. (newValue) => {
  60. isActive.value = newValue
  61. },
  62. { immediate: true, deep: true }
  63. )
  64. watch(
  65. () => isActive.value,
  66. (newValue) => {
  67. if (newValue) {
  68. if (queue && queue.closeOther) {
  69. queue.closeOther(proxy)
  70. } else {
  71. closeOther(proxy)
  72. }
  73. }
  74. }
  75. )
  76. const fabDirection = ref(props.direction)
  77. watch(
  78. () => props.direction,
  79. (direction) => (fabDirection.value = direction)
  80. )
  81. watch(
  82. () => props.position,
  83. () => initPosition()
  84. )
  85. const top = ref<number>(0)
  86. const left = ref<number>(0)
  87. const screen = reactive({ width: 0, height: 0 })
  88. const fabSize = reactive({ width: 56, height: 56 })
  89. const bounding = reactive({
  90. minTop: 0,
  91. minLeft: 0,
  92. maxTop: 0,
  93. maxLeft: 0
  94. })
  95. async function getBounding() {
  96. const sysInfo = uni.getSystemInfoSync()
  97. try {
  98. const trigerInfo = await getRect('#trigger', false, proxy)
  99. fabSize.width = trigerInfo.width || 56
  100. fabSize.height = trigerInfo.height || 56
  101. } catch (error) {
  102. console.log(error)
  103. }
  104. const { top = 16, left = 16, right = 16, bottom = 16 } = props.gap
  105. screen.width = sysInfo.windowWidth
  106. screen.height = isH5 ? sysInfo.windowTop + sysInfo.windowHeight : sysInfo.windowHeight
  107. bounding.minTop = isH5 ? sysInfo.windowTop + top : top
  108. bounding.minLeft = left
  109. bounding.maxLeft = screen.width - fabSize.width - right
  110. bounding.maxTop = screen.height - fabSize.height - bottom
  111. }
  112. function initPosition() {
  113. const pos = props.position
  114. const { minLeft, minTop, maxLeft, maxTop } = bounding
  115. const centerY = (maxTop + minTop) / 2
  116. const centerX = (maxLeft + minLeft) / 2
  117. switch (pos) {
  118. case 'left-top':
  119. top.value = minTop
  120. left.value = minLeft
  121. break
  122. case 'right-top':
  123. top.value = minTop
  124. left.value = maxLeft
  125. break
  126. case 'left-bottom':
  127. top.value = maxTop
  128. left.value = minLeft
  129. break
  130. case 'right-bottom':
  131. top.value = maxTop
  132. left.value = maxLeft
  133. break
  134. case 'left-center':
  135. top.value = centerY
  136. left.value = minLeft
  137. break
  138. case 'right-center':
  139. top.value = centerY
  140. left.value = maxLeft
  141. break
  142. case 'top-center':
  143. top.value = minTop
  144. left.value = centerX
  145. break
  146. case 'bottom-center':
  147. top.value = maxTop
  148. left.value = centerX
  149. break
  150. }
  151. }
  152. // 按下时坐标相对于元素的偏移量
  153. const touchOffset = reactive({ x: 0, y: 0 })
  154. const attractTransition = ref<boolean>(false)
  155. function handleTouchStart(e: TouchEvent) {
  156. if (props.draggable === false) return
  157. const touch = e.touches[0]
  158. touchOffset.x = touch.clientX - left.value
  159. touchOffset.y = touch.clientY - top.value
  160. attractTransition.value = false
  161. }
  162. function handleTouchMove(e: TouchEvent) {
  163. if (props.draggable === false) return
  164. const touch = e.touches[0]
  165. const { minLeft, minTop, maxLeft, maxTop } = bounding
  166. let x = touch.clientX - touchOffset.x
  167. let y = touch.clientY - touchOffset.y
  168. if (x < minLeft) x = minLeft
  169. else if (x > maxLeft) x = maxLeft
  170. if (y < minTop) y = minTop
  171. else if (y > maxTop) y = maxTop
  172. top.value = y
  173. left.value = x
  174. }
  175. function handleTouchEnd() {
  176. if (props.draggable === false) return
  177. const screenCenterX = screen.width / 2
  178. const fabCenterX = left.value + fabSize.width / 2
  179. attractTransition.value = true
  180. if (fabCenterX < screenCenterX) {
  181. left.value = bounding.minLeft
  182. fabDirection.value = 'right'
  183. } else {
  184. left.value = bounding.maxLeft
  185. fabDirection.value = 'left'
  186. }
  187. }
  188. const rootStyle = computed(() => {
  189. const style: CSSProperties = {
  190. top: top.value + 'px',
  191. left: left.value + 'px',
  192. transition: attractTransition.value ? 'all ease 0.3s' : 'none'
  193. }
  194. if (isDef(props.zIndex)) {
  195. style['z-index'] = props.zIndex
  196. }
  197. return `${objToStyle(style)}${props.customStyle}`
  198. })
  199. onMounted(() => {
  200. if (queue && queue.pushToQueue) {
  201. queue.pushToQueue(proxy)
  202. } else {
  203. pushToQueue(proxy)
  204. }
  205. const { start } = useRaf(async () => {
  206. await getBounding()
  207. initPosition()
  208. inited.value = true
  209. })
  210. start()
  211. })
  212. onBeforeUnmount(() => {
  213. if (queue && queue.removeFromQueue) {
  214. queue.removeFromQueue(proxy)
  215. } else {
  216. removeFromQueue(proxy)
  217. }
  218. })
  219. function handleClick() {
  220. if (props.disabled) {
  221. return
  222. }
  223. if (!props.expandable) {
  224. emit('click')
  225. return
  226. }
  227. isActive.value = !isActive.value
  228. emit('update:active', isActive.value)
  229. }
  230. function open() {
  231. isActive.value = true
  232. emit('update:active', true)
  233. }
  234. function close() {
  235. isActive.value = false
  236. emit('update:active', false)
  237. }
  238. defineExpose<FabExpose>({
  239. open,
  240. close
  241. })
  242. </script>
  243. <style lang="scss" scoped>
  244. @import './index.scss';
  245. </style>