Feat: 拖拽功能增加移动端支持
This commit is contained in:
parent
1e17531e6a
commit
f38493be2c
|
|
@ -1,7 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from "vue";
|
||||
import BaseDrawer from "../ui/drawer.vue";
|
||||
import BaseDrawer from "@/components/ui/drawer.vue";
|
||||
import { Check } from "lucide-vue-next";
|
||||
import { usePhotoDragSort } from "@/composables/use-photo-drag-sort";
|
||||
|
||||
interface MediaItem {
|
||||
id: number;
|
||||
|
|
@ -38,7 +39,6 @@ const tempSize = ref(props.initialSize);
|
|||
const tempColumnCount = ref(props.initialColumnCount);
|
||||
const tempShowPrice = ref(props.initialShowPrice);
|
||||
const tempColumns = ref<MediaItem[][]>([]);
|
||||
const draggingPhotoId = ref<number | null>(null);
|
||||
|
||||
watch(
|
||||
() => props.initialSize,
|
||||
|
|
@ -109,49 +109,24 @@ const emitColumns = (nextColumns: MediaItem[][]) => {
|
|||
emit("update:columns", cloneColumns(nextColumns));
|
||||
};
|
||||
|
||||
const handleDragStart = (photoId: number, event: DragEvent) => {
|
||||
draggingPhotoId.value = photoId;
|
||||
event.dataTransfer?.setData("text/plain", String(photoId));
|
||||
event.dataTransfer?.setDragImage?.((event.target as HTMLElement) ?? new Image(), 10, 10);
|
||||
};
|
||||
const {
|
||||
draggingPhotoId,
|
||||
handleDragStart,
|
||||
handleDragEnd,
|
||||
handleTouchStart,
|
||||
handleTouchMove,
|
||||
handleTouchEnd,
|
||||
movePhotoToColumn,
|
||||
movePhotoBeforeTarget,
|
||||
consumeSuppressedClick,
|
||||
} = usePhotoDragSort<MediaItem>({
|
||||
columns: tempColumns,
|
||||
onChange: emitColumns,
|
||||
});
|
||||
|
||||
const handleDragEnd = () => {
|
||||
draggingPhotoId.value = null;
|
||||
};
|
||||
|
||||
const takeDraggedPhoto = (columnsValue: MediaItem[][]) => {
|
||||
let draggedPhoto: MediaItem | undefined;
|
||||
|
||||
columnsValue.forEach((column) => {
|
||||
const index = column.findIndex((photo) => photo.id === draggingPhotoId.value);
|
||||
if (index !== -1) {
|
||||
draggedPhoto = column.splice(index, 1)[0];
|
||||
}
|
||||
});
|
||||
|
||||
return draggedPhoto;
|
||||
};
|
||||
|
||||
const movePhotoToColumn = (targetColumnIndex: number) => {
|
||||
if (draggingPhotoId.value === null) return;
|
||||
const columnsSnapshot = cloneColumns(tempColumns.value);
|
||||
const draggedPhoto = takeDraggedPhoto(columnsSnapshot);
|
||||
if (!draggedPhoto) return;
|
||||
columnsSnapshot[targetColumnIndex]?.push(draggedPhoto);
|
||||
emitColumns(columnsSnapshot);
|
||||
};
|
||||
|
||||
const movePhotoBeforeTarget = (targetColumnIndex: number, targetIndex: number) => {
|
||||
if (draggingPhotoId.value === null) return;
|
||||
const columnsSnapshot = cloneColumns(tempColumns.value);
|
||||
const draggedPhoto = takeDraggedPhoto(columnsSnapshot);
|
||||
if (!draggedPhoto) return;
|
||||
const targetColumn = columnsSnapshot[targetColumnIndex];
|
||||
if (!targetColumn) return;
|
||||
|
||||
const insertIndex = Math.min(Math.max(targetIndex, 0), targetColumn.length);
|
||||
targetColumn.splice(insertIndex, 0, draggedPhoto);
|
||||
emitColumns(columnsSnapshot);
|
||||
const handlePhotoButtonClick = (id: number) => {
|
||||
if (consumeSuppressedClick()) return;
|
||||
togglePhoto(id);
|
||||
};
|
||||
|
||||
const resetPhotoOrder = () => {
|
||||
|
|
@ -250,21 +225,29 @@ const resetPhotoOrder = () => {
|
|||
class="space-y-2"
|
||||
v-for="(column, index) in tempColumns"
|
||||
:key="index"
|
||||
:data-column-index="index"
|
||||
@dragover.prevent
|
||||
@drop.self.prevent="movePhotoToColumn(index)"
|
||||
>
|
||||
<button
|
||||
v-for="(photo, photoIndex) in column"
|
||||
:key="photo.id"
|
||||
@click="togglePhoto(photo.id)"
|
||||
:data-column-index="index"
|
||||
:data-photo-index="photoIndex"
|
||||
@click="handlePhotoButtonClick(photo.id)"
|
||||
draggable="true"
|
||||
@dragstart="handleDragStart(photo.id, $event)"
|
||||
@dragend="handleDragEnd"
|
||||
@dragover.prevent
|
||||
@drop.stop.prevent="movePhotoBeforeTarget(index, photoIndex)"
|
||||
@touchstart="handleTouchStart(photo.id, $event)"
|
||||
@touchmove.prevent="handleTouchMove"
|
||||
@touchend="handleTouchEnd"
|
||||
@touchcancel="handleTouchEnd"
|
||||
class="relative text-xs rounded-xl transition-colors"
|
||||
:class="[
|
||||
'w-full p-1 cursor-move',
|
||||
draggingPhotoId === photo.id ? 'ring-2 ring-yellow-500 ring-offset-1' : '',
|
||||
isPhotoHidden(photo.id) ? 'bg-gray-50 text-gray-400 opacity-60' : 'bg-yellow-300 text-gray-700',
|
||||
]"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,177 @@
|
|||
import { ref, type Ref } from "vue";
|
||||
|
||||
interface SortableItem {
|
||||
id: number;
|
||||
}
|
||||
|
||||
interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface UsePhotoDragSortOptions<T extends SortableItem> {
|
||||
columns: Ref<T[][]>;
|
||||
onChange: (nextColumns: T[][]) => void;
|
||||
}
|
||||
|
||||
const MOVE_START_THRESHOLD = 8;
|
||||
|
||||
export const usePhotoDragSort = <T extends SortableItem>({
|
||||
columns,
|
||||
onChange,
|
||||
}: UsePhotoDragSortOptions<T>) => {
|
||||
const draggingPhotoId = ref<number | null>(null);
|
||||
const dragStartPoint = ref<Point | null>(null);
|
||||
const hasTouchMoved = ref(false);
|
||||
const suppressNextClick = ref(false);
|
||||
const lastTouchTargetKey = ref<string | null>(null);
|
||||
|
||||
const cloneColumns = (columnsValue: T[][]) =>
|
||||
columnsValue.map((column) => column.slice());
|
||||
|
||||
const emitColumns = (nextColumns: T[][]) => {
|
||||
onChange(cloneColumns(nextColumns));
|
||||
};
|
||||
|
||||
const getPhotoPosition = (columnsValue: T[][], photoId: number) => {
|
||||
for (let columnIndex = 0; columnIndex < columnsValue.length; columnIndex += 1) {
|
||||
const photoIndex = columnsValue[columnIndex].findIndex((photo) => photo.id === photoId);
|
||||
if (photoIndex !== -1) {
|
||||
return { columnIndex, photoIndex };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const movePhotoToColumn = (targetColumnIndex: number) => {
|
||||
if (draggingPhotoId.value === null) return;
|
||||
|
||||
const columnsSnapshot = cloneColumns(columns.value);
|
||||
const source = getPhotoPosition(columnsSnapshot, draggingPhotoId.value);
|
||||
const targetColumn = columnsSnapshot[targetColumnIndex];
|
||||
if (!source || !targetColumn) return;
|
||||
|
||||
const [draggedPhoto] = columnsSnapshot[source.columnIndex].splice(source.photoIndex, 1);
|
||||
if (!draggedPhoto) return;
|
||||
|
||||
targetColumn.push(draggedPhoto);
|
||||
emitColumns(columnsSnapshot);
|
||||
};
|
||||
|
||||
const movePhotoBeforeTarget = (targetColumnIndex: number, targetPhotoIndex: number) => {
|
||||
if (draggingPhotoId.value === null) return;
|
||||
|
||||
const columnsSnapshot = cloneColumns(columns.value);
|
||||
const source = getPhotoPosition(columnsSnapshot, draggingPhotoId.value);
|
||||
const targetColumn = columnsSnapshot[targetColumnIndex];
|
||||
if (!source || !targetColumn) return;
|
||||
|
||||
const [draggedPhoto] = columnsSnapshot[source.columnIndex].splice(source.photoIndex, 1);
|
||||
if (!draggedPhoto) return;
|
||||
|
||||
let insertIndex = Math.min(Math.max(targetPhotoIndex, 0), targetColumn.length);
|
||||
if (source.columnIndex === targetColumnIndex && source.photoIndex < targetPhotoIndex) {
|
||||
insertIndex -= 1;
|
||||
}
|
||||
|
||||
targetColumn.splice(insertIndex, 0, draggedPhoto);
|
||||
emitColumns(columnsSnapshot);
|
||||
};
|
||||
|
||||
const resetTouchDragState = () => {
|
||||
dragStartPoint.value = null;
|
||||
hasTouchMoved.value = false;
|
||||
lastTouchTargetKey.value = null;
|
||||
};
|
||||
|
||||
const handleDragStart = (photoId: number, event: DragEvent) => {
|
||||
draggingPhotoId.value = photoId;
|
||||
event.dataTransfer?.setData("text/plain", String(photoId));
|
||||
event.dataTransfer?.setDragImage?.((event.target as HTMLElement) ?? new Image(), 10, 10);
|
||||
};
|
||||
|
||||
const handleDragEnd = () => {
|
||||
draggingPhotoId.value = null;
|
||||
resetTouchDragState();
|
||||
};
|
||||
|
||||
const handleTouchStart = (photoId: number, event: TouchEvent) => {
|
||||
const touch = event.touches[0];
|
||||
if (!touch) return;
|
||||
|
||||
draggingPhotoId.value = photoId;
|
||||
dragStartPoint.value = { x: touch.clientX, y: touch.clientY };
|
||||
hasTouchMoved.value = false;
|
||||
lastTouchTargetKey.value = null;
|
||||
};
|
||||
|
||||
const handleTouchMove = (event: TouchEvent) => {
|
||||
if (draggingPhotoId.value === null) return;
|
||||
|
||||
const touch = event.touches[0];
|
||||
if (!touch) return;
|
||||
|
||||
if (!hasTouchMoved.value && dragStartPoint.value) {
|
||||
const deltaX = touch.clientX - dragStartPoint.value.x;
|
||||
const deltaY = touch.clientY - dragStartPoint.value.y;
|
||||
const distance = Math.sqrt(deltaX ** 2 + deltaY ** 2);
|
||||
if (distance < MOVE_START_THRESHOLD) return;
|
||||
}
|
||||
|
||||
hasTouchMoved.value = true;
|
||||
event.preventDefault();
|
||||
|
||||
const target = document.elementFromPoint(touch.clientX, touch.clientY) as HTMLElement | null;
|
||||
if (!target) return;
|
||||
|
||||
const photoElement = target.closest<HTMLElement>("[data-photo-index][data-column-index]");
|
||||
if (photoElement) {
|
||||
const targetColumn = Number(photoElement.dataset.columnIndex);
|
||||
const targetPhoto = Number(photoElement.dataset.photoIndex);
|
||||
if (Number.isNaN(targetColumn) || Number.isNaN(targetPhoto)) return;
|
||||
|
||||
const key = `photo:${targetColumn}:${targetPhoto}`;
|
||||
if (lastTouchTargetKey.value === key) return;
|
||||
lastTouchTargetKey.value = key;
|
||||
movePhotoBeforeTarget(targetColumn, targetPhoto);
|
||||
return;
|
||||
}
|
||||
|
||||
const columnElement = target.closest<HTMLElement>("[data-column-index]");
|
||||
if (columnElement) {
|
||||
const targetColumn = Number(columnElement.dataset.columnIndex);
|
||||
if (Number.isNaN(targetColumn)) return;
|
||||
|
||||
const key = `column:${targetColumn}`;
|
||||
if (lastTouchTargetKey.value === key) return;
|
||||
lastTouchTargetKey.value = key;
|
||||
movePhotoToColumn(targetColumn);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
if (hasTouchMoved.value) {
|
||||
suppressNextClick.value = true;
|
||||
}
|
||||
draggingPhotoId.value = null;
|
||||
resetTouchDragState();
|
||||
};
|
||||
|
||||
const consumeSuppressedClick = () => {
|
||||
if (!suppressNextClick.value) return false;
|
||||
suppressNextClick.value = false;
|
||||
return true;
|
||||
};
|
||||
|
||||
return {
|
||||
draggingPhotoId,
|
||||
handleDragStart,
|
||||
handleDragEnd,
|
||||
handleTouchStart,
|
||||
handleTouchMove,
|
||||
handleTouchEnd,
|
||||
movePhotoToColumn,
|
||||
movePhotoBeforeTarget,
|
||||
consumeSuppressedClick,
|
||||
};
|
||||
};
|
||||
Loading…
Reference in New Issue