
拖拽中
介绍
子组件使用css tranisition实现动画效果,使用touchstart、touchmove、touchend实现拖拽事件。
*注意:该组件需要仅是上下拖拽,并且需要给出每个item统一固定高度,不满足您的需求请参考全网其他文章。
父组件使用
javascript
<template>
<m-drag :item-height="50" :list="list" @change="dragComplete">
<template #default="{ item }">
<view class="name">{{ item.name }}</view>
</template>
</m-drag>
</template>
<script setup>
const list = [
{
name: '余额宝支付'
},
{
name: '余额支付'
},
{
name: '建设银行储蓄卡支付'
},
{
name: '农业银行储蓄卡支付'
}
]
// 拖拽完成
function dragComplete(newList, dragItem) {
console.log(newList, dragItem)
}
</script>
<style lang="scss" scoped>
.name {
display: flex;
align-items: center;
margin: 0 24rpx;
height: 50px;
color: #383838;
font-size: 30rpx;
border-bottom: 1px solid #f5f5f5;
}
</style>
子组件
Vue3版本
javascript
<template>
<scroll-view class="m-drag" scroll-y :style="{ height: itemHeight * state.newList.length + 'px' }">
<view
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实现拖拽图标 -->
<view class="icon"
@touchstart="touchStart($event, index)"
@touchmove="touchMove"
@touchend="touchEnd">
<i class="lines" />
</view>
</view>
</scroll-view>
</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: [],
// 记录所有item的初始坐标
initialItemYList: [],
// 坐标数据
itemYList: [],
// 记录当前手指的垂直方向的坐标
touchY: 0,
// 记录当前操作的item数据
currentItemY: {},
// 当前操作的item的下标
currentIndex: -1
})
watch(
() => props.list,
(val) => {
if (!val?.length) return
// 获取数据列表
state.newList = val
// 获取所有item的初始坐标
state.initialItemYList = getItemsY()
// 初始化坐标
state.itemYList = getItemsY()
},
{
immediate: true
}
)
/** @初始化各个item的坐标 **/
function getItemsY() {
return props.list.map((item, i) => {
return {
left: 0,
top: i * props.itemHeight
}
})
}
/** @开始触摸 */
function touchStart(event, index) {
// 只读
if (props.readonly) return
// H5拖拽时,禁止触发ios回弹
h5BodyScroll(false)
const [{ pageY }] = event.touches
// 记录数据
state.currentIndex = index
state.touchY = pageY
state.currentItemY = state.itemYList[index]
}
/** @手指滑动 **/
function touchMove(event) {
// 只读
if (props.readonly) 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]
// 获取移动差值
state.itemYList[state.currentIndex] = {
top: current.top + (pageY - state.touchY)
}
// 记录手指坐标
state.touchY = pageY
// 向下移动(超过下一个的1/2就进行换位)
if (next && current.top > next.top - props.itemHeight / 2) {
changePosition(state.currentIndex + 1)
} else if (prep && current.top < prep.top + props.itemHeight / 2) {
// 向上移动(超过上一个的1/2就进行换位)
changePosition(state.currentIndex - 1)
}
}
/** @手指松开 */
function touchEnd() {
// 只读
if (props.readonly) return
// 传给父组件新数据
emits('change', state.newList, state.newList[state.currentIndex])
// 将拖拽的item归位
state.itemYList[state.currentIndex] = state.initialItemYList[state.currentIndex]
state.currentIndex = -1
// H5开启ios回弹
h5BodyScroll(true)
}
/** @交换位置 **/
// index 需要与第几个下标交换位置
function changePosition(index) {
console.log(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]
}
// h5 ios回弹
function h5BodyScroll(flag) {
// #ifdef H5
document.body.style.overflow = flag ? 'initial' : 'hidden'
// #endif
}
</script>
<style scoped lang="scss">
.m-drag {
position: relative;
width: 100%;
::-webkit-scrollbar {
display: none;
}
.m-drag-item {
position: absolute;
left: 0;
right: 0;
transition: all ease 0.25s;
display: flex;
align-items: center;
> :deep(view:not(.icon)) {
flex: 1;
}
.icon {
padding: 30rpx;
.lines {
background: #e0e0e0;
width: 20px;
height: 2px;
border-radius: 100rpx;
margin-left: auto;
position: relative;
display: block;
transition: all ease 0.25s;
&::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>
Vue2版本
javascript
<template>
<scroll-view class="m-drag" scroll-y :style="{ height: itemHeight * newList.length + 'px' }">
<view
v-for="(item, index) in newList"
:key="index"
class="m-drag-item"
:class="{ active: currentIndex === index }"
:style="{
top: itemYList[index].top + 'px'
}"
>
<slot :item="item" />
<!-- css实现拖拽图标 -->
<view class="icon"
@touchstart="touchStart($event, index)"
@touchmove="touchMove"
@touchend="touchEnd">
<i class="lines" />
</view>
</view>
</scroll-view>
</template>
<script>
export default {
props: {
// 每一项item高度,必须
itemHeight: {
type: Number,
required: true
},
// 数据列表,必须
list: {
type: Array,
required: true
},
// 是否只读
readonly: {
type: Boolean,
default: false
}
},
data() {
return {
// 数据
newList: [],
// 记录所有item的初始坐标
initialItemYList: [],
// 坐标数据
itemYList: [],
// 记录当前手指的垂直方向的坐标
touchY: 0,
// 记录当前操作的item数据
currentItemY: {},
// 当前操作的item的下标
currentIndex: -1
}
},
watch: {
list: {
handler(val) {
if (!val?.length) return
// 获取数据列表
this.newList = val
// 获取所有item的初始坐标
this.initialItemYList = this.getItemsY()
// 初始化坐标
this.itemYList = this.getItemsY()
},
immediate: true
}
},
created() {},
methods: {
/** @初始化各个item的坐标 **/
getItemsY() {
return this.list.map((item, i) => {
return {
left: 0,
top: i * this.itemHeight
}
})
},
/** @开始触摸 */
touchStart(event, index) {
// 只读
if (this.readonly) return
// H5拖拽时,禁止触发ios回弹
this.h5BodyScroll(false)
const [{ pageY }] = event.touches
// 记录数据
this.currentIndex = index
this.touchY = pageY
this.currentItemY = this.itemYList[index]
},
/** @手指滑动 **/
touchMove(event) {
// 只读
if (this.readonly) return
const [{ pageY }] = event.touches
const current = this.itemYList[this.currentIndex]
const prep = this.itemYList[this.currentIndex - 1]
const next = this.itemYList[this.currentIndex + 1]
// 获取移动差值
this.itemYList[this.currentIndex] = {
top: current.top + (pageY - this.touchY)
}
// 记录手指坐标
this.touchY = pageY
// 向下移动(超过下一个的1/2就进行换位)
if (next && current.top > next.top - this.itemHeight / 2) {
this.changePosition(this.currentIndex + 1)
} else if (prep && current.top < prep.top + this.itemHeight / 2) {
// 向上移动(超过上一个的1/2就进行换位)
this.changePosition(this.currentIndex - 1)
}
},
/** @手指松开 */
touchEnd() {
// 只读
if (this.readonly) return
// 传给父组件新数据
this.$emit('change', this.newList, this.newList[this.currentIndex])
// 将拖拽的item归位
this.itemYList[this.currentIndex] = this.initialItemYList[this.currentIndex]
this.currentIndex = -1
// H5开启ios回弹
this.h5BodyScroll(true)
},
/** @交换位置 **/
// index 需要与第几个下标交换位置
changePosition(index) {
// 记录当前拖拽的item数据
const tempItem = this.newList[this.currentIndex]
// 设置原来位置的item
this.newList[this.currentIndex] = this.newList[index]
// 将临时存放的数据设置好
this.newList[index] = tempItem
// 调整位置item
this.itemYList[index] = this.itemYList[this.currentIndex]
this.itemYList[this.currentIndex] = this.currentItemY
// 改变当前操作的的下标
this.currentIndex = index
// 记录新位置的数据
this.currentItemY = this.initialItemYList[this.currentIndex]
},
// h5 ios回弹
h5BodyScroll(flag) {
// #ifdef H5
document.body.style.overflow = flag ? 'initial' : 'hidden'
// #endif
}
}
}
</script>
<style scoped lang="scss">
.m-drag {
position: relative;
width: 100%;
::-webkit-scrollbar {
display: none;
}
.m-drag-item {
position: absolute;
left: 0;
right: 0;
transition: all ease 0.25s;
display: flex;
align-items: center;
> :deep(view:not(.icon)) {
flex: 1;
}
.icon {
padding: 30rpx;
.lines {
background: #e0e0e0;
width: 20px;
height: 2px;
border-radius: 100rpx;
margin-left: auto;
position: relative;
display: block;
transition: all ease 0.25s;
&::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>
iOS 回弹
在ios设备上,拖拽会触发设备的回弹,页面会跟着拖拽滚动,导致拖拽体验不好,我们需要禁用iOS回弹效果。
微信小程序
在pages.json中,添加属性 "disabledScroll": true
javascript
{
"path": "pages/a/index",
"style": {
"navigationBarTitleText": "页面标题",
"disabledScroll": true
}
}
支付宝小程序
在pages.json中,添加属性 "allowsBounceVertical": "NO"
javascript
{
"path": "pages/a/index",
"style": {
"navigationBarTitleText": "页面标题",
"allowsBounceVertical": "NO"
}
}
App
在pages.json中,添加属性 "bounce": "none"
javascript
{
"path": "pages/a/index",
"style": {
"navigationBarTitleText": "页面标题",
"app-plus": {
"bounce": "none"
}
}
}
H5
子组件已经对iOS H5页面回弹,作了处理,具体见 h5BodyScroll