269 lines
6.5 KiB
Vue
269 lines
6.5 KiB
Vue
<template>
|
||
<ul class="m-drag" :style="{ height: (itemHeight * state.newList.length) + 'px' }">
|
||
<li v-for="(item, index) in state.newList" :key="index" class="m-drag-item"
|
||
:class="{ active: state.currentIndex === index }" :style="{
|
||
top: state.itemYList[index].top + 'px'
|
||
}">
|
||
<slot :item="item" />
|
||
<!-- css实现拖拽图标 -->
|
||
|
||
<div class="icon" @touchstart.stop="touchStart($event, index)" @touchmove.stop="touchMove" @touchend.stop="touchEnd">
|
||
<image src="/static/icons/move.png" class="icon50 margin-left-30" />
|
||
<!-- <i class="lines" /> -->
|
||
</div>
|
||
<div class="icon" >
|
||
<image src="/static/icons/delete.png" class="icon50 margin-left-30" />
|
||
</div>
|
||
</li>
|
||
</ul>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { reactive, watch } from 'vue'
|
||
const emits = defineEmits(['change'])
|
||
const props = defineProps({
|
||
// 每一项item高度,必须
|
||
itemHeight: {
|
||
type: Number,
|
||
required: true
|
||
},
|
||
// 数据列表,必须
|
||
list: {
|
||
type: Array,
|
||
required: true
|
||
},
|
||
// 是否只读
|
||
readonly: {
|
||
type: Boolean,
|
||
default: false
|
||
}
|
||
})
|
||
|
||
const state = reactive({
|
||
// 数据
|
||
newList: [],
|
||
// 初始数据
|
||
initialList: [],
|
||
// 记录所有item的初始坐标
|
||
initialItemYList: [],
|
||
// 坐标数据
|
||
itemYList: [],
|
||
// 记录当前手指的垂直方向的坐标
|
||
touchY: 0,
|
||
// 记录当前操作的item数据
|
||
currentItemY: {},
|
||
// 当前操作的item的下标
|
||
currentIndex: -1
|
||
})
|
||
|
||
watch(
|
||
() => props.list,
|
||
(val) => {
|
||
if (!val?.length) return
|
||
// 获取数据列表
|
||
state.newList = [...val]
|
||
// 记录初始数据
|
||
state.initialList = [...val]
|
||
// 获取所有item的初始坐标
|
||
state.initialItemYList = getItemsY()
|
||
// 初始化坐标
|
||
state.itemYList = getItemsY()
|
||
},
|
||
{
|
||
immediate: true
|
||
}
|
||
)
|
||
|
||
/** @初始化各个item的坐标 **/
|
||
function getItemsY() {
|
||
return props.list.map((item, i) => {
|
||
return {
|
||
top: (i * props.itemHeight)
|
||
}
|
||
})
|
||
}
|
||
|
||
let isDragging = false
|
||
|
||
function touchStart(event, index) {
|
||
if (props.readonly) return
|
||
|
||
// ⚠️ 标记为正在拖拽
|
||
state.isDragging = true
|
||
|
||
h5BodyScroll(false)
|
||
const [{ pageY }] = event.touches
|
||
|
||
state.currentIndex = index
|
||
state.touchY = pageY
|
||
state.currentItemY = state.itemYList[index]
|
||
}
|
||
|
||
|
||
|
||
function touchMove(event) {
|
||
if (props.readonly || !state.isDragging) return
|
||
|
||
const [{ pageY }] = event.touches
|
||
const current = state.itemYList[state.currentIndex]
|
||
const prep = state.itemYList[state.currentIndex - 1]
|
||
const next = state.itemYList[state.currentIndex + 1]
|
||
|
||
// 计算移动距离
|
||
const diff = Math.abs(pageY - state.touchY)
|
||
|
||
// 只有移动超过一定距离才认为是拖拽(避免误触)
|
||
if (diff > 5) { // 5px阈值
|
||
state.itemYList[state.currentIndex] = {
|
||
top: current.top + (pageY - state.touchY)
|
||
}
|
||
state.touchY = pageY
|
||
|
||
// 交换位置逻辑
|
||
if (next && current.top > next.top - props.itemHeight / 2) {
|
||
changePosition(state.currentIndex + 1)
|
||
} else if (prep && current.top < prep.top + props.itemHeight / 2) {
|
||
changePosition(state.currentIndex - 1)
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
/** @手指松开 */
|
||
function touchEnd() {
|
||
if (props.readonly) return
|
||
|
||
// 触发change
|
||
change()
|
||
|
||
// 归位
|
||
state.itemYList[state.currentIndex] = state.initialItemYList[state.currentIndex]
|
||
state.initialList = [...state.newList]
|
||
state.currentIndex = -1
|
||
state.isDragging = false // ← 重置拖拽状态
|
||
|
||
// 恢复滚动
|
||
h5BodyScroll(true)
|
||
}
|
||
|
||
|
||
|
||
/** @交换位置 **/
|
||
// index 需要与第几个下标交换位置
|
||
function changePosition(index) {
|
||
// 记录当前拖拽的item数据
|
||
const tempItem = state.newList[state.currentIndex]
|
||
// 设置原来位置的item
|
||
state.newList[state.currentIndex] = state.newList[index]
|
||
// 将临时存放的数据设置好
|
||
state.newList[index] = tempItem
|
||
|
||
// 调整位置item
|
||
state.itemYList[index] = state.itemYList[state.currentIndex]
|
||
state.itemYList[state.currentIndex] = state.currentItemY
|
||
|
||
// 改变当前操作的的下标
|
||
state.currentIndex = index
|
||
|
||
// 记录新位置的数据
|
||
state.currentItemY = state.initialItemYList[state.currentIndex]
|
||
}
|
||
|
||
/** @回传数据 **/
|
||
function change() {
|
||
// 如果顺序未发生改变,则不触发change事件
|
||
if (JSON.stringify(state.newList) == JSON.stringify(state.initialList)) return
|
||
// 传给父组件新数据
|
||
emits('change', state.newList, state.newList[state.currentIndex])
|
||
}
|
||
|
||
// h5 ios回弹
|
||
function h5BodyScroll(flag) {
|
||
// #ifdef H5
|
||
document.body.style.overflow = flag ? 'initial' : 'hidden'
|
||
document.body.style.webkitOverflowScrolling = flag ? 'touch' : 'auto'
|
||
// #endif
|
||
}
|
||
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.m-drag {
|
||
position: relative;
|
||
}
|
||
|
||
.m-drag-item {
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
transition: all ease 0.25s;
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
// ⚠️ 防止长按选中文字或图片
|
||
user-select: none;
|
||
-webkit-user-select: none;
|
||
-moz-user-select: none;
|
||
-ms-user-select: none;
|
||
|
||
.content-wrapper {
|
||
flex: 1;
|
||
}
|
||
|
||
.icon {
|
||
position: relative;
|
||
top: -40rpx;
|
||
// padding: 30rpx;
|
||
.lines {
|
||
background: #e0e0e0;
|
||
width: 20px;
|
||
height: 2px;
|
||
border-radius: 100rpx;
|
||
margin-left: auto;
|
||
position: relative;
|
||
display: block;
|
||
transition: all ease 0.25s;
|
||
top: -40rpx;
|
||
|
||
&::before,
|
||
&::after {
|
||
position: absolute;
|
||
width: inherit;
|
||
height: inherit;
|
||
border-radius: inherit;
|
||
background: #e0e0e0;
|
||
transition: inherit;
|
||
content: '';
|
||
display: block;
|
||
}
|
||
|
||
&::before {
|
||
top: -14rpx;
|
||
}
|
||
|
||
&::after {
|
||
bottom: -14rpx;
|
||
}
|
||
}
|
||
}
|
||
|
||
&.active {
|
||
box-shadow: 0 0 14rpx rgba(0, 0, 0, 0.08);
|
||
transition: initial;
|
||
z-index: 1;
|
||
|
||
.icon .lines {
|
||
background: #2e97f9;
|
||
|
||
&::before,
|
||
&::after {
|
||
background: #2e97f9;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
</style> |