uniapp+v3开发小程序拖拽排序功能

发布于:2025-03-14 ⋅ 阅读:(17) ⋅ 点赞:(0)
项目需求:需要根据用户喜好手动排序(这里只需要上下排序)

排序前(左图) => 排序时(右图)

拖动演示

思路: 
1.创建一个拖动的元素,当拖动元素与其他元素触碰时更换位置重排列表
2.长按元素记录起点位置与下标,并将目标元素赋值与拖动元素
3.移动时处理触碰逻辑
4.松手时清空拖动元素
5.全部代码如下
1.getPos方法获取每个元素的位置top/left,便于交互后重新排列的元素定位
2.getIntersectRow方法根据拖动元素的top及bottom判断与那个元素相交,可能不止一个,取相交部分最多一个
<template>
	<view class="realTime">
		<uni-nav-bar dark left-icon="left" left-text="" @clickLeft="prePage" :fixed="true" shadow
			background-color="#3B45FF" status-bar title="实时参数" />
		<view class="wrap boxSize" @touchmove="moveHandle" @touchend="endHandle">
			<view class="paramsIt boxSize" @longpress="e => startHandle(e, index, item)" :class="{'sortIt': isSort}"
				:style="{'top':item.top + 'px', 'left': item.left + 'px'}" v-for="(item,index) in paramsList"
				:key="item.key">
				<view class="left">
					<image v-if="isSort" src="@/static/svg/sort.svg" mode=""></image>
					<view class="name">{{item.key}}</view>
				</view>
				<view class="val" v-if="!isSort">{{`${item.val}${item.unit}`}}</view>
			</view>
			<view class="paramsIt boxSize sortIt active" v-if="activeItem"
				:style="{'top':activeItem.top + 'px', 'left': activeItem.left + 'px'}">
				<view class="left">
					<image src="@/static/svg/sort.svg" mode=""></image>
					<view class="name">{{activeItem.key}}</view>
				</view>
			</view>
		</view>
		<view class="btnBox boxSize">
			<button type="default" style="color:#ffffff;backgroundColor:#3B45FF;borderColor:#1AAD19" class="btn"
				@tap="sortHandle">{{isSort ? '确认' : '参数排序'}}</button>
		</view>
	</view>
</template>

