基于 CSS Grid 的简易拖拉拽 Vue3 组件,从代码到NPM发布(1)- 拖拉拽交互
基于特定的应用场景,需要在页面中以网格的方式,实现目标组件在网格中可以进行拖拉拽、修改大小等交互。本章开始分享如何一步步从代码设计,最后到如何在 NPM 上发布。
请大家动动小手,给我一个免费的 Star 吧~
大家如果发现了 Bug,欢迎来提 Issue 哟~
github源码
示例地址
特别说明一下,此组件是基于 CSS 的 display: grid 的,并非全能型拖拉拽交互,grid 不支持的基本就是不支持的,此组件的目标是达到一些简易的网格布局拖拉拽交互。
效果图

项目结构
项目结构是基于另外一个项目 konva-designer-sample,特别说一下需要关注的部分:
└─ dist - 构建的组件库文件
└─ docs - 构建的在线示例网站
└─ src└─ demo│ └─ App.vue - 在线示例页面└─ lib└─ components└─ GridDragResize - 组件目录└─ GridDragResize.vue - 组件└─ GridDragResizeItem.vue - 子组件└─ index.ts - 组件入口└─ style.less - 组件样式└─ types.ts - 组件配套类型声明└─ main.ts - 在线示例代码入口
└─ index.html - 在线示例HTML入口
└─ package.json - 库信息
└─ tsconfig.build.json - 用于构建组件库配套的类型声明文件
└─ vite.config.ts - 构建配置
使用方式
直接先看看组件的使用方式:
src/demo/App.vue
<script setup lang="ts">
import { ref, h, type Ref } from 'vue'
// 组件
import { GridDragResize } from '@/lib/components/GridDragResize'
// 组件配套类型声明
import type { GridDragResizeProps } from '@/lib/components/GridDragResize/types'// 组件数据结构
const children: Ref<GridDragResizeProps['children']> = ref([{dragHandler: '.demo-item>button',render: () => h('div', { class: "demo-item", style: { background: '#eb9c64' } }, [h('button', 'drag handler')])},{columnStart: 2,draggable: false,render: () => h('div', { class: "demo-item", style: { background: '#ff8789' } }, 'disable drag')},{rowStart: 2,columnStart: 2,render: () => h('div', { class: "demo-item", style: { background: '#554e4f' } }, '1')},{rowStart: 2,rowEnd: 4,columnStart: 4,columnEnd: 5,render: () => h('div', { class: "demo-item", style: { background: '#8fbf9f' } }, '2')},{rowStart: 4,rowEnd: 6,columnStart: 2,columnEnd: 4,render: () => h('div', { class: "demo-item", style: { background: '#346145' } }, '3')},{rowStart: 4,rowEnd: 5,columnStart: 1,columnEnd: 2,render: () => h('div', { class: "demo-item", style: { background: '#c2baa6' } }, '4')},
])
</script><template>
<div class="page"><!-- 组件使用 --><GridDragResize :columns="4" :rows="5" :gap="10" :row-size="100" :readonly="false" :children="children"></GridDragResize><!-- 组件数据结构 实时状态 --><div v-html="JSON.stringify(children, null, 2).replace(/\n/g, '<br>').replace(/\s/g, ' ')"></div>
</div>
</template><style lang="less">
// 一些样式初始化*,
*::before,
*::after {box-sizing: border-box;margin: 0;font-weight: normal;
}body {min-height: 100vh;color: var(--color-text);background: var(--color-background);transition:color 0.5s,background-color 0.5s;line-height: 1.6;font-family:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Fira Sans','Droid Sans','Helvetica Neue',sans-serif;font-size: 15px;text-rendering: optimizeLegibility;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;
}
</style>
<style lang="less">
// 示例样式
.page {padding: 32px;
}.demo-item {padding: 10px;height: 100%;
}// 组件样式覆盖
.grid-drag-resize {background-color: #eee;.grid-drag-resize__item {background-color: #ddd;&--dragging {box-shadow: 0 0 6px 2px #0000ff;}}
}
</style>
上面可以看出,render 是比较关键的地方,该组件使用方式并非 插槽,而是通过数据结构传入的 render 实现每一块的显示的,它可以是 h 可以是一个个 其他组件。
接下来,可以看看定义:
组件 Props 定义
// src/lib/components/GridDragResize/types.tsimport type { VNode } from 'vue'// 子组件的 Props
export interface GridDragResizeItemProps {draggable?: booleandragHandler?: string // 满足 querySelector 的查询字符串,指向可拖拉拽的元素位置// css display grid 属性columnStart?: numbercolumnEnd?: numberrowStart?: numberrowEnd?: number//render?: () => VNode
}// 组件的 Props
export interface GridDragResizeProps {dragHandler?: string // 同上,优先级 低于 子组件readonly?: boolean // 优先级 低于 子组件 的 draggable//columns?: number // 列数rows?: number // 行数gap?: number // 间隙columnSize?: number // 列宽,默认是 1frrowSize?: number // 行高,默认是 1fr//children?: GridDragResizeItemProps[] // 子组件
}
目前为止,定义非常简单。
组件
src/lib/components/GridDragResize/GridDragResize.vue
逻辑说明,请留意代码注释
<script setup lang="ts">
import { ref, computed, provide, type Ref } from 'vue'import type { GridDragResizeProps, GridDragResizeItemProps } from './types'import GridDragResizeItem from './GridDragResizeItem.vue'const props = withDefaults(defineProps<GridDragResizeProps>(), {children: () => []
});const style = computed(() => {return {'grid-template-columns': Number.isInteger(props.columns) ? `repeat(${props.columns},${Number.isInteger(props.columnSize) ? `${props.columnSize}px` : '1fr'})` : '','grid-template-rows': Number.isInteger(props.rows) ? `repeat(${props.rows},${Number.isInteger(props.rowSize) ? `${props.rowSize}px` : '1fr'})` : '','grid-gap': Number.isInteger(props.gap) ? `${props.gap}px ${props.gap}px` : ''}
})const rootEle: Ref<HTMLElement | undefined> = ref()// 给子组件穿透转递组件 Props
provide('parentProps', props)// 组件位置、大小信息
const rootRect = computed(() => {return rootEle?.value?.getBoundingClientRect() ?? {height: 0,width: 0,x: 0,y: 0,bottom: 0,right: 0}
})// 列宽
const columnSize = computed(() => {return (rootRect.value.width - (props.gap ?? 0) * ((props.columns ?? 1) - 1)) / (props.columns ?? 1)
})// 行高
const rowSize = computed(() => {return (rootRect.value.height - (props.gap ?? 0) * ((props.rows ?? 1) - 1)) / (props.rows ?? 1)
})// 根据鼠标拖动偏移量,计算列/行方向上,移动后最新的位置和大小
function calcStartEnd(opts: { size: number, gap: number, span: number, max: number, offset: number, startBefore: number }) {let { size, gap, span, max, offset, startBefore } = optslet offsetStart = Math.round(offset / (size + gap))let start = startBefore + offsetStartif (start < 1) {start = 1}if (start + span > max) {start = max - span + 1}return {start,end: start + span}
}// 当前拖动小组件的数据项
const draggingChild: Ref<GridDragResizeItemProps | undefined> = ref()
// 当前拖动小组件的数据项(初始状态)
const draggingChildBefore: Ref<GridDragResizeItemProps | undefined> = ref()
// 当前拖动小组件的位置、大小信息
const draggingChildRect: Ref<DOMRect | undefined> = ref()// 拖动开始位置
let dragStartClientX = 0, dragStartClientY = 0;// 拖动偏移量
let dragOffsetClientX = 0, dragOffsetClientY = 0;let dragging = false// 开始拖动
function dragstart(e: MouseEvent) {if (!props.readonly) {dragging = true// 记录 拖动开始位置dragStartClientX = e.clientXdragStartClientY = e.clientY}
}// 拖动中
function drag(e: MouseEvent) {if (dragging && draggingChild.value && draggingChildRect.value) {// 计算 拖动开始位置dragOffsetClientX = e.clientX - dragStartClientXdragOffsetClientY = e.clientY - dragStartClientY// 当前拖动小组件的 grid 大小let rowSpan = (draggingChild.value.rowEnd ?? draggingChild.value.rowStart ?? 1) - (draggingChild.value.rowStart ?? 1)let columnSpan = (draggingChild.value.columnEnd ?? draggingChild.value.columnStart ?? 1) - (draggingChild.value.columnStart ?? 1)// 边界处理if (rowSpan <= 0) {rowSpan = 1}if (columnSpan <= 0) {columnSpan = 1}// 计算行方向上,移动后最新的位置和大小let { start: rowStart, end: rowEnd } = calcStartEnd({size: rowSize.value, gap: (props.gap ?? 0), span: rowSpan, max: props.rows ?? 1, offset: dragOffsetClientY, startBefore: draggingChildBefore.value?.rowStart ?? 1})// 计算列方向上,移动后最新的位置和大小let { start: columnStart, end: columnEnd } = calcStartEnd({size: columnSize.value, gap: (props.gap ?? 0), span: columnSpan, max: props.columns ?? 1, offset: dragOffsetClientX, startBefore: draggingChildBefore.value?.columnStart ?? 1})// 当前拖动小组件的数据项draggingChild.value.columnStart = columnStartdraggingChild.value.columnEnd = columnEnddraggingChild.value.rowStart = rowStartdraggingChild.value.rowEnd = rowEnd}
}// 拖动结束
function dragend(e: MouseEvent) {e.stopPropagation()dragging = falsedraggingChild.value = undefined
}// 超出组件区域,补充结束事件
document.body.addEventListener('mouseup', dragend)
</script><template>
<div class="grid-drag-resize" :style="style" @mousedown="dragstart" @mousemove="drag" @mouseup="dragend" ref="rootEle"><template v-for="(child, idx) of props.children" :key="idx"><GridDragResizeItem v-bind="child" v-model:column-start="child.columnStart" v-model:column-end="child.columnEnd"v-model:row-start="child.rowStart" v-model:row-end="child.rowEnd"@dragging="(rect) => { draggingChild = child; draggingChildBefore = { ...child }; draggingChildRect = rect }":style="{ 'zIndex': draggingChild === child ? props.children.length + 1 : idx + 1 }":class="{ 'grid-drag-resize__item--dragging': draggingChild === child }"><component :is="child.render"></component></GridDragResizeItem></template>
</div>
</template>
子组件
src/lib/components/GridDragResize/GridDragResizeItem.vue
逻辑说明,请留意代码注释
<script setup lang="ts">
import { ref, computed, watchEffect, inject, type Ref } from 'vue'import type { GridDragResizeProps, GridDragResizeItemProps } from './types'const parentProps = inject<GridDragResizeProps>('parentProps')const props = withDefaults(defineProps<GridDragResizeItemProps>(), {draggable: true
});const emit = defineEmits(['update:columnStart', 'update:columnEnd', 'update:rowStart', 'update:rowEnd', 'dragging'])// 数据整理
watchEffect(() => {if (props.columnStart !== void 0) {if (props.columnEnd === void 0 || props.columnEnd < props.columnStart) {emit('update:columnEnd', props.columnStart + 1)}} else {emit('update:columnStart', 1)}if (props.rowStart !== void 0) {if (props.rowEnd === void 0 || props.rowEnd < props.rowStart) {emit('update:rowEnd', props.rowStart + 1)}} else {emit('update:rowStart', 1)}
})// 样式
const style = computed(() => {return {'grid-column-start': props.columnStart,'grid-column-end': props.columnEnd,'grid-row-start': props.rowStart,'grid-row-end': props.rowEnd,}
})const itemEle: Ref<HTMLElement | undefined> = ref()const dragHandlerParsed = computed(() => props.dragHandler ?? parentProps?.dragHandler)
const draggableParsed = computed(() => parentProps?.readonly ? false : props.draggable)// dragHandler 定位、处理、事件绑定
watchEffect(() => {if (draggableParsed.value && dragHandlerParsed.value && itemEle.value) {const handlerEle = itemEle.value.querySelector(dragHandlerParsed.value)if (handlerEle instanceof HTMLElement) {handlerEle.style.cursor = 'grab'handlerEle.addEventListener('mousedown', dragstart)}}
})// 拖动开始
function dragstart() {if (draggableParsed.value) {// 通知父组件 当前拖动小组件emit('dragging', itemEle?.value?.getBoundingClientRect() ?? {height: 0,width: 0,x: 0,y: 0,bottom: 0,right: 0})}
}
</script><template>
<div class="grid-drag-resize__item" :class="{'grid-drag-resize__item--draggable': draggableParsed,'grid-drag-resize__item--draggable-full': draggableParsed && dragHandlerParsed === void 0
}" :style="style" @mousedown="() => dragHandlerParsed ? undefined : dragstart()" ref="itemEle"><slot></slot>
</div>
</template>
样式
.grid-drag-resize {display: grid;.grid-drag-resize__item {&--draggable-full {cursor: grab;user-select: none;}&--dragging {opacity: 0.6;}}
}
组件入口
// src/lib/components/GridDragResize/index.ts
import GridDragResize from './GridDragResize.vue'
import GridDragResizeItem from './GridDragResizeItem.vue'import './style.less'export * from './types'export { GridDragResize, GridDragResizeItem }
Thanks watching~
下一章,我们说说如何构建在线示例、组件库,及其如何发布到 NPM 上供开源使用!
More Stars please!勾勾手指~
github源码
示例地址
相关文章:
基于 CSS Grid 的简易拖拉拽 Vue3 组件,从代码到NPM发布(1)- 拖拉拽交互
基于特定的应用场景,需要在页面中以网格的方式,实现目标组件在网格中可以进行拖拉拽、修改大小等交互。本章开始分享如何一步步从代码设计,最后到如何在 NPM 上发布。 请大家动动小手,给我一个免费的 Star 吧~ 大家如果发现了 Bug…...
【华为HCIP实战课程六】OSPF邻居关系排错网络子网掩码问题,网络工程师
一、链路上网络和掩码引发的OSPF邻居问题 R3和R4已经建立正常的ospf邻居关系 更改IP地址前R3接口IP地址 interface Serial2/0/0 link-protocol ppp ip address 10.1.34.3 255.255.255.240 [R3-Serial2/0/0]ip address 10.1.88.2 255.255.255.240 更改为10.1.88.2 R3和R4虽…...
基础教程 | 用VuePress搭建一个简单的个人博客(附源码)
先附上自己个人博客页面:https://illusionno.github.io/ 源码也在这里:https://github.com/illusionno/my-blog (如果觉得有帮助,可以点颗star✨) 使用的主题是vuepress-theme-reco2.x,并在上面进行了一些调…...
Ubuntu20.04,编译安装BCC
https://github.com/iovisor/bcc/blob/master/INSTALL.md 一、内核配置 In general, to use these features, a Linux kernel version 4.1 or newer is required. In addition, the kernel should have been compiled with the following flags set: CONFIG_BPFy CONFIG_BP…...
# 显卡算力参数对比
显卡算力参数对比 文章目录 显卡算力参数对比A 显卡参数查询B 显卡性能对比: 综合看:T4最具性价比 A 显卡参数查询 查询网址:https://www.techpowerup.com/gpu-specs/ ,以下列出部分: Product NameGPU ChipReleasedB…...
掌握RocketMQ4.X消息中间件(一)-RocketMQ基本概念与系统架构
1 MQ介绍 MQ(Message Quene) : 翻译为 消息队列,别名为 消息中间件,通过典型的 生产者和消费者模型,生产者不断向消息队列中生产消息,消费者不断的从队列中获取消息。因为消息的生产和消费都是异步的,而且只关心消息的发送和接收,…...
实际开发中,java开发的准备工作
实际开发中,java开发的准备工作 一、IDEA工具环境设置 1、编码设置...
SQL进阶技巧:Order by 中 NULLS LAST特性使用?
目录 1 需求描述 2 数据准备 3 问题分析 4 小结 如果觉得本文对你有帮助,想进一步学习SQL语言这门艺术的,那么不妨也可以选择去看看我的博客专栏 ,部分内容如下: 数字化建设通关指南 专栏 原价99,现在活动价59…...
Redis:cpp.redis++类型操作
Redis:cpp.redis类型操作 stringsetmsetmgetgetrangesetrangeincrbydecrby listlpushrpushlrangellenlpoprpopblpopbrpop setsaddsmemeberssismemberscardspopsintersinterstore hashhsethgethexistshdelhkeyshvalshmsethmget zsetzaddzrangezcardzremzscorezrank 总…...
感冒用药记录
问题描述:国庆感冒了,头昏喉咙不舒服 用药过程: – 前3天:未用药,不好也不坏 – 中间2天:开始喉痛,使用复方氨酚烷胺胶囊【含对乙酰氨基酚】,基本没有效果 – 后面1天:开…...
JMeter性能测试时,如何做CSV参数化
在现代软件开发中,性能测试是保证应用程序在高负载条件下稳定运行的重要环节。为了实现真实场景的测试,参数化技术应运而生。其中,CSV参数化是一种高效且灵活的方法,可以让测试人员通过外部数据文件驱动测试脚本,从而模…...
爬虫获取不同数据类型(如JSON,HTML)的处理方法以及图片相对URL地址的转换
当我们爬取图片的URL地址时,我们要确保它们都是有效的绝对URL,这样就可以直接用这些URL来下载图片了。但是很多时候,它们都不是绝对URL地址,因此我们需要它进行URL转换。 if img_url.startswith(//): 这个条件检查URL是否以//开头…...
Elasticsearch 实战应用
Elasticsearch 实战应用 引言 Elasticsearch 是一个分布式、RESTful 风格的搜索和分析引擎,能够快速、实时地处理大规模数据,广泛应用于全文搜索、日志分析、推荐系统等领域。在这篇博客中,我们将从 Elasticsearch 的基本概念入手ÿ…...
前端数据加载慢的解决方法
都是和前端性能优化非常类似的做法。 1. 懒加载 (Lazy Loading) 对于图片、视频等资源,或者某些组件,在用户滚动到相关区域时再加载,而不是页面一开始就加载所有内容。使用 IntersectionObserver 实现懒加载,或者一些 UI 框架&am…...
探索MultiApp:一款强大的多应用管理工具
探索MultiApp:一款强大的多应用管理工具 在这个数字化时代,多任务并行已经成为我们日常生活的一部分。无论是工作还是娱乐,我们都需要频繁地在多个应用之间切换。今天,我要向大家介绍一款能够帮助你在同一设备上无缝切换和管理多…...
qt QGraphicsItem详解
一、概述 QGraphicsItem是Qt框架中图形视图框架(Graphics View Framework)的一个核心组件,它是用于表示2D图形元素的基类。 它支持的功能包括: 设置和获取图形项的位置和尺寸。控制图形项的外观,如颜色、笔刷、边框…...
LVS搭建负载均衡
LVS搭建负载均衡 引言 在现代互联网应用中,用户对服务的可用性和响应速度要求越来越高。为了应对高并发请求,保证系统的稳定性和容错能力,负载均衡技术应运而生。LVS(Linux Virtual Server)是一种高性能、高可用性的…...
Unity MVC框架演示 1-1 理论分析
本文仅作学习笔记分享与交流,不做任何商业用途,该课程资源来源于唐老狮 1.一般的图解MVC 什么是MVC我就不说了,老生常谈,网上有大量的介绍,想看看这三层都起到什么职责?那就直接上图吧 2.我举一个栗子 我有…...
基于springboot+vue人脸识别的考勤管理系统(源码+定制+开发)
博主介绍: ✌我是阿龙,一名专注于Java技术领域的程序员,全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师,我在计算机毕业设计开发方面积累了丰富的经验。同时,我也是掘金、华为云、阿里云、InfoQ等平台…...
【api连接ChatGPT的最简单方式】
通过api连接ChatGPT的最简单方式 建立client 其中base_url为代理,若连接官网可省略;配置环境变量 from openai import OpenAI client OpenAI(base_url"https://api.chatanywhere.tech/v1" )或给出api和base_url client OpenAI(api_key&…...
【Python】 -- 趣味代码 - 小恐龙游戏
文章目录 文章目录 00 小恐龙游戏程序设计框架代码结构和功能游戏流程总结01 小恐龙游戏程序设计02 百度网盘地址00 小恐龙游戏程序设计框架 这段代码是一个基于 Pygame 的简易跑酷游戏的完整实现,玩家控制一个角色(龙)躲避障碍物(仙人掌和乌鸦)。以下是代码的详细介绍:…...
论文解读:交大港大上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(二)
HoST框架核心实现方法详解 - 论文深度解读(第二部分) 《Learning Humanoid Standing-up Control across Diverse Postures》 系列文章: 论文深度解读 + 算法与代码分析(二) 作者机构: 上海AI Lab, 上海交通大学, 香港大学, 浙江大学, 香港中文大学 论文主题: 人形机器人…...
k8s业务程序联调工具-KtConnect
概述 原理 工具作用是建立了一个从本地到集群的单向VPN,根据VPN原理,打通两个内网必然需要借助一个公共中继节点,ktconnect工具巧妙的利用k8s原生的portforward能力,简化了建立连接的过程,apiserver间接起到了中继节…...
云原生玩法三问:构建自定义开发环境
云原生玩法三问:构建自定义开发环境 引言 临时运维一个古董项目,无文档,无环境,无交接人,俗称三无。 运行设备的环境老,本地环境版本高,ssh不过去。正好最近对 腾讯出品的云原生 cnb 感兴趣&…...
html css js网页制作成品——HTML+CSS榴莲商城网页设计(4页)附源码
目录 一、👨🎓网站题目 二、✍️网站描述 三、📚网站介绍 四、🌐网站效果 五、🪓 代码实现 🧱HTML 六、🥇 如何让学习不再盲目 七、🎁更多干货 一、👨…...
使用Spring AI和MCP协议构建图片搜索服务
目录 使用Spring AI和MCP协议构建图片搜索服务 引言 技术栈概览 项目架构设计 架构图 服务端开发 1. 创建Spring Boot项目 2. 实现图片搜索工具 3. 配置传输模式 Stdio模式(本地调用) SSE模式(远程调用) 4. 注册工具提…...
Netty从入门到进阶(二)
二、Netty入门 1. 概述 1.1 Netty是什么 Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients. Netty是一个异步的、基于事件驱动的网络应用框架,用于…...
2025年渗透测试面试题总结-腾讯[实习]科恩实验室-安全工程师(题目+回答)
安全领域各种资源,学习文档,以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具,欢迎关注。 目录 腾讯[实习]科恩实验室-安全工程师 一、网络与协议 1. TCP三次握手 2. SYN扫描原理 3. HTTPS证书机制 二…...
宇树科技,改名了!
提到国内具身智能和机器人领域的代表企业,那宇树科技(Unitree)必须名列其榜。 最近,宇树科技的一项新变动消息在业界引发了不少关注和讨论,即: 宇树向其合作伙伴发布了一封公司名称变更函称,因…...
C语言中提供的第三方库之哈希表实现
一. 简介 前面一篇文章简单学习了C语言中第三方库(uthash库)提供对哈希表的操作,文章如下: C语言中提供的第三方库uthash常用接口-CSDN博客 本文简单学习一下第三方库 uthash库对哈希表的操作。 二. uthash库哈希表操作示例 u…...
