uniapp vue3 拖拽排序

187 阅读3分钟
<template>
	<movable-area :style="getAreaStyle">
		<movable-view v-for="(item, index) in list" :animation="props.animation" :direction="props.direction"
			:key="item.key" :damping="props.damping" :x="item.x" :y="item.y"
			:disabled="isItemDisabled(index)" @longpress="handleLongpress"
			@touchstart="handleDragStart(index)" @change="handleMoving" @touchend="handleDragEnd" 
			:style="getViewStyle"
			class="base-drag-wrapper" :class="{ active: activeIndex === index, 'disabled-item': isFixedItem(index) }">

			<slot name="item" :element="item" :index="index"></slot>
			
		</movable-view>
	</movable-area>
</template>

<script setup>
import { ref, reactive, computed, watch } from 'vue';

const props = defineProps({
	column: {
		type: Number,
		default: 3
	},
	modelValue: {
		type: Array,
		default: () => []
	},
	width: {
		type: String,
		default: '100%'
	},
	height: {
		type: String,
		default: 'auto'
	},
	itemKey: {
		type: String,
		required: true
	},
	itemHeight: {
		type: String,
		default: '100px'
	},
	// 添加间隔属性
	gap: {
		type: Number,
		default: 10
	},
	direction: {
		type: String,
		default: 'all',
		validator: value => {
			return ['all', 'vertical', 'horizontal', 'none'].includes(value);
		}
	},
	animation: {
		type: Boolean,
		default: true
	},
	damping: {
		type: Number,
		default: 20
	},
	longpress: {
		type: Boolean,
		default: true
	},
	// 添加新属性:固定位置的元素索引数组
	fixedItems: {
		type: Array,
		default: () => []
	},
	// 添加新属性:是否允许拖拽
	draggable: {
		type: Boolean,
		default: false
	}
});

const emit = defineEmits(['update:modelValue', 'end']);

const list = ref([]);
const disabled = ref(true);
const activeIndex = ref(-1);
const moveToIndex = ref(-1);
const oldIndex = ref(-1);
const tempDragInfo = reactive({
	x: '',
	y: ''
});
const cloneList = ref([]);

// 判断元素是否为固定元素(不可拖拽)
const isFixedItem = (index) => {
	return props.fixedItems.includes(index);
};

// 判断元素是否禁用拖拽
const isItemDisabled = (index) => {
	// 如果整体不允许拖拽或者是固定元素,则禁用
	if (!props.draggable || isFixedItem(index)) {
		return true;
	}
	return props.longpress ? disabled.value : true;
};

// 计算属性
const getItemHeight = computed(() => {
	return parseFloat(props.itemHeight);
});

const getItemWidth = computed(() => {
	if (props.column === 0) return 0;
	const width = getRealWidth(props.width);
	// 考虑间隔,计算实际宽度
	const totalGapWidth = props.gap * (props.column - 1);
	return ((parseFloat(width) - totalGapWidth) / props.column).toFixed(2);
});

const getAreaStyle = computed(() => {
	const width = getRealWidth(props.width);
	const rows = Math.ceil(list.value.length / props.column);
	// 考虑行间距
	const totalHeight = rows * getItemHeight.value + (rows - 1) * props.gap;
	
	return {
		width: width + 'px',
		height: props.height !== 'auto' ? props.height : totalHeight + 'px'
	};
});

const getViewStyle = computed(() => {
	return {
		width: getItemWidth.value + 'px',
		height: props.itemHeight
	};
});

// 方法
// 获取实际的宽度
const getRealWidth = (width) => {
	if (width.includes('%')) {
		const windowWidth = uni.getSystemInfoSync().windowWidth;
		width = windowWidth * (parseFloat(width) / 100);
	}
	return width;
};

const initList = (valueList = []) => {
	const newList = deepCopy(valueList);
	list.value = newList.map((item, index) => {
		const [x, y] = getPosition(index);
		return {
			...item,
			x,
			y,
			key: Math.random() + index
		};
	});
	cloneList.value = deepCopy(list.value);
};

// 长按
const handleLongpress = () => {
	// 只有在允许拖拽的情况下才能设置disabled为false
	if (props.draggable) {
		disabled.value = false;
	}
};

// 拖拽开始
const handleDragStart = (index) => {
	// 如果不允许拖拽或者是固定元素,不允许拖拽
	if (!props.draggable || isFixedItem(index)) return;
	
	activeIndex.value = index;
	oldIndex.value = index;
};

