673 lines
17 KiB
Plaintext
673 lines
17 KiB
Plaintext
|
|
<template>
|
|||
|
|
<view
|
|||
|
|
class="cl-draggable"
|
|||
|
|
:class="[
|
|||
|
|
{
|
|||
|
|
'cl-draggable--grid': props.columns > 1
|
|||
|
|
},
|
|||
|
|
pt.className
|
|||
|
|
]"
|
|||
|
|
>
|
|||
|
|
<view
|
|||
|
|
v-for="(item, index) in list"
|
|||
|
|
:key="getItemKey(item, index)"
|
|||
|
|
class="cl-draggable__item"
|
|||
|
|
:class="[
|
|||
|
|
{
|
|||
|
|
'cl-draggable__item--disabled': disabled
|
|||
|
|
},
|
|||
|
|
dragging && dragIndex == index ? `opacity-80 ${pt.ghost?.className}` : ''
|
|||
|
|
]"
|
|||
|
|
:style="getItemStyle(index)"
|
|||
|
|
@touchstart="
|
|||
|
|
(event: UniTouchEvent) => {
|
|||
|
|
onTouchStart(event, index);
|
|||
|
|
}
|
|||
|
|
"
|
|||
|
|
@touchmove.stop.prevent="onTouchMove"
|
|||
|
|
@touchend="onTouchEnd"
|
|||
|
|
>
|
|||
|
|
<slot
|
|||
|
|
name="item"
|
|||
|
|
:item="item"
|
|||
|
|
:index="index"
|
|||
|
|
:dragging="dragging"
|
|||
|
|
:dragIndex="dragIndex"
|
|||
|
|
:insertIndex="insertIndex"
|
|||
|
|
>
|
|||
|
|
</slot>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script lang="ts" setup>
|
|||
|
|
import { computed, ref, getCurrentInstance, type PropType, watch } from "vue";
|
|||
|
|
import { isNull, parsePt, uuid } from "@/cool";
|
|||
|
|
import type { PassThroughProps } from "../../types";
|
|||
|
|
|
|||
|
|
defineOptions({
|
|||
|
|
name: "cl-draggable"
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
defineSlots<{
|
|||
|
|
item(props: {
|
|||
|
|
item: UTSJSONObject;
|
|||
|
|
index: number;
|
|||
|
|
dragging: boolean;
|
|||
|
|
dragIndex: number;
|
|||
|
|
insertIndex: number;
|
|||
|
|
}): any;
|
|||
|
|
}>();
|
|||
|
|
|
|||
|
|
// 项目位置信息类型定义
|
|||
|
|
type ItemPosition = {
|
|||
|
|
top: number;
|
|||
|
|
left: number;
|
|||
|
|
width: number;
|
|||
|
|
height: number;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 位移偏移量类型定义
|
|||
|
|
type TranslateOffset = {
|
|||
|
|
x: number;
|
|||
|
|
y: number;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const props = defineProps({
|
|||
|
|
/** PassThrough 样式配置 */
|
|||
|
|
pt: {
|
|||
|
|
type: Object,
|
|||
|
|
default: () => ({})
|
|||
|
|
},
|
|||
|
|
/** 数据数组,支持双向绑定 */
|
|||
|
|
modelValue: {
|
|||
|
|
type: Array as PropType<UTSJSONObject[]>,
|
|||
|
|
default: () => []
|
|||
|
|
},
|
|||
|
|
/** 是否禁用拖拽功能 */
|
|||
|
|
disabled: {
|
|||
|
|
type: Boolean,
|
|||
|
|
default: false
|
|||
|
|
},
|
|||
|
|
/** 动画持续时间(毫秒) */
|
|||
|
|
animation: {
|
|||
|
|
type: Number,
|
|||
|
|
default: 150
|
|||
|
|
},
|
|||
|
|
/** 列数:1为单列纵向布局,>1为多列网格布局 */
|
|||
|
|
columns: {
|
|||
|
|
type: Number,
|
|||
|
|
default: 1
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const emit = defineEmits(["update:modelValue", "change", "start", "end"]);
|
|||
|
|
|
|||
|
|
const { proxy } = getCurrentInstance()!;
|
|||
|
|
|
|||
|
|
// 透传样式类型定义
|
|||
|
|
type PassThrough = {
|
|||
|
|
className?: string;
|
|||
|
|
ghost?: PassThroughProps;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/** PassThrough 样式解析 */
|
|||
|
|
const pt = computed(() => parsePt<PassThrough>(props.pt));
|
|||
|
|
|
|||
|
|
/** 数据列表 */
|
|||
|
|
const list = ref<UTSJSONObject[]>([]);
|
|||
|
|
|
|||
|
|
/** 是否正在拖拽 */
|
|||
|
|
const dragging = ref(false);
|
|||
|
|
/** 当前拖拽元素的原始索引 */
|
|||
|
|
const dragIndex = ref(-1);
|
|||
|
|
/** 预期插入的目标索引 */
|
|||
|
|
const insertIndex = ref(-1);
|
|||
|
|
/** 触摸开始时的Y坐标 */
|
|||
|
|
const startY = ref(0);
|
|||
|
|
/** 触摸开始时的X坐标 */
|
|||
|
|
const startX = ref(0);
|
|||
|
|
/** Y轴偏移量 */
|
|||
|
|
const offsetY = ref(0);
|
|||
|
|
/** X轴偏移量 */
|
|||
|
|
const offsetX = ref(0);
|
|||
|
|
/** 当前拖拽的数据项 */
|
|||
|
|
const dragItem = ref<UTSJSONObject>({});
|
|||
|
|
/** 所有项目的位置信息缓存 */
|
|||
|
|
const itemPositions = ref<ItemPosition[]>([]);
|
|||
|
|
/** 是否处于放下动画状态 */
|
|||
|
|
const dropping = ref(false);
|
|||
|
|
/** 动态计算的项目高度 */
|
|||
|
|
const itemHeight = ref(0);
|
|||
|
|
/** 动态计算的项目宽度 */
|
|||
|
|
const itemWidth = ref(0);
|
|||
|
|
/** 是否已开始排序模拟(防止误触) */
|
|||
|
|
const sortingStarted = ref(false);
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 重置所有拖拽相关的状态
|
|||
|
|
* 在拖拽结束后调用,确保组件回到初始状态
|
|||
|
|
*/
|
|||
|
|
function reset() {
|
|||
|
|
dragging.value = false; // 拖拽状态
|
|||
|
|
dropping.value = false; // 放下动画状态
|
|||
|
|
dragIndex.value = -1; // 拖拽元素索引
|
|||
|
|
insertIndex.value = -1; // 插入位置索引
|
|||
|
|
offsetX.value = 0; // X轴偏移
|
|||
|
|
offsetY.value = 0; // Y轴偏移
|
|||
|
|
dragItem.value = {}; // 拖拽的数据项
|
|||
|
|
itemPositions.value = []; // 位置信息缓存
|
|||
|
|
itemHeight.value = 0; // 动态计算的高度
|
|||
|
|
itemWidth.value = 0; // 动态计算的宽度
|
|||
|
|
sortingStarted.value = false; // 排序模拟状态
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 计算网格布局中元素的位移偏移
|
|||
|
|
* @param index 当前元素索引
|
|||
|
|
* @param dragIdx 拖拽元素索引
|
|||
|
|
* @param insertIdx 插入位置索引
|
|||
|
|
* @returns 包含 x 和 y 坐标偏移的对象
|
|||
|
|
*/
|
|||
|
|
function calculateGridOffset(index: number, dragIdx: number, insertIdx: number): TranslateOffset {
|
|||
|
|
const cols = props.columns;
|
|||
|
|
|
|||
|
|
// 计算当前元素在网格中的行列位置
|
|||
|
|
const currentRow = Math.floor(index / cols);
|
|||
|
|
const currentCol = index % cols;
|
|||
|
|
|
|||
|
|
// 计算元素在拖拽后的新位置索引
|
|||
|
|
let newIndex = index;
|
|||
|
|
|
|||
|
|
if (dragIdx < insertIdx) {
|
|||
|
|
// 向后拖拽:dragIdx+1 到 insertIdx 之间的元素需要向前移动一位
|
|||
|
|
if (index > dragIdx && index <= insertIdx) {
|
|||
|
|
newIndex = index - 1;
|
|||
|
|
}
|
|||
|
|
} else if (dragIdx > insertIdx) {
|
|||
|
|
// 向前拖拽:insertIdx 到 dragIdx-1 之间的元素需要向后移动一位
|
|||
|
|
if (index >= insertIdx && index < dragIdx) {
|
|||
|
|
newIndex = index + 1;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 计算新位置的行列坐标
|
|||
|
|
const newRow = Math.floor(newIndex / cols);
|
|||
|
|
const newCol = newIndex % cols;
|
|||
|
|
|
|||
|
|
// 使用动态计算的网格尺寸
|
|||
|
|
const cellWidth = itemWidth.value;
|
|||
|
|
const cellHeight = itemHeight.value;
|
|||
|
|
|
|||
|
|
// 计算实际的像素位移
|
|||
|
|
const offsetX = (newCol - currentCol) * cellWidth;
|
|||
|
|
const offsetY = (newRow - currentRow) * cellHeight;
|
|||
|
|
|
|||
|
|
return { x: offsetX, y: offsetY };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 计算网格布局的插入位置
|
|||
|
|
* @param dragCenterX 拖拽元素中心点X坐标
|
|||
|
|
* @param dragCenterY 拖拽元素中心点Y坐标
|
|||
|
|
* @returns 最佳插入位置索引
|
|||
|
|
*/
|
|||
|
|
function calculateGridInsertIndex(dragCenterX: number, dragCenterY: number): number {
|
|||
|
|
if (itemPositions.value.length == 0) {
|
|||
|
|
return dragIndex.value;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let closestIndex = dragIndex.value;
|
|||
|
|
let minDistance = Infinity;
|
|||
|
|
|
|||
|
|
// 使用欧几里得距离找到最近的网格位置(包括原位置)
|
|||
|
|
for (let i = 0; i < itemPositions.value.length; i++) {
|
|||
|
|
const position = itemPositions.value[i];
|
|||
|
|
|
|||
|
|
// 计算到元素中心点的距离
|
|||
|
|
const centerX = position.left + position.width / 2;
|
|||
|
|
const centerY = position.top + position.height / 2;
|
|||
|
|
|
|||
|
|
// 使用欧几里得距离公式
|
|||
|
|
const distance = Math.sqrt(
|
|||
|
|
Math.pow(dragCenterX - centerX, 2) + Math.pow(dragCenterY - centerY, 2)
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 更新最近的位置
|
|||
|
|
if (distance < minDistance) {
|
|||
|
|
minDistance = distance;
|
|||
|
|
closestIndex = i;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return closestIndex;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 计算单列布局的插入位置
|
|||
|
|
* @param clientY Y坐标
|
|||
|
|
* @returns 最佳插入位置索引
|
|||
|
|
*/
|
|||
|
|
function calculateSingleColumnInsertIndex(clientY: number): number {
|
|||
|
|
let closestIndex = dragIndex.value;
|
|||
|
|
let minDistance = Infinity;
|
|||
|
|
|
|||
|
|
// 遍历所有元素,找到距离最近的元素中心
|
|||
|
|
for (let i = 0; i < itemPositions.value.length; i++) {
|
|||
|
|
const position = itemPositions.value[i];
|
|||
|
|
|
|||
|
|
// 计算到元素中心点的距离
|
|||
|
|
const itemCenter = position.top + position.height / 2;
|
|||
|
|
const distance = Math.abs(clientY - itemCenter);
|
|||
|
|
|
|||
|
|
if (distance < minDistance) {
|
|||
|
|
minDistance = distance;
|
|||
|
|
closestIndex = i;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return closestIndex;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 计算拖拽元素的最佳插入位置
|
|||
|
|
* @param clientPosition 在主轴上的坐标(仅用于单列布局的Y轴坐标)
|
|||
|
|
* @returns 最佳插入位置的索引
|
|||
|
|
*/
|
|||
|
|
function calculateInsertIndex(clientPosition: number): number {
|
|||
|
|
// 如果没有位置信息,保持原位置
|
|||
|
|
if (itemPositions.value.length == 0) {
|
|||
|
|
return dragIndex.value;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 根据布局类型选择计算方式
|
|||
|
|
if (props.columns > 1) {
|
|||
|
|
// 多列网格布局:计算拖拽元素的中心点坐标,使用2D坐标计算最近位置
|
|||
|
|
const dragPos = itemPositions.value[dragIndex.value];
|
|||
|
|
const dragCenterX = dragPos.left + dragPos.width / 2 + offsetX.value;
|
|||
|
|
const dragCenterY = dragPos.top + dragPos.height / 2 + offsetY.value;
|
|||
|
|
return calculateGridInsertIndex(dragCenterX, dragCenterY);
|
|||
|
|
} else {
|
|||
|
|
// 单列布局:基于Y轴距离计算最近的元素中心
|
|||
|
|
return calculateSingleColumnInsertIndex(clientPosition);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 计算单列布局的位移偏移
|
|||
|
|
* @param index 元素索引
|
|||
|
|
* @param dragIdx 拖拽元素索引
|
|||
|
|
* @param insertIdx 插入位置索引
|
|||
|
|
* @returns 位移偏移对象
|
|||
|
|
*/
|
|||
|
|
function calculateSingleColumnOffset(
|
|||
|
|
index: number,
|
|||
|
|
dragIdx: number,
|
|||
|
|
insertIdx: number
|
|||
|
|
): TranslateOffset {
|
|||
|
|
if (dragIdx < insertIdx) {
|
|||
|
|
// 向下拖拽:dragIdx+1 到 insertIdx 之间的元素向上移动
|
|||
|
|
if (index > dragIdx && index <= insertIdx) {
|
|||
|
|
return { x: 0, y: -itemHeight.value };
|
|||
|
|
}
|
|||
|
|
} else if (dragIdx > insertIdx) {
|
|||
|
|
// 向上拖拽:insertIdx 到 dragIdx-1 之间的元素向下移动
|
|||
|
|
if (index >= insertIdx && index < dragIdx) {
|
|||
|
|
return { x: 0, y: itemHeight.value };
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return { x: 0, y: 0 };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 计算非拖拽元素的位移偏移量
|
|||
|
|
* @param index 元素索引
|
|||
|
|
* @returns 包含 x 和 y 坐标偏移的对象
|
|||
|
|
*/
|
|||
|
|
function getItemTranslateOffset(index: number): TranslateOffset {
|
|||
|
|
// 只在满足所有条件时才计算位移:拖拽中、非放下状态、已开始排序
|
|||
|
|
if (!dragging.value || dropping.value || !sortingStarted.value) {
|
|||
|
|
return { x: 0, y: 0 };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const dragIdx = dragIndex.value;
|
|||
|
|
const insertIdx = insertIndex.value;
|
|||
|
|
|
|||
|
|
// 跳过正在拖拽的元素(拖拽元素由位置控制)
|
|||
|
|
if (index == dragIdx) {
|
|||
|
|
return { x: 0, y: 0 };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 没有位置变化时不需要位移(拖回原位置)
|
|||
|
|
if (dragIdx == insertIdx) {
|
|||
|
|
return { x: 0, y: 0 };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 根据布局类型计算位移
|
|||
|
|
if (props.columns > 1) {
|
|||
|
|
// 多列网格布局:使用2D位移计算
|
|||
|
|
return calculateGridOffset(index, dragIdx, insertIdx);
|
|||
|
|
} else {
|
|||
|
|
// 单列布局:使用简单的纵向位移
|
|||
|
|
return calculateSingleColumnOffset(index, dragIdx, insertIdx);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 计算项目的完整样式对象
|
|||
|
|
* @param index 项目索引
|
|||
|
|
* @returns 样式对象
|
|||
|
|
*/
|
|||
|
|
function getItemStyle(index: number) {
|
|||
|
|
const style = {};
|
|||
|
|
const isCurrent = dragIndex.value == index;
|
|||
|
|
|
|||
|
|
// 多列布局时设置等宽分布
|
|||
|
|
if (props.columns > 1) {
|
|||
|
|
const widthPercent = 100 / props.columns;
|
|||
|
|
style["flex-basis"] = `${widthPercent}%`;
|
|||
|
|
style["width"] = `${widthPercent}%`;
|
|||
|
|
style["box-sizing"] = "border-box";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 放下动画期间,只保留基础样式
|
|||
|
|
if (dropping.value) {
|
|||
|
|
return style;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 为非拖拽元素添加过渡动画
|
|||
|
|
if (props.animation > 0 && !isCurrent) {
|
|||
|
|
style["transition-property"] = "transform";
|
|||
|
|
style["transition-duration"] = `${props.animation}ms`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 拖拽状态下的样式处理
|
|||
|
|
if (dragging.value) {
|
|||
|
|
if (isCurrent) {
|
|||
|
|
// 拖拽元素:跟随移动
|
|||
|
|
style["transform"] = `translate(${offsetX.value}px, ${offsetY.value}px)`;
|
|||
|
|
style["z-index"] = "100";
|
|||
|
|
} else {
|
|||
|
|
// 其他元素:显示排序预览位移
|
|||
|
|
const translateOffset = getItemTranslateOffset(index);
|
|||
|
|
if (translateOffset.x != 0 || translateOffset.y != 0) {
|
|||
|
|
style["transform"] = `translate(${translateOffset.x}px, ${translateOffset.y}px)`;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return style;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取所有项目的位置信息
|
|||
|
|
*/
|
|||
|
|
async function getItemPosition(): Promise<void> {
|
|||
|
|
return new Promise((resolve) => {
|
|||
|
|
uni.createSelectorQuery()
|
|||
|
|
.in(proxy)
|
|||
|
|
.select(".cl-draggable")
|
|||
|
|
.boundingClientRect()
|
|||
|
|
.exec((res) => {
|
|||
|
|
const box = res[0] as NodeInfo;
|
|||
|
|
|
|||
|
|
itemWidth.value = (box.width ?? 0) / props.columns;
|
|||
|
|
|
|||
|
|
uni.createSelectorQuery()
|
|||
|
|
.in(proxy)
|
|||
|
|
.selectAll(".cl-draggable__item")
|
|||
|
|
.boundingClientRect()
|
|||
|
|
.exec((res) => {
|
|||
|
|
const rects = res[0] as NodeInfo[];
|
|||
|
|
const positions: ItemPosition[] = [];
|
|||
|
|
|
|||
|
|
for (let i = 0; i < rects.length; i++) {
|
|||
|
|
const rect = rects[i];
|
|||
|
|
|
|||
|
|
if (i == 0) {
|
|||
|
|
itemHeight.value = rect.height ?? 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
positions.push({
|
|||
|
|
top: rect.top ?? 0,
|
|||
|
|
left: rect.left ?? 0,
|
|||
|
|
width: itemWidth.value,
|
|||
|
|
height: itemHeight.value
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
itemPositions.value = positions;
|
|||
|
|
|
|||
|
|
resolve();
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取项目是否禁用
|
|||
|
|
* @param index 项目索引
|
|||
|
|
* @returns 是否禁用
|
|||
|
|
*/
|
|||
|
|
function getItemDisabled(index: number): boolean {
|
|||
|
|
return !isNull(list.value[index]["disabled"]) && (list.value[index]["disabled"] as boolean);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 检查拖拽元素的中心点是否移动到其他元素区域
|
|||
|
|
*/
|
|||
|
|
function checkMovedToOtherElement(): boolean {
|
|||
|
|
// 如果没有位置信息,默认未移出
|
|||
|
|
if (itemPositions.value.length == 0) return false;
|
|||
|
|
|
|||
|
|
const dragIdx = dragIndex.value;
|
|||
|
|
const dragPosition = itemPositions.value[dragIdx];
|
|||
|
|
|
|||
|
|
// 计算拖拽元素当前的中心点位置(考虑拖拽偏移)
|
|||
|
|
const dragCenterX = dragPosition.left + dragPosition.width / 2 + offsetX.value;
|
|||
|
|
const dragCenterY = dragPosition.top + dragPosition.height / 2 + offsetY.value;
|
|||
|
|
|
|||
|
|
// 根据布局类型采用不同的判断策略
|
|||
|
|
if (props.columns > 1) {
|
|||
|
|
// 多列网格布局:检查中心点是否与其他元素区域重叠
|
|||
|
|
for (let i = 0; i < itemPositions.value.length; i++) {
|
|||
|
|
if (i == dragIdx) continue;
|
|||
|
|
|
|||
|
|
const otherPosition = itemPositions.value[i];
|
|||
|
|
const isOverlapping =
|
|||
|
|
dragCenterX >= otherPosition.left &&
|
|||
|
|
dragCenterX <= otherPosition.left + otherPosition.width &&
|
|||
|
|
dragCenterY >= otherPosition.top &&
|
|||
|
|
dragCenterY <= otherPosition.top + otherPosition.height;
|
|||
|
|
|
|||
|
|
if (isOverlapping) {
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// 检查是否向上移动超过上一个元素的中线
|
|||
|
|
if (dragIdx > 0) {
|
|||
|
|
const prevPosition = itemPositions.value[dragIdx - 1];
|
|||
|
|
const prevCenterY = prevPosition.top + prevPosition.height / 2;
|
|||
|
|
if (dragCenterY <= prevCenterY) {
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查是否向下移动超过下一个元素的中线
|
|||
|
|
if (dragIdx < itemPositions.value.length - 1) {
|
|||
|
|
const nextPosition = itemPositions.value[dragIdx + 1];
|
|||
|
|
const nextCenterY = nextPosition.top + nextPosition.height / 2;
|
|||
|
|
if (dragCenterY >= nextCenterY) {
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 触摸开始事件处理
|
|||
|
|
* @param event 触摸事件对象
|
|||
|
|
* @param index 触摸的项目索引
|
|||
|
|
*/
|
|||
|
|
async function onTouchStart(event: UniTouchEvent, index: number): Promise<void> {
|
|||
|
|
// 检查是否禁用或索引无效
|
|||
|
|
if (props.disabled) return;
|
|||
|
|
if (getItemDisabled(index)) return;
|
|||
|
|
if (index < 0 || index >= list.value.length) return;
|
|||
|
|
|
|||
|
|
const touch = event.touches[0];
|
|||
|
|
|
|||
|
|
// 初始化拖拽状态
|
|||
|
|
dragging.value = true;
|
|||
|
|
dragIndex.value = index;
|
|||
|
|
insertIndex.value = index; // 初始插入位置为原位置
|
|||
|
|
startX.value = touch.clientX;
|
|||
|
|
startY.value = touch.clientY;
|
|||
|
|
offsetX.value = 0;
|
|||
|
|
offsetY.value = 0;
|
|||
|
|
dragItem.value = list.value[index];
|
|||
|
|
|
|||
|
|
// 先获取所有项目的位置信息,为后续计算做准备
|
|||
|
|
await getItemPosition();
|
|||
|
|
|
|||
|
|
// 触发开始事件
|
|||
|
|
emit("start", index);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 触摸移动事件处理
|
|||
|
|
* @param event 触摸事件对象
|
|||
|
|
*/
|
|||
|
|
function onTouchMove(event: TouchEvent): void {
|
|||
|
|
if (!dragging.value) return;
|
|||
|
|
|
|||
|
|
const touch = event.touches[0];
|
|||
|
|
|
|||
|
|
// 更新拖拽偏移量
|
|||
|
|
offsetX.value = touch.clientX - startX.value;
|
|||
|
|
offsetY.value = touch.clientY - startY.value;
|
|||
|
|
|
|||
|
|
// 智能启动排序模拟:只有移出原元素区域才开始
|
|||
|
|
if (!sortingStarted.value) {
|
|||
|
|
if (checkMovedToOtherElement()) {
|
|||
|
|
sortingStarted.value = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 只有开始排序模拟后才计算插入位置
|
|||
|
|
if (sortingStarted.value) {
|
|||
|
|
// 计算拖拽元素当前的中心点坐标
|
|||
|
|
const dragPos = itemPositions.value[dragIndex.value];
|
|||
|
|
const dragCenterX = dragPos.left + dragPos.width / 2 + offsetX.value;
|
|||
|
|
const dragCenterY = dragPos.top + dragPos.height / 2 + offsetY.value;
|
|||
|
|
|
|||
|
|
// 根据布局类型选择坐标轴:网格布局使用X坐标,单列布局使用Y坐标
|
|||
|
|
const dragCenter = props.columns > 1 ? dragCenterX : dragCenterY;
|
|||
|
|
|
|||
|
|
// 计算最佳插入位置
|
|||
|
|
const newIndex = calculateInsertIndex(dragCenter);
|
|||
|
|
if (newIndex != insertIndex.value) {
|
|||
|
|
insertIndex.value = newIndex;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 阻止默认行为和事件冒泡
|
|||
|
|
event.preventDefault();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 触摸结束事件处理
|
|||
|
|
*/
|
|||
|
|
function onTouchEnd(): void {
|
|||
|
|
if (!dragging.value) return;
|
|||
|
|
|
|||
|
|
const oldIndex = dragIndex.value;
|
|||
|
|
const newIndex = insertIndex.value;
|
|||
|
|
|
|||
|
|
// 如果位置发生变化,立即更新数组
|
|||
|
|
if (oldIndex != newIndex && newIndex >= 0) {
|
|||
|
|
const newList = [...list.value];
|
|||
|
|
const item = newList.splice(oldIndex, 1)[0];
|
|||
|
|
newList.splice(newIndex, 0, item);
|
|||
|
|
list.value = newList;
|
|||
|
|
|
|||
|
|
// 触发变化事件
|
|||
|
|
emit("update:modelValue", list.value);
|
|||
|
|
emit("change", list.value);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 开始放下动画
|
|||
|
|
dropping.value = true;
|
|||
|
|
dragging.value = false;
|
|||
|
|
|
|||
|
|
// 让拖拽元素回到自然位置(偏移归零)
|
|||
|
|
offsetX.value = 0;
|
|||
|
|
offsetY.value = 0;
|
|||
|
|
|
|||
|
|
// 等待放下动画完成后重置所有状态
|
|||
|
|
setTimeout(() => {
|
|||
|
|
emit("end", newIndex >= 0 ? newIndex : oldIndex);
|
|||
|
|
reset();
|
|||
|
|
}, 10);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 根据平台选择合适的key
|
|||
|
|
* @param item 数据项
|
|||
|
|
* @param index 索引
|
|||
|
|
* @returns 合适的key
|
|||
|
|
*/
|
|||
|
|
function getItemKey(item: UTSJSONObject, index: number): string {
|
|||
|
|
// #ifdef MP
|
|||
|
|
// 小程序环境使用 index 作为 key,避免数据错乱
|
|||
|
|
return `${index}`;
|
|||
|
|
// #endif
|
|||
|
|
|
|||
|
|
// #ifndef MP
|
|||
|
|
// 其他平台使用 uid,提供更好的性能
|
|||
|
|
return item["uid"] as string;
|
|||
|
|
// #endif
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
watch(
|
|||
|
|
computed(() => props.modelValue),
|
|||
|
|
(val: UTSJSONObject[]) => {
|
|||
|
|
list.value = val.map((e) => {
|
|||
|
|
return {
|
|||
|
|
uid: e["uid"] ?? uuid(),
|
|||
|
|
...e
|
|||
|
|
};
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
immediate: true
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.cl-draggable {
|
|||
|
|
@apply flex-col relative overflow-visible;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.cl-draggable--grid {
|
|||
|
|
@apply flex-row flex-wrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.cl-draggable__item {
|
|||
|
|
@apply relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.cl-draggable__item--dragging {
|
|||
|
|
@apply opacity-80;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.cl-draggable__item--disabled {
|
|||
|
|
@apply opacity-60;
|
|||
|
|
}
|
|||
|
|
</style>
|