在实际业务中,我们经常遇到「标签排序」或「菜单调整」的场景。微信小程序原生的 movable-view
为我们提供了一个简单、高效的拖拽能力,结合 Vue3 + uni-app
的组合,我们可以实现一个体验良好的标签管理界面。
核心组件 :<movable-view>
、<movable-area>
1.每项数据结构加入 y
坐标
javascript
const itemHeight = uni.upx2px(110); // 每项高度,决定拖动步长
function initDragg(list) {
return list.map((item, index) => ({
...item,
y: index * itemHeight, // 初始位置
}));
}
2. 拖动状态管理变量
javascript
const canDrag = ref(false); // 当前是否允许拖动(长按才激活)
const draggingIndex = ref(-1); // 当前拖动的 item 的 index
3.用户手指按下某项
javascript
<image
@touchstart="canDrag = true"
:src="getSpecImgUrl('sort_set')"
/>
4.进入拖动状态
javascript
function onTouchStart(index) {
if (!canDrag.value) return;
draggingIndex.value = index;
}
5.拖动过程中(自动触发)
javascript
function onChange(e, index) {
const dy = e.detail.y;
const targetIndex = Math.round(dy / itemHeight);
if (targetIndex !== index && targetIndex >= 0 && targetIndex < groupList.value.length) {
// 拖动项移到新位置
const moved = groupList.value.splice(index, 1)[0];
groupList.value.splice(targetIndex, 0, moved);
draggingIndex.value = targetIndex;
} else {
// 仅移动视觉,不换位置
groupList.value[index].y = dy;
}
}
6.拖动结束
javascript
function onTouchEnd() {
if (!canDrag.value) return;
draggingIndex.value = -1;
// 所有项重置为标准位置
groupList.value.forEach((item, i) => {
item.y = i * itemHeight;
});
// 同步顺序到后端
sortLabelList(groupList.value);
canDrag.value = false;
}
完整代码测试代码
javascript
<template>
<scroll-view scroll-y style="height: 100vh">
<movable-area style="height: 1000rpx; width: 100vw; position: relative">
<block v-for="(item, index) in list" :key="item.id">
<movable-view
direction="vertical"
damping="50"
inertia
:y="item.y"
:style="getStyle(index)"
@touchstart="onTouchStart(index)"
@touchend="onTouchEnd"
@change="onChange($event, index)"
>
<view class="item">
{{ item.name }}
</view>
</movable-view>
</block>
</movable-area>
</scroll-view>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { debounce } from '@/utils/util';
const itemHeight = 100; // 每项高度,单位 rpx(需配合实际样式调整)
const list = reactive([
{ id: 1, name: '任务一', y: 0 },
{ id: 2, name: '任务二', y: 100 },
{ id: 3, name: '任务三', y: 200 },
{ id: 4, name: '任务四', y: 300 },
]);
let draggingIndex = ref(-1);
function onTouchStart(index) {
draggingIndex.value = index;
}
function onTouchEnd(e, index) {
console.log(444);
draggingIndex.value = -1;
// 重置 Y 防止漂移
list.forEach((item, i) => {
item.y = i * itemHeight;
});
}
// 拖拽过程中计算是否需要交换
function onChange(e, index) {
// const bounce = debounce(foo,500);
// bounce()
const dy = e.detail.y;
const targetIndex = Math.round(dy / itemHeight);
console.log(2222,e,index,targetIndex);
if (
targetIndex !== index &&
targetIndex >= 0 &&
targetIndex < list.length
) {
console.log(3333);
// 交换数据
const moved = list.splice(index, 1)[0];
list.splice(targetIndex, 0, moved);
// 重新设置 y 值
// list.forEach((item, i) => {
// item.y = i * itemHeight;
// });
draggingIndex.value = targetIndex;
} else {
list[index].y = dy;
}
}
function getStyle(index) {
return `position: absolute; left: 0; width: 100%; height: ${itemHeight}rpx; z-index: ${
draggingIndex.value === index ? 10 : 1
};`;
}
</script>
<style scoped>
.item {
background-color: #f1f1f1;
margin: 10rpx;
border-radius: 10rpx;
text-align: center;
line-height: 100rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}
</style>