voluntary-majors-draggable.vue 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  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" @click="$emit('delete', m)"/>
  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" @click="$emit('delete', m)" />
  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: "delete", major: VoluntaryMajorItem): void;
  68. (e: "update:majors", majors: VoluntaryMajorItem[]): void;
  69. (e: "change", majors: VoluntaryMajorItem[]): void;
  70. }>();
  71. const isSorting = inject(VOLUNTARY_SORTING) || ref(false)
  72. const innerMajors = ref<VoluntaryMajorItem[]>([]);
  73. watch(
  74. () => props.majors,
  75. (v) => (innerMajors.value = v ? [...v] : []),
  76. {immediate: true, deep: true}
  77. );
  78. function toFixedLen(i: number, len: number = 2) {
  79. return String(i + 1).padStart(len, "0");
  80. }
  81. function getKey(m: any, i: number) {
  82. // 你的示例 majorId 可能重复,所以拼 i
  83. return m.majorId
  84. }
  85. // ========== H5:SortableJS ==========
  86. const h5ListRef = ref<any>(null);
  87. // #ifdef H5
  88. let sortableIns: Sortable | null = null;
  89. onMounted(async () => {
  90. await nextTick();
  91. const el = h5ListRef.value?.$el || h5ListRef.value; // 兼容 view ref
  92. if (!el) return;
  93. sortableIns = Sortable.create(el, {
  94. animation: 150,
  95. handle: ".drag-handle", // 只允许手柄拖
  96. forceFallback: true,
  97. onStart: () => {
  98. isSorting.value = true
  99. console.log('sortableIns onStart', true)
  100. },
  101. onEnd: (evt) => {
  102. isSorting.value = false;
  103. console.log('sortableIns onEnd', false)
  104. // sort logic
  105. if (evt.oldIndex == null || evt.newIndex == null) return;
  106. const arr = [...innerMajors.value];
  107. const [moved] = arr.splice(evt.oldIndex, 1);
  108. arr.splice(evt.newIndex, 0, moved);
  109. innerMajors.value = arr;
  110. emits("update:majors", arr);
  111. emits('change', arr)
  112. },
  113. });
  114. });
  115. onBeforeUnmount(() => {
  116. sortableIns?.destroy();
  117. sortableIns = null;
  118. });
  119. // #endif
  120. // ========== 小程序:movable-view(重写版) ==========
  121. const dragging = ref(false);
  122. const activeIndex = ref(-1);
  123. const activeY = ref(0);
  124. const rowGapPx = uni.upx2px(20);
  125. const rowHeightPx = uni.upx2px(92) + rowGapPx;
  126. const areaHeight = computed(() => innerMajors.value.length * rowHeightPx);
  127. const noop = () => {}; // 关键:给 @touchmove 一个“函数”,不要给 boolean
  128. function getY(i: number) {
  129. return i === activeIndex.value ? activeY.value : i * rowHeightPx;
  130. }
  131. function clampY(y: number) {
  132. const maxY = Math.max(0, (innerMajors.value.length - 1) * rowHeightPx);
  133. return Math.min(Math.max(y, 0), maxY);
  134. }
  135. function swap(arr: any[], a: number, b: number) {
  136. const t = arr[a];
  137. arr[a] = arr[b];
  138. arr[b] = t;
  139. }
  140. function onHandleDown(i: number) {
  141. console.log("onHandleDown", i);
  142. dragging.value = true;
  143. activeIndex.value = i;
  144. activeY.value = i * rowHeightPx;
  145. }
  146. function onHandleUp() {
  147. console.log("onHandleUp");
  148. // 不要在这里结束;结束交给 movable-view 的 touchend
  149. }
  150. function onDragChange(e: any, i: number) {
  151. // 有了上面的报错修复后,这里应该能打出来
  152. console.log("onDragChange", e?.detail, i);
  153. if (!dragging.value) return;
  154. if (i !== activeIndex.value) return;
  155. if (e?.detail?.source !== "touch") return;
  156. const y = clampY(Number(e.detail.y || 0));
  157. activeY.value = y;
  158. const target = Math.round(y / rowHeightPx);
  159. if (target === i) return;
  160. swap(innerMajors.value as any[], i, target);
  161. activeIndex.value = target;
  162. }
  163. function onDragEnd() {
  164. console.log("onDragEnd");
  165. if (!dragging.value) return;
  166. dragging.value = false;
  167. if (activeIndex.value === -1) return;
  168. activeY.value = activeIndex.value * rowHeightPx;
  169. const arr = [...innerMajors.value];
  170. activeIndex.value = -1;
  171. console.log('emits update:majors', arr)
  172. emits("update:majors", arr);
  173. emits('change', arr);
  174. }
  175. </script>
  176. <style scoped>
  177. .drag-handle {
  178. touch-action: none;
  179. -webkit-user-select: none;
  180. user-select: none;
  181. }
  182. </style>