Feat: 拖拽功能增加移动端支持
This commit is contained in:
parent
1e17531e6a
commit
f38493be2c
|
|
@ -1,7 +1,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from "vue";
|
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 { Check } from "lucide-vue-next";
|
||||||
|
import { usePhotoDragSort } from "@/composables/use-photo-drag-sort";
|
||||||
|
|
||||||
interface MediaItem {
|
interface MediaItem {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -38,7 +39,6 @@ const tempSize = ref(props.initialSize);
|
||||||
const tempColumnCount = ref(props.initialColumnCount);
|
const tempColumnCount = ref(props.initialColumnCount);
|
||||||
const tempShowPrice = ref(props.initialShowPrice);
|
const tempShowPrice = ref(props.initialShowPrice);
|
||||||
const tempColumns = ref<MediaItem[][]>([]);
|
const tempColumns = ref<MediaItem[][]>([]);
|
||||||
const draggingPhotoId = ref<number | null>(null);
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.initialSize,
|
() => props.initialSize,
|
||||||
|
|
@ -109,49 +109,24 @@ const emitColumns = (nextColumns: MediaItem[][]) => {
|
||||||
emit("update:columns", cloneColumns(nextColumns));
|
emit("update:columns", cloneColumns(nextColumns));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragStart = (photoId: number, event: DragEvent) => {
|
const {
|
||||||
draggingPhotoId.value = photoId;
|
draggingPhotoId,
|
||||||
event.dataTransfer?.setData("text/plain", String(photoId));
|
handleDragStart,
|
||||||
event.dataTransfer?.setDragImage?.((event.target as HTMLElement) ?? new Image(), 10, 10);
|
handleDragEnd,
|
||||||
};
|
handleTouchStart,
|
||||||
|
handleTouchMove,
|
||||||
|
handleTouchEnd,
|
||||||
|
movePhotoToColumn,
|
||||||
|
movePhotoBeforeTarget,
|
||||||
|
consumeSuppressedClick,
|
||||||
|
} = usePhotoDragSort<MediaItem>({
|
||||||
|
columns: tempColumns,
|
||||||
|
onChange: emitColumns,
|
||||||
|
});
|
||||||
|
|
||||||
const handleDragEnd = () => {
|
const handlePhotoButtonClick = (id: number) => {
|
||||||
draggingPhotoId.value = null;
|
if (consumeSuppressedClick()) return;
|
||||||
};
|
togglePhoto(id);
|
||||||
|
|
||||||
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 resetPhotoOrder = () => {
|
const resetPhotoOrder = () => {
|
||||||
|
|
@ -250,21 +225,29 @@ const resetPhotoOrder = () => {
|
||||||
class="space-y-2"
|
class="space-y-2"
|
||||||
v-for="(column, index) in tempColumns"
|
v-for="(column, index) in tempColumns"
|
||||||
:key="index"
|
:key="index"
|
||||||
|
:data-column-index="index"
|
||||||
@dragover.prevent
|
@dragover.prevent
|
||||||
@drop.self.prevent="movePhotoToColumn(index)"
|
@drop.self.prevent="movePhotoToColumn(index)"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-for="(photo, photoIndex) in column"
|
v-for="(photo, photoIndex) in column"
|
||||||
:key="photo.id"
|
:key="photo.id"
|
||||||
@click="togglePhoto(photo.id)"
|
:data-column-index="index"
|
||||||
|
:data-photo-index="photoIndex"
|
||||||
|
@click="handlePhotoButtonClick(photo.id)"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
@dragstart="handleDragStart(photo.id, $event)"
|
@dragstart="handleDragStart(photo.id, $event)"
|
||||||
@dragend="handleDragEnd"
|
@dragend="handleDragEnd"
|
||||||
@dragover.prevent
|
@dragover.prevent
|
||||||
@drop.stop.prevent="movePhotoBeforeTarget(index, photoIndex)"
|
@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="relative text-xs rounded-xl transition-colors"
|
||||||
:class="[
|
:class="[
|
||||||
'w-full p-1 cursor-move',
|
'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',
|
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