<script setup>
	import {
		getCurrentInstance,
		nextTick,
		reactive,
		toRefs
	} from 'vue'
	import {
		prePage,
		goPage
	} from '@/utils/util'
	import {
		request
	} from '@/utils/api'
	import {
		onLoad
	} from "@dcloudio/uni-app"

	onLoad(config => {
		openFunction(config)
	})

	let state = reactive({
		routeParams: {},
		isSort: false,
		activeIdx: null,
		activeItem: null,
		copyItem: null,
		itemHeight: 65,
		positionList: [],
		startY: '',
		paramsList: []
	})
	let {
		isSort,
		activeIdx,
		positionList,
		paramsList,
		activeItem
	} = toRefs(state)

	let pageInstance = getCurrentInstance()

	let openFunction = (config) => {
		state.routeParams = JSON.parse(JSON.stringify(config))
		getParams()
	}

	//获取所有参数
	let getParams = async () => {
		let res = await request('device/FieldOrder/current_data/', 'GET', {
			pond_id: state.routeParams.pond_id,
			user_id: JSON.parse(uni.getStorageSync('userInfo')).id
		})
		if (res) {
			state.paramsList = JSON.parse(JSON.stringify(res.data))
		}
	}

	//获取所有元素的定位信息
	let getPos = () => {
		let query = uni.createSelectorQuery(pageInstance.proxy)
		query.selectAll('.paramsIt').boundingClientRect().exec(res => {
			let arr = []
			res[0].forEach((it, idx) => {
				let obj = {}
				obj.top = (it.top - 88) + 12 * idx
				obj.left = it.left
				arr.push(obj)
			})
			state.positionList = JSON.parse(JSON.stringify(arr))
			state.paramsList.forEach((it, idx) => {
				it.top = state.positionList[idx].top
				it.left = state.positionList[idx].left
			})
		})
	}

	//开始排序
	let sortHandle = async () => {
		if (!state.isSort) {
			getPos()
			state.isSort = true
			return
		}
		
		let paramsList = state.paramsList.map(it => ({
			key: it.key,
			val: it.val,
			unit: it.unit,
			status: it.status
		}))

		let res = await request('device/FieldOrder/', 'POST', {
			pond_id: state.routeParams.pond_id,
			user_id: JSON.parse(uni.getStorageSync('userInfo')).id,
			result: paramsList
		})

		if (res) {
			uni.showToast({
				icon: 'none',
				title: '操作成功!'
			})
			state.isSort = false
			getParams()
		}
	}

	//按下
	let startHandle = (e, index, item) => {
		if (!state.isSort) return
		state.activeIdx = index
		state.activeItem = JSON.parse(JSON.stringify(item))
		state.startY = e.touches[0].clientY
	}

	//移动
	let moveHandle = (e, index) => {
		if (!state.activeItem) return

		//元素跟随鼠标移动
		let diffY = e.touches[0].clientY - state.startY
		state.activeItem.top = state.activeItem.top + diffY
		state.startY = e.touches[0].clientY

		//判断交叉元素
		let dragTop = state.activeItem.top
		let dragBtm = dragTop + state.itemHeight
		let intersectRow = getIntersectRow(dragTop, dragBtm)
		if (!intersectRow) return
		let intersectIdx = state.positionList.findIndex(i => i.top == intersectRow.top)

		//其他元素变换位置
		let copyParams = [...state.paramsList]
		if (intersectIdx !== state.activeIdx) {
			copyParams.splice(state.activeIdx, 1)
			copyParams.splice(intersectIdx, 0, state.paramsList[state.activeIdx])
			copyParams.forEach((it, idx) => {
				it.top = state.positionList[idx].top
				it.left = state.positionList[idx].left
			})
			state.activeIdx = intersectIdx
			nextTick(() => {
				state.paramsList = [...copyParams]
			})
		}
	}

	//抬起
	let endHandle = (e, index) => {
		state.activeIdx = null
		state.activeItem = null
	}

	//交叉元素
	let getIntersectRow = (dragTop, dragBtm) => {
		let filter = state.positionList.filter(it => {
			let acmeFlag = it.top < dragTop && (it.top + state.itemHeight) > dragTop
			let lowFlag = it.top < dragBtm && (it.top + state.itemHeight) > dragBtm
			return acmeFlag || lowFlag
		})
		//取最近一个
		let midY = dragTop + state.itemHeight / 2
		filter.forEach(i => i.diffY = Math.abs((i.top + state.itemHeight / 2) - midY))
		filter.sort((a, b) => a.diffY - b.diffY)
		return filter[0] || null
	}
</script>

<style lang="scss" scoped>
	.realTime {
		width: 100%;
		height: 100%;

		.wrap {
			width: 100%;
			height: calc(100% - 160px);
			padding: 12px 18px;
			overflow-y: auto;
			border-radius: 8px;
			position: relative;

			.paramsIt {
				position: relative;
				width: 100%;
				height: 65px;
				padding: 0 20px;
				display: flex;
				align-items: center;
				justify-content: space-between;
				background-color: #fff;
				transition: all 0.3s ease; // 添加过渡效果

				.left {
					display: flex;
					align-items: center;

					image {
						width: 24px;
						height: 24px;
						margin-right: 10px;
					}

					.name {
						font-family: PingFang SC;
						font-size: 18px;
						font-weight: 500;
						line-height: normal;
						letter-spacing: 0px;
						font-variation-settings: "opsz" auto;
						/* 正文色/正文色 */
						color: #1A1A1A;
					}
				}

				.val {
					font-family: PingFang SC;
					font-size: 18px;
					font-weight: normal;
					line-height: normal;
					text-align: right;
					letter-spacing: 0px;
					font-variation-settings: "opsz" auto;
					/* 字体/次要文字 */
					color: #909399;
				}
			}

			.sortIt {
				position: absolute;
				width: calc(100% - 36px);
				border-radius: 8px;
				margin-bottom: 12px;
			}

			.active {
				box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
				/* 添加阴影 */
				opacity: 0.8;
				/* 降低透明度 */
				// 确保拖拽元素在最上层
				z-index: 2;
				// 拖拽元素禁用过渡
				transition: none !important;
			}
		}

		.btnBox {
			width: 100%;
			padding: 0 18px;
			display: flex;
			align-items: center;
			justify-content: space-between;

			.btn {
				width: 100%;
			}
		}
	}
</style>

注意: 拖动后再给原数组赋值时需加上nextTick函数,如需多行多列拖动只需再次基础上加部分逻辑即可