// 拖拽中
const handleMoving = (e) => {
	if (!props.draggable || e.detail.source !== 'touch') return;
	if (activeIndex.value === -1) return;
	
	const { x, y } = e.detail;
	Object.assign(tempDragInfo, { x, y });
	
	// 优化位置计算逻辑
	const itemWidth = parseFloat(getItemWidth.value);
	const itemWidthWithGap = itemWidth + props.gap;
	const itemHeightWithGap = getItemHeight.value + props.gap;
	
	// 计算当前位置在网格中的坐标
	// 使用中心点位置来判断,更加精确
	const centerX = x + itemWidth / 2;
	const centerY = y + getItemHeight.value / 2;
	
	const currentX = Math.floor(centerX / itemWidthWithGap);
	const currentY = Math.floor(centerY / itemHeightWithGap);
	
	// 确保计算的索引在有效范围内
	const maxColumn = props.column - 1;
	const maxRow = Math.floor((list.value.length - 1) / props.column);
	
	const boundedX = Math.max(0, Math.min(currentX, maxColumn));
	const boundedY = Math.max(0, Math.min(currentY, maxRow));
	
	const newMoveToIndex = boundedY * props.column + boundedX;
	
	// 确保索引不超出数组长度
	if (newMoveToIndex >= list.value.length) return;
	
	// 如果目标位置是固定元素,不允许移动到该位置
	if (isFixedItem(newMoveToIndex)) return;
	
	// 如果目标位置与当前激活元素相同,不需要处理
	if (newMoveToIndex === activeIndex.value) return;
	
	moveToIndex.value = newMoveToIndex;

	if (oldIndex.value !== moveToIndex.value && oldIndex.value !== -1 && moveToIndex.value !== -1) {
		const newList = deepCopy(cloneList.value);
		
		// 保存当前激活元素
		const activeItem = newList[activeIndex.value];
		
		// 从数组中移除激活元素
		newList.splice(activeIndex.value, 1);
		
		// 在目标位置插入激活元素
		newList.splice(moveToIndex.value, 0, activeItem);

		// 更新所有元素的位置
		list.value.forEach((item, index) => {
			if (index !== activeIndex.value) {
				const itemIndex = newList.findIndex(val => val[props.itemKey] === item[props.itemKey]);
				if (itemIndex !== -1) { // 确保找到了对应元素
					[item.x, item.y] = getPosition(itemIndex);
				}
			}
		});
		
		oldIndex.value = moveToIndex.value;
	}
};

// 获取当前的位置
const getPosition = (index) => {
	const columnIndex = index % props.column;
	const rowIndex = Math.floor(index / props.column);
	
	// 计算x坐标,考虑列间距
	const x = columnIndex * (parseFloat(getItemWidth.value) + props.gap);
	
	// 计算y坐标,考虑行间距
	const y = rowIndex * (getItemHeight.value + props.gap);
	
	return [x, y];
};

// 拖拽结束
const handleDragEnd = (e) => {
	if (!props.draggable || disabled.value || activeIndex.value === -1) return;
	
	if (moveToIndex.value !== -1 && activeIndex.value !== -1 && moveToIndex.value !== activeIndex.value) {
		// 确保不会移动到固定元素的位置
		if (!isFixedItem(moveToIndex.value)) {
			// 保存当前激活元素
			const activeItem = cloneList.value[activeIndex.value];
			
			// 从数组中移除激活元素
			cloneList.value.splice(activeIndex.value, 1);
			
			// 在目标位置插入激活元素
			cloneList.value.splice(moveToIndex.value, 0, activeItem);
		}
	} else {
		// 如果没有移动到新位置,恢复原来的位置
		const [x, y] = getPosition(activeIndex.value);
		list.value[activeIndex.value].x = x;
		list.value[activeIndex.value].y = y;
	}
	
	initList(cloneList.value);
	const endList = list.value.map(item => omit(item, ['x', 'y', 'key']));
	emit('update:modelValue', endList);
	emit('end', endList);

	activeIndex.value = -1;
	oldIndex.value = -1;
	moveToIndex.value = -1;
	disabled.value = true;
};

const deepCopy = (source) => {
	return JSON.parse(JSON.stringify(source));
};

/**
 * 排除掉obj里面的key值
 * @param {object} obj
 * @param {Array|string} args
 * @returns {object}
 */
const omit = (obj, args) => {
	if (!args) return obj;
	const newObj = {};
	const isString = typeof args === 'string';
	const keys = Object.keys(obj).filter(item => {
		if (isString) {
			return item !== args;
		}
		return !args.includes(item);
	});

	keys.forEach(key => {
		if (obj[key] !== undefined) newObj[key] = obj[key];
	});
	return newObj;
};

// 监听属性变化
watch(
  () => props.modelValue,
  (newVal) => {
    initList(newVal);
  },
  { immediate: true, deep: true }
);
</script>

<style lang="scss" scoped>
.base-drag-wrapper {
	opacity: 1;
	z-index: 1;
	box-sizing: border-box; /* 确保尺寸计算包含内边距和边框 */

	&.active {
		opacity: 0.7;
		transform: scale(1.3);
		z-index: 99;
	}
	
	&.disabled-item {
		cursor: not-allowed;
	}
}
</style>

import drag from '@/components/drag.vue'

<drag v-model="subscriptionList" :width="'92%'"  :column="3" itemKey="infoTypeName" :fixed-items="noDrag" :draggable="isEdit" :itemHeight="'40px'">
	<template #item="{element,index}">
		<view class="item">
		{{element.infoTypeName}}
		<image v-if="isEdit && element.readonly == 0" class="close" src="/static/img/close.png"  @click.stop="cancelSubscription(element,index)">
                </image>
		</view>
	</template>
</drag>

# 十九.uniapp之实现拖拽排序 基于大佬的文章 做的vue3 版本的修改 但是拖拽到目标 还是有点问题 但没找到问题所在 。。。 凑活能用