|
|
@@ -0,0 +1,209 @@
|
|
|
+<template>
|
|
|
+ <!-- H5:SortableJS -->
|
|
|
+ <!-- #ifdef H5 -->
|
|
|
+ <view ref="h5ListRef" class="mt-30 flex flex-col">
|
|
|
+ <view
|
|
|
+ v-for="(m, i) in innerMajors"
|
|
|
+ :key="getKey(m, i)"
|
|
|
+ class="bg-back-light rounded-lg p-20 flex justify-between items-center gap-20 mb-20 min-w-0"
|
|
|
+ :data-index="i"
|
|
|
+ >
|
|
|
+ <view class="text-32 text-fore-placeholder font-bold">{{ toFixedLen(i) }}</view>
|
|
|
+ <view class="flex-1 w-0 text-28 text-fore-title font-bold truncate">{{ m.majorName }}</view>
|
|
|
+
|
|
|
+ <uv-icon name="trash" size="18" color="error"/>
|
|
|
+
|
|
|
+ <!-- 拖拽手柄:Sortable handle -->
|
|
|
+ <view class="drag-handle">
|
|
|
+ <uv-icon name="list-dot" size="18" color="primary"/>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <!-- #endif -->
|
|
|
+
|
|
|
+ <!-- #ifndef H5 -->
|
|
|
+ <movable-area
|
|
|
+ class="mt-30 w-full"
|
|
|
+ :style="{ height: areaHeight + 'px' }"
|
|
|
+ :catchtouchmove="dragging"
|
|
|
+ @touchmove.stop.prevent="noop"
|
|
|
+ >
|
|
|
+ <movable-view
|
|
|
+ v-for="(m, i) in innerMajors"
|
|
|
+ :key="getKey(m, i)"
|
|
|
+ class="w-full"
|
|
|
+ direction="vertical"
|
|
|
+ :y="getY(i)"
|
|
|
+ :disabled="activeIndex !== i"
|
|
|
+ :inertia="false"
|
|
|
+ :damping="80"
|
|
|
+ :animation="true"
|
|
|
+ @change="(e) => onDragChange(e, i)"
|
|
|
+ @touchend="onDragEnd"
|
|
|
+ >
|
|
|
+ <view class="bg-back-light rounded-lg p-20 flex justify-between items-center gap-20">
|
|
|
+ <!-- 主内容:阻断触摸,确保只能手柄拖 -->
|
|
|
+ <view class="flex-1 flex items-center gap-20" @touchstart.stop @touchmove.stop>
|
|
|
+ <view class="text-32 text-fore-placeholder font-bold">{{ toFixedLen(i) }}</view>
|
|
|
+ <view class="flex-1 w-0 text-28 text-fore-title font-bold truncate">{{ m.majorName }}</view>
|
|
|
+ <uv-icon name="trash" size="18" color="error" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 手柄:不 stop / prevent -->
|
|
|
+ <view class="ml-10" @touchstart="onHandleDown(i)" @touchend="onHandleUp">
|
|
|
+ <uv-icon name="list-dot" size="18" color="primary" />
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </movable-view>
|
|
|
+ </movable-area>
|
|
|
+ <!-- #endif -->
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import {VoluntaryMajorItem} from "@/types/voluntary";
|
|
|
+import {VOLUNTARY_SORTING} from "@/types/injectionSymbols";
|
|
|
+// #ifdef H5
|
|
|
+import Sortable from "sortablejs";
|
|
|
+// #endif
|
|
|
+
|
|
|
+const props = defineProps<{
|
|
|
+ majors: VoluntaryMajorItem[];
|
|
|
+}>();
|
|
|
+
|
|
|
+const emits = defineEmits<{
|
|
|
+ (e: "update:majors", majors: VoluntaryMajorItem[]): void;
|
|
|
+}>();
|
|
|
+
|
|
|
+const isSorting = inject(VOLUNTARY_SORTING) || ref(false)
|
|
|
+const innerMajors = ref<VoluntaryMajorItem[]>([]);
|
|
|
+watch(
|
|
|
+ () => props.majors,
|
|
|
+ (v) => (innerMajors.value = v ? [...v] : []),
|
|
|
+ {immediate: true, deep: true}
|
|
|
+);
|
|
|
+
|
|
|
+function toFixedLen(i: number, len: number = 2) {
|
|
|
+ return String(i + 1).padStart(len, "0");
|
|
|
+}
|
|
|
+
|
|
|
+function getKey(m: any, i: number) {
|
|
|
+ // 你的示例 majorId 可能重复,所以拼 i
|
|
|
+ return m.majorId
|
|
|
+}
|
|
|
+
|
|
|
+// ========== H5:SortableJS ==========
|
|
|
+const h5ListRef = ref<any>(null);
|
|
|
+// #ifdef H5
|
|
|
+let sortableIns: Sortable | null = null;
|
|
|
+
|
|
|
+onMounted(async () => {
|
|
|
+ await nextTick();
|
|
|
+ const el = h5ListRef.value?.$el || h5ListRef.value; // 兼容 view ref
|
|
|
+ if (!el) return;
|
|
|
+
|
|
|
+ sortableIns = Sortable.create(el, {
|
|
|
+ animation: 150,
|
|
|
+ handle: ".drag-handle", // 只允许手柄拖
|
|
|
+ forceFallback: true,
|
|
|
+ onStart: () => {
|
|
|
+ isSorting.value = true
|
|
|
+ console.log('sortableIns onStart', true)
|
|
|
+ },
|
|
|
+ onEnd: (evt) => {
|
|
|
+ isSorting.value = false;
|
|
|
+ console.log('sortableIns onEnd', false)
|
|
|
+ // sort logic
|
|
|
+ if (evt.oldIndex == null || evt.newIndex == null) return;
|
|
|
+ const arr = [...innerMajors.value];
|
|
|
+ const [moved] = arr.splice(evt.oldIndex, 1);
|
|
|
+ arr.splice(evt.newIndex, 0, moved);
|
|
|
+ innerMajors.value = arr;
|
|
|
+ emits("update:majors", arr);
|
|
|
+ },
|
|
|
+ });
|
|
|
+});
|
|
|
+
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ sortableIns?.destroy();
|
|
|
+ sortableIns = null;
|
|
|
+});
|
|
|
+// #endif
|
|
|
+
|
|
|
+// ========== 小程序:movable-view(重写版) ==========
|
|
|
+const dragging = ref(false);
|
|
|
+const activeIndex = ref(-1);
|
|
|
+const activeY = ref(0);
|
|
|
+
|
|
|
+const rowGapPx = uni.upx2px(20);
|
|
|
+const rowHeightPx = uni.upx2px(92) + rowGapPx;
|
|
|
+const areaHeight = computed(() => innerMajors.value.length * rowHeightPx);
|
|
|
+
|
|
|
+const noop = () => {}; // 关键:给 @touchmove 一个“函数”,不要给 boolean
|
|
|
+
|
|
|
+function getY(i: number) {
|
|
|
+ return i === activeIndex.value ? activeY.value : i * rowHeightPx;
|
|
|
+}
|
|
|
+
|
|
|
+function clampY(y: number) {
|
|
|
+ const maxY = Math.max(0, (innerMajors.value.length - 1) * rowHeightPx);
|
|
|
+ return Math.min(Math.max(y, 0), maxY);
|
|
|
+}
|
|
|
+
|
|
|
+function swap(arr: any[], a: number, b: number) {
|
|
|
+ const t = arr[a];
|
|
|
+ arr[a] = arr[b];
|
|
|
+ arr[b] = t;
|
|
|
+}
|
|
|
+
|
|
|
+function onHandleDown(i: number) {
|
|
|
+ console.log("onHandleDown", i);
|
|
|
+ dragging.value = true;
|
|
|
+ activeIndex.value = i;
|
|
|
+ activeY.value = i * rowHeightPx;
|
|
|
+}
|
|
|
+
|
|
|
+function onHandleUp() {
|
|
|
+ console.log("onHandleUp");
|
|
|
+ // 不要在这里结束;结束交给 movable-view 的 touchend
|
|
|
+}
|
|
|
+
|
|
|
+function onDragChange(e: any, i: number) {
|
|
|
+ // 有了上面的报错修复后,这里应该能打出来
|
|
|
+ console.log("onDragChange", e?.detail, i);
|
|
|
+
|
|
|
+ if (!dragging.value) return;
|
|
|
+ if (i !== activeIndex.value) return;
|
|
|
+ if (e?.detail?.source !== "touch") return;
|
|
|
+
|
|
|
+ const y = clampY(Number(e.detail.y || 0));
|
|
|
+ activeY.value = y;
|
|
|
+
|
|
|
+ const target = Math.round(y / rowHeightPx);
|
|
|
+ if (target === i) return;
|
|
|
+
|
|
|
+ swap(innerMajors.value as any[], i, target);
|
|
|
+ activeIndex.value = target;
|
|
|
+}
|
|
|
+
|
|
|
+function onDragEnd() {
|
|
|
+ console.log("onDragEnd");
|
|
|
+ if (!dragging.value) return;
|
|
|
+
|
|
|
+ dragging.value = false;
|
|
|
+
|
|
|
+ if (activeIndex.value === -1) return;
|
|
|
+ activeY.value = activeIndex.value * rowHeightPx;
|
|
|
+
|
|
|
+ const arr = [...innerMajors.value];
|
|
|
+ activeIndex.value = -1;
|
|
|
+ emits("update:majors", arr);
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.drag-handle {
|
|
|
+ touch-action: none;
|
|
|
+ -webkit-user-select: none;
|
|
|
+ user-select: none;
|
|
|
+}
|
|
|
+</style>
|