update
This commit is contained in:
parent
10cd938a18
commit
87508cd1f6
|
|
@ -0,0 +1,269 @@
|
|||
<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>
|
||||
Loading…
Reference in New Issue