voluntary-majors-draggable.vue 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. <template>
  2. <!-- H5:SortableJS -->
  3. <!-- #ifdef H5 -->
  4. <view ref="h5ListRef" class="mt-30 flex flex-col">
  5. <view
  6. v-for="(m, i) in innerMajors"
  7. :key="getKey(m, i)"
  8. class="bg-back-light rounded-lg p-20 flex justify-between items-center gap-20 mb-20 min-w-0"
  9. :data-index="i"
  10. >
  11. <view class="text-32 text-fore-placeholder font-bold">{{ toFixedLen(i) }}</view>
  12. <view class="flex-1 w-0 text-28 text-fore-title font-bold truncate">{{ m.majorName }}</view>
  13. <uv-icon name="trash" size="18" color="error"/>
  14. <!-- 拖拽手柄:Sortable handle -->
  15. <view class="drag-handle">
  16. <uv-icon name="list-dot" size="18" color="primary"/>
  17. </view>
  18. </view>
  19. </view>
  20. <!-- #endif -->
  21. <!-- #ifndef H5 -->
  22. <movable-area
  23. class="mt-30 w-full"
  24. :style="{ height: areaHeight + 'px' }"
  25. :catchtouchmove="dragging"
  26. @touchmove.stop.prevent="noop"
  27. >
  28. <movable-view
  29. v-for="(m, i) in innerMajors"
  30. :key="getKey(m, i)"
  31. class="w-full"
  32. direction="vertical"
  33. :y="getY(i)"
  34. :disabled="activeIndex !== i"
  35. :inertia="false"
  36. :damping="80"
  37. :animation="true"
  38. @change="(e) => onDragChange(e, i)"
  39. @touchend="onDragEnd"
  40. >
  41. <view class="bg-back-light rounded-lg p-20 flex justify-between items-center gap-20">
  42. <!-- 主内容:阻断触摸,确保只能手柄拖 -->
  43. <view class="flex-1 flex items-center gap-20" @touchstart.stop @touchmove.stop>
  44. <view class="text-32 text-fore-placeholder font-bold">{{ toFixedLen(i) }}</view>
  45. <view class="flex-1 w-0 text-28 text-fore-title font-bold truncate">{{ m.majorName }}</view>
  46. <uv-icon name="trash" size="18" color="error" />
  47. </view>
  48. <!-- 手柄:不 stop / prevent -->
  49. <view class="ml-10" @touchstart="onHandleDown(i)" @touchend="onHandleUp">
  50. <uv-icon name="list-dot" size="18" color="primary" />
  51. </view>
  52. </view>
  53. </movable-view>
  54. </movable-area>
  55. <!-- #endif -->
  56. </template>
  57. <script setup lang="ts">
  58. import {VoluntaryMajorItem} from "@/types/voluntary";
  59. import {VOLUNTARY_SORTING} from "@/types/injectionSymbols";
  60. // #ifdef H5
  61. import Sortable from "sortablejs";
  62. // #endif
  63. const props = defineProps<{
  64. majors: VoluntaryMajorItem[];
  65. }>();
  66. const emits = defineEmits<{
  67. (e: "update:majors", majors: VoluntaryMajorItem[]): void;
  68. (e: "change", majors: VoluntaryMajorItem[]): void;
  69. }>();
  70. const isSorting = inject(VOLUNTARY_SORTING) || ref(false)
  71. const innerMajors = ref<VoluntaryMajorItem[]>([]);
  72. watch(
  73. () => props.majors,
  74. (v) => (innerMajors.value = v ? [...v] : []),
  75. {immediate: true, deep: true}
  76. );
  77. function toFixedLen(i: number, len: number = 2) {
  78. return String(i + 1).padStart(len, "0");
  79. }
  80. function getKey(m: any, i: number) {
  81. // 你的示例 majorId 可能重复,所以拼 i
  82. return m.majorId
  83. }
  84. // ========== H5:SortableJS ==========
  85. const h5ListRef = ref<any>(null);
  86. // #ifdef H5
  87. let sortableIns: Sortable | null = null;
  88. onMounted(async () => {
  89. await nextTick();
  90. const el = h5ListRef.value?.$el || h5ListRef.value; // 兼容 view ref
  91. if (!el) return;
  92. sortableIns = Sortable.create(el, {
  93. animation: 150,
  94. handle: ".drag-handle", // 只允许手柄拖
  95. forceFallback: true,
  96. onStart: () => {
  97. isSorting.value = true
  98. console.log('sortableIns onStart', true)
  99. },
  100. onEnd: (evt) => {
  101. isSorting.value = false;
  102. console.log('sortableIns onEnd', false)
  103. // sort logic
  104. if (evt.oldIndex == null || evt.newIndex == null) return;
  105. const arr = [...innerMajors.value];
  106. const [moved] = arr.splice(evt.oldIndex, 1);
  107. arr.splice(evt.newIndex, 0, moved);
  108. innerMajors.value = arr;
  109. emits("update:majors", arr);
  110. emits('change', arr)
  111. },
  112. });
  113. });
  114. onBeforeUnmount(() => {
  115. sortableIns?.destroy();
  116. sortableIns = null;
  117. });
  118. // #endif
  119. // ========== 小程序:movable-view(重写版) ==========
  120. const dragging = ref(false);
  121. const activeIndex = ref(-1);
  122. const activeY = ref(0);
  123. const rowGapPx = uni.upx2px(20);
  124. const rowHeightPx = uni.upx2px(92) + rowGapPx;
  125. const areaHeight = computed(() => innerMajors.value.length * rowHeightPx);
  126. const noop = () => {}; // 关键:给 @touchmove 一个“函数”,不要给 boolean
  127. function getY(i: number) {
  128. return i === activeIndex.value ? activeY.value : i * rowHeightPx;
  129. }
  130. function clampY(y: number) {
  131. const maxY = Math.max(0, (innerMajors.value.length - 1) * rowHeightPx);
  132. return Math.min(Math.max(y, 0), maxY);
  133. }
  134. function swap(arr: any[], a: number, b: number) {
  135. const t = arr[a];
  136. arr[a] = arr[b];
  137. arr[b] = t;
  138. }
  139. function onHandleDown(i: number) {
  140. console.log("onHandleDown", i);
  141. dragging.value = true;
  142. activeIndex.value = i;
  143. activeY.value = i * rowHeightPx;
  144. }
  145. function onHandleUp() {
  146. console.log("onHandleUp");
  147. // 不要在这里结束;结束交给 movable-view 的 touchend
  148. }
  149. function onDragChange(e: any, i: number) {
  150. // 有了上面的报错修复后,这里应该能打出来
  151. console.log("onDragChange", e?.detail, i);
  152. if (!dragging.value) return;
  153. if (i !== activeIndex.value) return;
  154. if (e?.detail?.source !== "touch") return;
  155. const y = clampY(Number(e.detail.y || 0));
  156. activeY.value = y;
  157. const target = Math.round(y / rowHeightPx);
  158. if (target === i) return;
  159. swap(innerMajors.value as any[], i, target);
  160. activeIndex.value = target;
  161. }
  162. function onDragEnd() {
  163. console.log("onDragEnd");
  164. if (!dragging.value) return;
  165. dragging.value = false;
  166. if (activeIndex.value === -1) return;
  167. activeY.value = activeIndex.value * rowHeightPx;
  168. const arr = [...innerMajors.value];
  169. activeIndex.value = -1;
  170. console.log('emits update:majors', arr)
  171. emits("update:majors", arr);
  172. emits('change', arr);
  173. }
  174. </script>
  175. <style scoped>
  176. .drag-handle {
  177. touch-action: none;
  178. -webkit-user-select: none;
  179. user-select: none;
  180. }
  181. </style>