m-drag.vue 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. <template>
  2. <scroll-view class="m-drag" scroll-y :style="{ height: itemHeight * state.newList.length + 'px' }">
  3. <view
  4. v-for="(item, index) in state.newList"
  5. :key="index"
  6. class="m-drag-item"
  7. :class="{ active: state.currentIndex === index }"
  8. :style="{ top: state.itemYList[index].top + 'px' }">
  9. <slot v-bind="{item, index}"/>
  10. <view class="icon" @touchstart="touchStart($event, index)" @touchmove="touchMove" @touchend="touchEnd">
  11. <i class="lines"/>
  12. </view>
  13. </view>
  14. </scroll-view>
  15. </template>
  16. <script setup>
  17. import {reactive, watch, toRefs} from 'vue';
  18. const emits = defineEmits(['change'])
  19. const props = defineProps({
  20. // 每一项item高度
  21. itemHeight: {
  22. type: Number,
  23. required: true
  24. },
  25. // 数据列表
  26. list: {
  27. type: Array,
  28. required: true
  29. },
  30. // 是否只读
  31. readonly: {
  32. type: Boolean,
  33. default: false
  34. }
  35. })
  36. const state = reactive({
  37. // 数据
  38. newList: [],
  39. // 记录所有item的初始坐标
  40. initialItemYList: [],
  41. // 坐标数据
  42. itemYList: [],
  43. // 记录当前手指的垂直方向的坐标
  44. touchY: 0,
  45. // 记录当前操作的item数据
  46. currentItemY: {},
  47. // 当前操作的item的下标
  48. currentIndex: -1,
  49. // 正在拖拽
  50. dragging: false
  51. })
  52. watch(
  53. () => props.list,
  54. (val) => {
  55. if (!val?.length) return
  56. // 获取数据列表
  57. state.newList = [...val] // copy array
  58. // 获取所有item的初始坐标
  59. state.initialItemYList = getItemsY()
  60. // 初始化坐标
  61. state.itemYList = getItemsY()
  62. },
  63. {
  64. immediate: true
  65. }
  66. )
  67. /** @初始化各个item的坐标 **/
  68. function getItemsY() {
  69. return props.list.map((item, i) => {
  70. return {
  71. left: 0,
  72. top: i * props.itemHeight
  73. }
  74. })
  75. }
  76. /** @开始触摸 */
  77. function touchStart(event, index) {
  78. // 只读
  79. if (props.readonly) return
  80. // H5拖拽时,禁止触发ios回弹
  81. h5BodyScroll(false)
  82. const [{pageY}] = event.touches
  83. // 记录数据
  84. state.dragging = true
  85. state.currentIndex = index
  86. state.touchY = pageY
  87. state.currentItemY = state.itemYList[index]
  88. }
  89. /** @手指滑动 **/
  90. function touchMove(event) {
  91. // 只读
  92. if (props.readonly) return
  93. const [{pageY}] = event.touches
  94. const current = state.itemYList[state.currentIndex]
  95. const prep = state.itemYList[state.currentIndex - 1]
  96. const next = state.itemYList[state.currentIndex + 1]
  97. // 获取移动差值
  98. state.itemYList[state.currentIndex] = {
  99. top: current.top + (pageY - state.touchY)
  100. }
  101. // 记录手指坐标
  102. state.touchY = pageY
  103. // 向下移动(超过下一个的1/2就进行换位)
  104. if (next && current.top > next.top - props.itemHeight / 2) {
  105. changePosition(state.currentIndex + 1)
  106. } else if (prep && current.top < prep.top + props.itemHeight / 2) {
  107. // 向上移动(超过上一个的1/2就进行换位)
  108. changePosition(state.currentIndex - 1)
  109. }
  110. }
  111. /** @手指松开 */
  112. function touchEnd() {
  113. // 只读
  114. if (props.readonly) return
  115. // 传给父组件新数据
  116. emits('change', state.newList, props.list, state.newList[state.currentIndex])
  117. // 将拖拽的item归位
  118. state.itemYList[state.currentIndex] = state.initialItemYList[state.currentIndex]
  119. state.currentIndex = -1
  120. // H5开启ios回弹
  121. h5BodyScroll(true)
  122. state.dragging = false
  123. }
  124. /** @交换位置 **/
  125. // index 需要与第几个下标交换位置
  126. function changePosition(index) {
  127. console.log(index)
  128. // 记录当前拖拽的item数据
  129. const tempItem = state.newList[state.currentIndex]
  130. // 设置原来位置的item
  131. state.newList[state.currentIndex] = state.newList[index]
  132. // 将临时存放的数据设置好
  133. state.newList[index] = tempItem
  134. // 调整位置item
  135. state.itemYList[index] = state.itemYList[state.currentIndex]
  136. state.itemYList[state.currentIndex] = state.currentItemY
  137. // 改变当前操作的的下标
  138. state.currentIndex = index
  139. // 记录新位置的数据
  140. state.currentItemY = state.initialItemYList[state.currentIndex]
  141. }
  142. // h5 ios回弹
  143. function h5BodyScroll(flag) {
  144. // #ifdef H5
  145. document.body.style.overflow = flag ? 'initial' : 'hidden'
  146. // #endif
  147. }
  148. defineExpose({...toRefs(state)})
  149. </script>
  150. <style scoped lang="scss">
  151. .m-drag {
  152. position: relative;
  153. width: 100%;
  154. ::-webkit-scrollbar {
  155. display: none;
  156. }
  157. .m-drag-item {
  158. position: absolute;
  159. left: 0;
  160. right: 0;
  161. transition: all ease 0.25s;
  162. display: flex;
  163. align-items: center;
  164. > :deep(view:not(.icon)) {
  165. flex: 1;
  166. }
  167. .icon {
  168. padding: 30rpx;
  169. .lines {
  170. background: #e0e0e0;
  171. width: 20px;
  172. height: 2px;
  173. border-radius: 100rpx;
  174. margin-left: auto;
  175. position: relative;
  176. display: block;
  177. transition: all ease 0.25s;
  178. &::before,
  179. &::after {
  180. position: absolute;
  181. width: inherit;
  182. height: inherit;
  183. border-radius: inherit;
  184. background: #e0e0e0;
  185. transition: inherit;
  186. content: '';
  187. display: block;
  188. }
  189. &::before {
  190. top: -14rpx;
  191. }
  192. &::after {
  193. bottom: -14rpx;
  194. }
  195. }
  196. }
  197. // 拖拽中的元素,添加阴影、关闭动画、层级置顶
  198. &.active {
  199. box-shadow: 0 0 14rpx rgba(0, 0, 0, 0.08);
  200. transition: initial;
  201. z-index: 1;
  202. .icon .lines {
  203. background: #2e97f9;
  204. &::before,
  205. &::after {
  206. background: #2e97f9;
  207. }
  208. }
  209. }
  210. }
  211. }
  212. </style>