UniApp 实现兼容 H5 和小程序的拖拽排序组件
如何使用 UniApp 实现一个兼容 H5 和小程序的 九宫格拖拽排序组件,实现思路和关键步骤。
一、完整效果图示例
H5端

小程序端

git地址
二、实现目标
- 支持拖动菜单项改变顺序
- 拖拽过程实时预览移动位置
- 拖拽松开后自动吸附回网格
- 兼容 H5 和小程序平台
三、功能结构拆解以及完整代码
完整代码:
<template><view class="container"><view class="menu-title">菜单列表</view><view class="grid-container"><viewclass="grid-item"v-for="(item, index) in menuList":key="index":class="{ 'active': currentIndex === index }":style="getPositionStyle(index)"@touchstart="handleTouchStart($event, index)"@touchmove.stop.prevent="handleTouchMove($event)"@touchend="handleTouchEnd"><view class="item-content"><view class="item-icon"><uni-icons :type="item.icon || 'star'" size="24"></uni-icons></view><view class="item-name">{{ item.name }}</view></view></view></view></view>
</template><script>
export default {name: 'MenuGrid',data() {return {// 菜单项列表menuList: [{ name: '首页', icon: 'home' },{ name: '消息', icon: 'chat' },{ name: '联系人', icon: 'contact' },{ name: '日历', icon: 'calendar' },{ name: '设置', icon: 'gear' },{ name: '相册', icon: 'image' },{ name: '文件', icon: 'folder' },{ name: '位置', icon: 'location' },{ name: '收藏', icon: 'star-filled' },{ name: '视频', icon: 'videocam' },{ name: '音乐', icon: 'sound' },{ name: '订单', icon: 'paperplane' }],// 网格配置columns: 4, // 每行显示的列数itemSize: 80, // 每个项目的大小 (单位px)itemGap: 15, // 项目之间的间隔// 拖拽状态currentIndex: -1, // 当前拖拽的项目索引startX: 0, // 触摸开始X坐标startY: 0, // 触摸开始Y坐标moveOffsetX: 0, // X轴移动的距离moveOffsetY: 0, // Y轴移动的距离positions: [], // 所有项目的位置isDragging: false // 是否正在拖拽}},mounted() {this.initPositions();},methods: {// 初始化所有项目的位置initPositions() {this.positions = [];const { itemSize, itemGap, columns } = this;this.menuList.forEach((_, index) => {const row = Math.floor(index / columns);const col = index % columns;// 计算项目位置this.positions.push({x: col * (itemSize + itemGap),y: row * (itemSize + itemGap),zIndex: 1});});},// 获取项目定位样式getPositionStyle(index) {if (!this.positions[index]) return '';const position = this.positions[index];const { itemSize } = this;return {transform: `translate3d(${position.x}px, ${position.y}px, 0)`,width: `${itemSize}px`,height: `${itemSize}px`,zIndex: position.zIndex || 1};},// 处理触摸开始handleTouchStart(event, index) {if (this.isDragging) return;const touch = event.touches[0];this.currentIndex = index;this.startX = touch.clientX;this.startY = touch.clientY;this.moveOffsetX = 0;this.moveOffsetY = 0;this.isDragging = true;// 提升当前项的层级this.positions[index].zIndex = 10;// 震动反馈uni.vibrateShort();},// 处理触摸移动handleTouchMove(event) {if (this.currentIndex === -1 || !this.isDragging) return;const touch = event.touches[0];// 计算移动距离const deltaX = touch.clientX - this.startX;const deltaY = touch.clientY - this.startY;this.moveOffsetX += deltaX;this.moveOffsetY += deltaY;// 更新拖拽项的位置this.positions[this.currentIndex].x += deltaX;this.positions[this.currentIndex].y += deltaY;// 更新开始位置,用于下一次移动计算this.startX = touch.clientX;this.startY = touch.clientY;// 检查是否需要交换位置this.checkForSwap();},// 处理触摸结束handleTouchEnd() {if (this.currentIndex === -1) return;// 重置拖拽项的层级if (this.positions[this.currentIndex]) {this.positions[this.currentIndex].zIndex = 1;}// 将所有项吸附到网格this.snapAllItemsToGrid();// 重置拖拽状态this.isDragging = false;this.currentIndex = -1;this.moveOffsetX = 0;this.moveOffsetY = 0;// 触发排序完成事件this.$emit('sort-complete', [...this.menuList]);},// 将所有项吸附到网格snapAllItemsToGrid() {const { itemSize, itemGap, columns } = this;this.menuList.forEach((_, index) => {const row = Math.floor(index / columns);const col = index % columns;this.positions[index] = {x: col * (itemSize + itemGap),y: row * (itemSize + itemGap),zIndex: 1};});},// 检查是否需要交换位置checkForSwap() {if (this.currentIndex === -1) return;const currentPos = this.positions[this.currentIndex];const { itemSize, itemGap } = this;let closestIndex = -1;let minDistance = Number.MAX_VALUE;// 找出与当前拖拽项距离最近的项this.positions.forEach((pos, index) => {if (index !== this.currentIndex) {// 计算中心点之间的距离const centerX1 = currentPos.x + itemSize / 2;const centerY1 = currentPos.y + itemSize / 2;const centerX2 = pos.x + itemSize / 2;const centerY2 = pos.y + itemSize / 2;const distance = Math.sqrt(Math.pow(centerX1 - centerX2, 2) +Math.pow(centerY1 - centerY2, 2));// 只考虑距离小于阈值的项const threshold = (itemSize + itemGap) * 0.6;if (distance < threshold && distance < minDistance) {minDistance = distance;closestIndex = index;}}});// 如果找到了足够近的项,交换位置if (closestIndex !== -1) {this.swapItems(this.currentIndex, closestIndex);}},// 交换两个项目swapItems(fromIndex, toIndex) {// 交换菜单列表中的项const temp = { ...this.menuList[fromIndex] };this.$set(this.menuList, fromIndex, { ...this.menuList[toIndex] });this.$set(this.menuList, toIndex, temp);// 交换位置信息[this.positions[fromIndex], this.positions[toIndex]] =[this.positions[toIndex], this.positions[fromIndex]];// 更新当前拖拽的索引this.currentIndex = toIndex;}}
}
</script><style scoped>
.container {padding: 20rpx;background-color: #f7f7f7;
}.menu-title {font-size: 32rpx;font-weight: bold;margin-bottom: 30rpx;text-align: center;
}.grid-container {position: relative;width: 100%;min-height: 500rpx;overflow: hidden;
}.grid-item {position: absolute;left: 0;top: 0;transition: transform 0.3s ease;will-change: transform;
}.grid-item.active {transition: none;transform: scale(1.05);z-index: 10;
}.item-content {width: 100%;height: 100%;display: flex;flex-direction: column;align-items: center;justify-content: center;background-color: #ffffff;border-radius: 12rpx;box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}.item-icon {display: flex;justify-content: center;align-items: center;margin-bottom: 10rpx;
}.item-name {font-size: 24rpx;color: #333;text-align: center;
}
</style>
整个功能可以拆分为以下几个部分:
- 网格布局计算:确定每个 item 的初始位置
- 拖拽事件绑定:监听
touchstart/touchmove/touchend - 实时移动渲染:跟随手指移动改变 transform 样式
- 最近距离判断:判断最近的可交换项并交换
- 松开后归位:释放手指后吸附至新的位置
四、组件结构设计
1. 模板部分
使用 v-for 渲染菜单项,并绑定触摸事件。
<view class="grid-item"v-for="(item, index) in menuList":key="index":class="{ 'active': currentIndex === index }":style="getPositionStyle(index)"@touchstart="handleTouchStart($event, index)"@touchmove.stop.prevent="handleTouchMove($event)"@touchend="handleTouchEnd"><!-- 图标和文字 -->
</view>
2. 数据结构
menuList: 菜单数据positions: 所有 item 的坐标信息currentIndex: 当前拖拽的索引startX/Y: 拖拽起始点坐标moveOffsetX/Y: 移动的累计距离isDragging: 是否正在拖拽中
3. 初始化位置
通过 itemSize + itemGap + columns 计算每一项的坐标。
const row = Math.floor(index / columns);
const col = index % columns;
positions.push({x: col * (itemSize + itemGap),y: row * (itemSize + itemGap),zIndex: 1
});
4. 拖拽处理流程
- 触摸开始
- 记录初始触摸位置
- 提升 z-index
- 设置当前拖拽 index
- 拖动中
- 计算当前位置偏移量
- 实时更新拖拽项的 transform 位置
- 检查距离最近的其他项是否可交换
- 拖动结束
- 重置拖拽状态
- 吸附所有项回网格对齐
- 发出排序完成事件
5. 交换逻辑
通过拖拽项与其它项之间的中心点距离,找到最近项,判断是否在交换阈值范围内(比如 0.6 倍 itemSize + gap),再触发 swapItems。
const distance = Math.sqrt((dx)^2 + (dy)^2);
if (distance < threshold) swapItems(fromIndex, toIndex);
五、平台兼容性说明
- 小程序端: 使用
touchstart,touchmove,touchend原生事件即可 - H5端: 同样支持原生事件,需使用
stop.prevent修饰符阻止页面滚动 - 注意事项: 不建议使用
@mousedown等 PC 事件,移动端表现不一致
六、性能优化建议
- 使用
transform: translate3d提升动画性能 - 拖拽时关闭 transition,松开后再开启
- 将 drag 状态变化为响应式变量,避免频繁操作 DOM
七、总结
本组件通过计算每个 item 的位置并绑定触摸事件,实现了拖拽排序的能力,支持吸附、交换和动态位置调整,兼容多个平台。适用于菜单管理、组件排序等场景,封装后复用性强。
如果你有更多关于 UniApp 拖拽交互的场景需求,欢迎留言讨论!
**
相关文章:
UniApp 实现兼容 H5 和小程序的拖拽排序组件
如何使用 UniApp 实现一个兼容 H5 和小程序的 九宫格拖拽排序组件,实现思路和关键步骤。 一、完整效果图示例 H5端 小程序端 git地址 二、实现目标 支持拖动菜单项改变顺序拖拽过程实时预览移动位置拖拽松开后自动吸附回网格兼容 H5 和小程序平台 三、功能…...
C,C++,C#
C、C 和 C# 是三种不同的编程语言,虽然它们名称相似,但在设计目标、语法特性、运行环境和应用场景上有显著区别。以下是它们的核心区别: 1. 设计目标和历史 语言诞生时间设计目标特点C1972(贝尔实验室)面向过程&#…...
MySQL | 三大日志文件
Undo Log(回滚日志) 实现原理与分类 原理:Undo Log 记录的是数据修改前的旧值,通过这些旧值可以将数据恢复到修改之前的状态。它采用的是逻辑日志,即记录的是如何撤销操作,而不是物理数据的实际值。 分类…...
【网络协议】WebSocket讲解
目录 webSocket简介 连接原理解析: 客户端API 服务端API(java) 实战案例 (1)引入依赖 (2)编写服务端逻辑 (3)注册配置类 (4)前端连接 WebSocket 示例…...
啥是Spring,有什么用,既然收费,如何免费创建SpringBoot项目,依赖下载不下来的解决方法,解决99%问题!
一、啥是Spring,为啥选择它 我们平常说的Spring指的是Spring全家桶,我们为什么要选择Spring,看看官方的话: 意思就是:用这个东西,又快又好又安全,反正就是好处全占了,所以我们选择它…...
一天时间,我用AI(deepseek)做了一个配色网站
前言 最近在开发颜色搭配主题的相关H5和小程序,想到需要补充一个web网站,因此有了这篇文章。 一、确定需求 向AI要答案之前,一定要清楚自己想要做什么。如果你没有100%了解自己的需求,可以先让AI帮你理清逻辑和思路,…...
Day14:关于MySQL的索引——创、查、删
前言:先创建一个练习的数据库和数据 1.创建数据库并创建数据表的基本结构 -- 创建练习数据库 CREATE DATABASE index_practice; USE index_practice;-- 创建基础表(包含CREATE TABLE时创建索引) CREATE TABLE products (id INT PRIMARY KEY…...
Pytorch深度学习框架60天进阶学习计划 - 第41天:生成对抗网络进阶(二)
Pytorch深度学习框架60天进阶学习计划 - 第41天:生成对抗网络进阶(二) 7. 实现条件WGAN-GP # 训练条件WGAN-GP def train_conditional_wgan_gp():# 用于记录损失d_losses []g_losses []# 用于记录生成样本的多样性(通过类别分…...
Spring - 13 ( 11000 字 Spring 入门级教程 )
一: Spring AOP 备注:之前学习 Spring 学到 AOP 就去梳理之前学习的知识点了,后面因为各种原因导致 Spring AOP 的博客一直搁置。。。。。。下面开始正式的讲解。 学习完 Spring 的统一功能后,我们就进入了 Spring AOP 的学习。…...
Spring Cloud Alibaba微服务治理实战:Nacos+Sentinel深度解析
一、引言 在微服务架构中,服务发现、配置管理、流量控制是保障系统稳定性的核心问题。Spring Cloud Netflix 生态曾主导微服务解决方案,但其部分组件(如 Eureka、Hystrix)已进入维护模式。 Spring Cloud Alibaba 凭借 高性能、轻…...
设计模式之迭代器模式:遍历的艺术与实现
引言 迭代器模式(Iterator Pattern)是一种行为型设计模式,它提供了一种顺序访问聚合对象中各个元素的方法,而又不暴露其底层实现。迭代器模式将遍历逻辑与聚合对象解耦,使得我们可以用统一的方式处理不同的集合结构。…...
红宝书第三十六讲:持续集成(CI)配置入门指南
红宝书第三十六讲:持续集成(CI)配置入门指南 资料取自《JavaScript高级程序设计(第5版)》。 查看总目录:红宝书学习大纲 一、什么是持续集成? 持续集成(CI)就像咖啡厅的…...
Java—HTML:3D形变
今天我要介绍的是在Java HTML中CSS的相关知识点内容之一:3D形变(3D变换)。该内容包含透视(属性:perspective),3D变换,3D变换函数以及案例演示, 接下来我将逐一介绍&…...
什么是音频预加重与去加重,预加重与去加重的原理是什么,在什么条件下会使用预加重与去加重?
音频预加重与去加重是音频处理中的两个重要概念,以下是对其原理及应用条件的详细介绍: 1、音频预加重与去加重的定义 预加重:在音频信号的发送端,对音频信号的高频部分进行提升,增加高频信号的幅度,使其在…...
免费下载 | 2025清华五道口:“十五五”金融规划研究白皮书
《2025清华五道口:“十五五”金融规划研究白皮书》的核心内容主要包括以下几个方面: 一、五年金融规划的重要功能与作用 凝聚共识:五年金融规划是国家金融发展的前瞻性谋划和战略性安排,通过广泛听取社会各界意见,凝…...
微信小程序实战案例 - 餐馆点餐系统 阶段 4 - 订单列表 状态
✅ 阶段 4 – 订单列表 & 状态 目标 展示用户「我的订单」列表支持状态筛选(全部 / 待处理 / 已完成)支持分页加载和实时刷新使用原生组件编写 ✅ 1. 页面结构:文件结构 pages/orders/├─ index.json├─ index.wxml├─ index.js└─…...
如何为C++实习做准备?
博主介绍:程序喵大人 35- 资深C/C/Rust/Android/iOS客户端开发10年大厂工作经验嵌入式/人工智能/自动驾驶/音视频/游戏开发入门级选手《C20高级编程》《C23高级编程》等多本书籍著译者更多原创精品文章,首发gzh,见文末👇…...
【docker】--部署--安装docker教程
文章目录 环境方法一:脚本安装方法二:手动安装**步骤 1:卸载旧版本(如有)****步骤 2:更新系统并安装依赖****步骤 3:添加 Docker 官方 GPG 密钥****步骤 4:设置 Docker 仓库****步骤…...
【开发记录】服务外包大赛记录
参加服务外包大赛的A07赛道中,最近因为频繁的DEBUG,心态爆炸 记录错误 以防止再次出现错误浪费时间。。。 2025.4.13 项目在上传图片之后 会自动刷新 没有等待后端返回 Network中的fetch /upload显示canceled. 然而这是使用了VS的live Server插件才这样&…...
Cesium.js(6):Cesium相机系统
Camera表示观察场景的视角。通过操作摄像机,可以控制视图的位置、方向和角度。 帮助文档:Camera - Cesium Documentation 1 setView setView 方法允许你指定相机的目标位置和姿态。你可以通过 Cartesian3 对象来指定目标位置,并通过 orien…...
AI 代码生成工具如何突破 Java 单元测试效能天花板?
一、传统单元测试的四大痛点 时间黑洞:根据 JetBrains 调研,Java 开发者平均花费 35% 时间编写测试代码覆盖盲区:手工测试覆盖率普遍低于 60%(Jacoco 全球统计数据)维护困境:业务代码变更导致 38% 的测试用…...
AF3 ProteinDataset类的_patch方法解读
AlphaFold3 protein_dataset模块 ProteinDataset 类 _patch 方法的主要目的是围绕锚点残基(anchor residues)裁剪蛋白质数据,提取一个局部补丁(patch)作为模型输入。 源代码: def _patch(self, data):"""Cut the data around the anchor residues."…...
客户端负载均衡与服务器端负载均衡详解
客户端负载均衡与服务器端负载均衡详解 1. 客户端负载均衡(Client-Side Load Balancing) 核心概念 定义:负载均衡逻辑在客户端实现,客户端主动选择目标服务实例。典型场景:微服务内部调用(如Spring Cloud…...
vue-element-plus-admin的安装
文档链接:开始 | vue-element-plus-admin 之前尝试按照官方文档来安装,运行npm run dev命令却不能正常打开访问浏览器,换一个方式 首先在目录下打开命令窗口 1、克隆项目 从 GitHub 获取代码 # clone 代码 git clone https://github.com…...
基于springboot的“流浪动物管理系统”的设计与实现(源码+数据库+文档+PPT)
基于springboot的“流浪动物管理系统”的设计与实现(源码数据库文档PPT) 开发语言:Java 数据库:MySQL 技术:springboot 工具:IDEA/Ecilpse、Navicat、Maven 系统展示 系统功能结构图 局部E-R图 系统首页界面 系统…...
爬虫解决debbugger之替换文件
鼠鼠上次做一个网站的时候,遇到的debbugger问题,是通过打断点然后编辑断点解决的,现在鼠鼠又学会了一个新的技能 首先需要大家下载一个reres的插件,这里最好用谷歌浏览器 先请大家看看案例国家水质自动综合监管平台 这里我们只…...
奇怪的电梯——DFS算法
题目 题解 每到一层楼都面临了两种选择:上还是下?因此我们可以定义一个布尔数组用来记录选择。 终止条件其实也明显,要么到了B层,要么没有找到楼层。 如果找到了,选择一个步骤少的方式。又怎么表示没有找到楼层&…...
Open GL ES-> 工厂设计模式包装 SurfaceView + 自定义EGL的OpenGL ES 渲染框架
XML文件 <?xml version"1.0" encoding"utf-8"?> <com.example.myapplication.EGLSurfaceView xmlns:android"http://schemas.android.com/apk/res/android"android:layout_width"match_parent"android:layout_height"…...
2.4goweb加解密和jwt
MD5的基本实现 1. 标准库调用 Go语言通过crypto/md5包提供MD5算法的实现。核心步骤包括: 创建哈希对象:使用md5.New()生成一个实现了hash.Hash接口的实例。写入数据:通过Write()方法或io.WriteString()将数据写入…...
深入解析多表联查(MySQL)
前言 在面试中以及实际开发中,多表联查是每个程序员必备技能,下文通过最简单的学生表和课程表的实例帮大家最快入门多表联查技能。 建立数据表 1. 学生表(students) 创建学生表 CREATE TABLE students (student_id INT AUTO_…...
