<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 版本的修改 但是拖拽到目标 还是有点问题 但没找到问题所在 。。。 凑活能用