Feat: 拖拽功能增加移动端支持

This commit is contained in:
2026-03-21 02:56:39 +08:00
parent 1e17531e6a
commit f38493be2c
2 changed files with 205 additions and 45 deletions
+177
View File
@@ -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,
};
};