<template>
|
<view class="container">
|
<view class="sortable-list" ref="list">
|
<view
|
v-for="(item, index) in visibleItems"
|
:key="item.uniqueKey"
|
class="sortable-item"
|
:class="{
|
'dragging': draggingId === item.id,
|
'drop-target': dropTargetIndex === index
|
}"
|
:data-id="item.id"
|
:data-index="index"
|
@touchstart="startDrag(item.id, $event, index)"
|
@touchmove="handleDrag($event)"
|
@touchend="endDrag"
|
@touchcancel="endDrag"
|
>
|
<view class="item-content">
|
<text>{{ item.name }}</text>
|
<text class="position">位置: {{ index + 1 }}</text>
|
<view class="drag-handle">☰</view>
|
</view>
|
</view>
|
</view>
|
</view>
|
</template>
|
|
<script>
|
let keyCounter = 0
|
|
export default {
|
data() {
|
return {
|
items: [
|
{ id: 1, name: '项目A' },
|
{ id: 2, name: '项目B' },
|
{ id: 3, name: '项目C' },
|
{ id: 4, name: '项目D' },
|
{ id: 5, name: '项目E' }
|
],
|
visibleItems: [],
|
draggingId: null,
|
startY: 0,
|
currentY: 0,
|
itemHeight: 60,
|
scrollTop: 0,
|
dropTargetIndex: -1,
|
currentIndex: -1,
|
listTop: 0
|
}
|
},
|
created() {
|
this.initItems()
|
},
|
mounted() {
|
this.getListPosition()
|
},
|
methods: {
|
initItems() {
|
this.visibleItems = this.items.map(item => ({
|
...item,
|
uniqueKey: `item-${item.id}-${keyCounter++}`
|
}))
|
},
|
|
async startDrag(id, e, index) {
|
this.draggingId = id
|
this.currentIndex = index
|
this.dropTargetIndex = -1
|
|
// 获取元素和列表的精确位置
|
await this.getScrollPosition()
|
await this.getItemDimensions()
|
await this.getListPosition()
|
|
// 计算相对于列表顶部的起始位置
|
this.startY = e.touches[0].clientY - this.listTop + this.scrollTop
|
this.currentY = this.startY
|
},
|
|
handleDrag(e) {
|
if (!this.draggingId) return
|
|
e.preventDefault()
|
|
// 计算相对于列表顶部的当前位置
|
this.currentY = e.touches[0].clientY - this.listTop + this.scrollTop
|
|
// 计算应该放置的位置索引
|
const newIndex = Math.floor(this.currentY / this.itemHeight)
|
|
// 边界检查
|
const clampedIndex = Math.max(0, Math.min(newIndex, this.visibleItems.length - 1))
|
|
// 更新目标位置
|
this.dropTargetIndex = clampedIndex
|
|
// 如果位置发生变化,则执行交换
|
if (clampedIndex !== this.currentIndex) {
|
this.swapItems(clampedIndex)
|
}
|
},
|
|
swapItems(newIndex) {
|
const items = [...this.visibleItems]
|
const draggingItem = items.find(item => item.id === this.draggingId)
|
const oldIndex = items.indexOf(draggingItem)
|
|
// 执行位置交换
|
items.splice(oldIndex, 1)
|
items.splice(newIndex, 0, draggingItem)
|
|
this.visibleItems = items
|
this.currentIndex = newIndex
|
},
|
|
endDrag() {
|
if (!this.draggingId) return
|
|
// 同步回原始数据
|
this.items = this.visibleItems.map(item => {
|
const { uniqueKey, ...rest } = item
|
return rest
|
})
|
|
// 重置状态
|
this.draggingId = null
|
this.dropTargetIndex = -1
|
this.currentIndex = -1
|
},
|
|
async getItemDimensions() {
|
return new Promise(resolve => {
|
const query = uni.createSelectorQuery().in(this)
|
query.select('.sortable-item').boundingClientRect()
|
query.exec(res => {
|
if (res[0]) this.itemHeight = res[0].height
|
resolve()
|
})
|
})
|
},
|
|
async getScrollPosition() {
|
return new Promise(resolve => {
|
const query = uni.createSelectorQuery().in(this)
|
query.selectViewport().scrollOffset()
|
query.exec(res => {
|
this.scrollTop = res[0]?.scrollTop || 0
|
resolve()
|
})
|
})
|
},
|
|
async getListPosition() {
|
return new Promise(resolve => {
|
const query = uni.createSelectorQuery().in(this)
|
query.select('.sortable-list').boundingClientRect()
|
query.exec(res => {
|
this.listTop = res[0]?.top || 0
|
resolve()
|
})
|
})
|
},
|
|
getItemStyle(item) {
|
if (item.id !== this.draggingId) return {}
|
|
// 计算相对于列表顶部的偏移量
|
const offset = this.currentY - this.startY
|
|
return {
|
transform: `translateY(${offset}px)`,
|
transition: 'none',
|
zIndex: 100,
|
opacity: 0.9,
|
position: 'relative'
|
}
|
}
|
}
|
}
|
</script>
|
|
<style>
|
.container {
|
padding: 20px;
|
}
|
|
.sortable-list {
|
background-color: #f5f5f5;
|
border-radius: 8px;
|
padding: 10px;
|
position: relative;
|
}
|
|
.sortable-item {
|
margin-bottom: 10px;
|
transition: transform 0.2s;
|
position: relative;
|
}
|
|
.item-content {
|
height: 60px;
|
background-color: #fff;
|
border-radius: 8px;
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
padding: 0 15px;
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
}
|
|
.sortable-item.dragging .item-content {
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
background-color: #f8fbff;
|
}
|
|
.sortable-item.drop-target .item-content {
|
background-color: #f0f7ff;
|
border: 1px dashed #4a90e2;
|
}
|
|
.position {
|
color: #888;
|
font-size: 12px;
|
}
|
|
.drag-handle {
|
color: #ccc;
|
font-size: 20px;
|
padding: 0 10px;
|
}
|
</style>
|