wd-swipe-action.vue 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. <template>
  2. <!--注意阻止横向滑动的穿透:横向移动时阻止冒泡-->
  3. <view
  4. :class="`wd-swipe-action ${customClass}`"
  5. :style="customStyle"
  6. @click.stop="onClick()"
  7. @touchstart="startDrag"
  8. @touchmove="onDrag"
  9. @touchend="endDrag"
  10. @touchcancel="endDrag"
  11. >
  12. <!--容器-->
  13. <view class="wd-swipe-action__wrapper" :style="wrapperStyle">
  14. <!--左侧操作-->
  15. <view class="wd-swipe-action__left" @click="onClick('left')">
  16. <slot name="left" />
  17. </view>
  18. <!--内容-->
  19. <slot />
  20. <!--右侧操作-->
  21. <view class="wd-swipe-action__right" @click="onClick('right')">
  22. <slot name="right" />
  23. </view>
  24. </view>
  25. </view>
  26. </template>
  27. <script lang="ts">
  28. export default {
  29. name: 'wd-swipe-action',
  30. options: {
  31. addGlobalClass: true,
  32. virtualHost: true,
  33. styleIsolation: 'shared'
  34. }
  35. }
  36. </script>
  37. <script lang="ts" setup>
  38. import { getCurrentInstance, inject, onBeforeMount, onBeforeUnmount, onMounted, ref, watch } from 'vue'
  39. import { closeOther, pushToQueue, removeFromQueue } from '../common/clickoutside'
  40. import { type Queue, queueKey } from '../composables/useQueue'
  41. import { useTouch } from '../composables/useTouch'
  42. import { getRect } from '../common/util'
  43. import { swipeActionProps, type SwipeActionPosition, type SwipeActionReason, type SwipeActionStatus } from './types'
  44. const props = defineProps(swipeActionProps)
  45. const emit = defineEmits(['click', 'update:modelValue'])
  46. const queue = inject<Queue | null>(queueKey, null)
  47. const wrapperStyle = ref<string>('')
  48. // 滑动开始时,wrapper的偏移量
  49. const originOffset = ref<number>(0)
  50. // wrapper现在的偏移量
  51. const wrapperOffset = ref<number>(0)
  52. // 是否处于滑动状态
  53. const touching = ref<boolean>(false)
  54. const touch = useTouch()
  55. const { proxy } = getCurrentInstance() as any
  56. watch(
  57. () => props.modelValue,
  58. (value, old) => {
  59. changeState(value, old)
  60. },
  61. {
  62. deep: true
  63. }
  64. )
  65. onBeforeMount(() => {
  66. if (queue && queue.pushToQueue) {
  67. queue.pushToQueue(proxy)
  68. } else {
  69. pushToQueue(proxy)
  70. }
  71. // 滑动开始时,wrapper的偏移量
  72. originOffset.value = 0
  73. // wrapper现在的偏移量
  74. wrapperOffset.value = 0
  75. // 是否处于滑动状态
  76. touching.value = false
  77. })
  78. onMounted(() => {
  79. touching.value = true
  80. changeState(props.modelValue)
  81. touching.value = false
  82. })
  83. onBeforeUnmount(() => {
  84. if (queue && queue.removeFromQueue) {
  85. queue.removeFromQueue(proxy)
  86. } else {
  87. removeFromQueue(proxy)
  88. }
  89. })
  90. function changeState(value: SwipeActionStatus, old?: SwipeActionStatus) {
  91. if (props.disabled) {
  92. return
  93. }
  94. getWidths().then(([leftWidth, rightWidth]) => {
  95. switch (value) {
  96. case 'close':
  97. // 调用此函数时,偏移量本就是0
  98. if (wrapperOffset.value === 0) return
  99. close('value', old)
  100. break
  101. case 'left':
  102. swipeMove(leftWidth)
  103. break
  104. case 'right':
  105. swipeMove(-rightWidth)
  106. break
  107. }
  108. })
  109. }
  110. /**
  111. * @description 获取左/右操作按钮的宽度
  112. * @return {Promise<[Number, Number]>} 左宽度、右宽度
  113. */
  114. function getWidths() {
  115. return Promise.all([
  116. getRect('.wd-swipe-action__left', false, proxy).then((rect) => {
  117. return rect.width ? rect.width : 0
  118. }),
  119. getRect('.wd-swipe-action__right', false, proxy).then((rect) => {
  120. return rect.width ? rect.width : 0
  121. })
  122. ])
  123. }
  124. /**
  125. * @description wrapper滑动函数
  126. * @param {Number} offset 滑动漂移量
  127. */
  128. function swipeMove(offset = 0) {
  129. // this.offset = offset
  130. const transform = `translate3d(${offset}px, 0, 0)`
  131. // 跟随手指滑动,不需要动画
  132. const transition = touching.value ? 'none' : '.6s cubic-bezier(0.18, 0.89, 0.32, 1)'
  133. wrapperStyle.value = `
  134. -webkit-transform: ${transform};
  135. -webkit-transition: ${transition};
  136. transform: ${transform};
  137. transition: ${transition};
  138. `
  139. // 记录容器当前偏移的量
  140. wrapperOffset.value = offset
  141. }
  142. /**
  143. * @description click的handler
  144. * @param event
  145. */
  146. function onClick(position?: SwipeActionPosition) {
  147. if (props.disabled || wrapperOffset.value === 0) {
  148. return
  149. }
  150. position = position || 'inside'
  151. close('click', position)
  152. emit('click', {
  153. value: position
  154. })
  155. }
  156. /**
  157. * @description 开始滑动
  158. */
  159. function startDrag(event: TouchEvent) {
  160. if (props.disabled) return
  161. originOffset.value = wrapperOffset.value
  162. touch.touchStart(event)
  163. if (queue && queue.closeOther) {
  164. queue.closeOther(proxy)
  165. } else {
  166. closeOther(proxy)
  167. }
  168. }
  169. /**
  170. * @description 滑动时,逐渐展示按钮
  171. * @param event
  172. */
  173. function onDrag(event: TouchEvent) {
  174. if (props.disabled) return
  175. touch.touchMove(event)
  176. if (touch.direction.value === 'vertical') {
  177. return
  178. } else {
  179. event.preventDefault()
  180. event.stopPropagation()
  181. }
  182. touching.value = true
  183. // 本次滑动,wrapper应该设置的偏移量
  184. const offset = originOffset.value + touch.deltaX.value
  185. getWidths().then(([leftWidth, rightWidth]) => {
  186. // 如果需要想滑出来的按钮不存在,对应的按钮肯定滑不出来,容器处于初始状态。此时需要模拟一下位于此处的start事件。
  187. if ((leftWidth === 0 && offset > 0) || (rightWidth === 0 && offset < 0)) {
  188. swipeMove(0)
  189. return startDrag(event)
  190. }
  191. // 按钮已经展示完了,再滑动没有任何意义,相当于滑动结束。此时需要模拟一下位于此处的start事件。
  192. if (leftWidth !== 0 && offset >= leftWidth) {
  193. swipeMove(leftWidth)
  194. return startDrag(event)
  195. } else if (rightWidth !== 0 && -offset >= rightWidth) {
  196. swipeMove(-rightWidth)
  197. return startDrag(event)
  198. }
  199. swipeMove(offset)
  200. })
  201. }
  202. /**
  203. * @description 滑动结束,自动修正位置
  204. */
  205. function endDrag() {
  206. if (props.disabled) return
  207. // 滑出"操作按钮"的阈值
  208. const THRESHOLD = 0.3
  209. touching.value = false
  210. getWidths().then(([leftWidth, rightWidth]) => {
  211. if (
  212. originOffset.value < 0 && // 之前展示的是右按钮
  213. wrapperOffset.value < 0 && // 目前仍然是右按钮
  214. wrapperOffset.value - originOffset.value < rightWidth * THRESHOLD // 并且滑动的范围不超过右边框阀值
  215. ) {
  216. swipeMove(-rightWidth) // 回归右按钮
  217. emit('update:modelValue', 'right')
  218. } else if (
  219. originOffset.value > 0 && // 之前展示的是左按钮
  220. wrapperOffset.value > 0 && // 现在仍然是左按钮
  221. originOffset.value - wrapperOffset.value < leftWidth * THRESHOLD // 并且滑动的范围不超过左按钮阀值
  222. ) {
  223. swipeMove(leftWidth) // 回归左按钮
  224. emit('update:modelValue', 'left')
  225. } else if (
  226. rightWidth > 0 &&
  227. originOffset.value >= 0 && // 之前是初始状态或者展示左按钮显
  228. wrapperOffset.value < 0 && // 现在展示右按钮
  229. Math.abs(wrapperOffset.value) > rightWidth * THRESHOLD // 视图中已经展示的右按钮长度超过阀值
  230. ) {
  231. swipeMove(-rightWidth)
  232. emit('update:modelValue', 'right')
  233. } else if (
  234. leftWidth > 0 &&
  235. originOffset.value <= 0 && // 之前初始状态或者右按钮显示
  236. wrapperOffset.value > 0 && // 现在左按钮
  237. Math.abs(wrapperOffset.value) > leftWidth * THRESHOLD // 视图中已经展示的左按钮长度超过阀值
  238. ) {
  239. swipeMove(leftWidth)
  240. emit('update:modelValue', 'left')
  241. } else {
  242. // 回归初始状态
  243. close('swipe')
  244. }
  245. })
  246. }
  247. /**
  248. * @description 关闭操过按钮,并在合适的时候调用 beforeClose
  249. */
  250. function close(reason: SwipeActionReason, position?: SwipeActionPosition) {
  251. if (reason === 'swipe' && originOffset.value === 0) {
  252. // offset:0 ——> offset:0
  253. return swipeMove(0)
  254. } else if (reason === 'swipe' && originOffset.value > 0) {
  255. // offset > 0 ——> offset:0
  256. position = 'left'
  257. } else if (reason === 'swipe' && originOffset.value < 0) {
  258. // offset < 0 ——> offset:0
  259. position = 'right'
  260. }
  261. if (reason && position) {
  262. props.beforeClose && props.beforeClose(reason, position)
  263. }
  264. swipeMove(0)
  265. if (props.modelValue !== 'close') {
  266. emit('update:modelValue', 'close')
  267. }
  268. }
  269. defineExpose({ close })
  270. </script>
  271. <style lang="scss" scoped>
  272. @import './index.scss';
  273. </style>