基于 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, '&nbsp; ')"></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源码

示例地址

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/444015.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

探索未来:mosquitto-python,AI领域的新宠

文章目录 探索未来&#xff1a;mosquitto-python&#xff0c;AI领域的新宠背景&#xff1a;为何选择mosquitto-python&#xff1f;库简介&#xff1a;mosquitto-python是什么&#xff1f;安装指南&#xff1a;如何安装mosquitto-python&#xff1f;函数用法&#xff1a;5个简单…

代码随想录算法训练营第四十六天 | 647. 回文子串,516.最长回文子序列

四十六天打卡&#xff0c;今天用动态规划解决回文问题&#xff0c;回文问题需要用二维dp解决 647.回文子串 题目链接 解题思路 没做出来&#xff0c;布尔类型的dp[i][j]&#xff1a;表示区间范围[i,j] &#xff08;注意是左闭右闭&#xff09;的子串是否是回文子串&#xff0…

深入理解Transformer的笔记记录(精简版本)---- Transformer

自注意力机制开启大规模预训练时代 1 从机器翻译模型举例 1.1把编码器和解码器联合起来看待的话,则整个流程就是(如下图从左至右所示): 1.首先,从编码器输入的句子会先经过一个自注意力层(即self-attention),它会帮助编码器在对每个单词编码时关注输入句子中的的其他单…

【JavaEE】——回显服务器的实现

阿华代码&#xff0c;不是逆风&#xff0c;就是我疯 你们的点赞收藏是我前进最大的动力&#xff01;&#xff01; 希望本文内容能够帮助到你&#xff01;&#xff01; 目录 一&#xff1a;引入 1&#xff1a;基本概念 二&#xff1a;UDP socket API使用 1&#xff1a;socke…

2-118 基于matlab的六面体建模和掉落仿真

基于matlab的六面体建模和掉落仿真&#xff0c;将对象建模为刚体来模拟将立方体扔到地面上。同时考虑地面摩擦力、刚度和阻尼所施加的力&#xff0c;在三个维度上跟踪平移运动和旋转运动。程序已调通&#xff0c;可直接运行。 下载源程序请点链接&#xff1a;2-118 基于matla…

基于SpringBoot“花开富贵”花园管理系统【附源码】

效果如下&#xff1a; 系统注册页面 系统首页界面 植物信息详细页面 后台登录界面 管理员主界面 植物分类管理界面 植物信息管理界面 园艺记录管理界面 研究背景 随着城市化进程的加快和人们生活质量的提升&#xff0c;越来越多的人开始追求与自然和谐共生的生活方式&#xf…

使用激光跟踪仪提升码垛机器人精度

标题1.背景 码垛机器人是一种用于工业自动化的机器人&#xff0c;专门设计用来将物品按照一定的顺序和结构堆叠起来&#xff0c;通常用于仓库、物流中心和生产线上&#xff0c;它们可以自动执行重复的、高强度的搬运和堆垛任务。 图1 码垛机器人 传统调整码垛机器人的方法&a…

通信工程学习:什么是DIP数据集成点

DIP&#xff1a;数据集成点 DIP数据集成点&#xff08;Data Integration Point&#xff09;&#xff0c;简称DIP&#xff0c;是物联网技术&#xff08;IoT&#xff09;和机器到机器&#xff08;M2M&#xff09;通信中的一个重要组成部分。DIP在数据集成和传输过程中扮演着关键角…

【笔记】6.2 玻璃的成型

玻璃熔体的成型方法,有压制法(例如,制作水杯、烟灰缸等)、压延法(例如,制作压花玻璃等)、浇铸法(例如,制作光学玻璃、熔铸耐火材料、铸石等) 、吹制法(例如,制作瓶罐等空心玻璃)、拉制法(例如,制作窗用玻璃、玻璃管、玻璃纤维等)、离心法(例如,制作玻璃棉等)、喷吹法(例如,制作…

Ansible 工具从入门到使用

1. Ansible概述 Ansible是一个基于Python开发的配置管理和应用部署工具&#xff0c;现在也在自动化管理领域大放异彩。它融合了众多老牌运维工具的优点&#xff0c;Pubbet和Saltstack能实现的功能&#xff0c;Ansible基本上都可以实现。 Ansible能批量配置、部署、管理上千台主…

各类排序详解

前言 本篇博客将为大家介绍各类排序算法&#xff0c;大家知道&#xff0c;在我们生活中&#xff0c;排序其实是一件很重要的事&#xff0c;我们在网上购物&#xff0c;需要根据不同的需求进行排序&#xff0c;异或是我们在高考完报志愿时&#xff0c;需要看看院校的排名&#…

qt QGraphicsItem详解

一、概述 QGraphicsItem是Qt框架中图形视图框架&#xff08;Graphics View Framework&#xff09;的一个核心组件&#xff0c;它是用于表示2D图形元素的基类。 它支持的功能包括&#xff1a; 设置和获取图形项的位置和尺寸。控制图形项的外观&#xff0c;如颜色、笔刷、边框…

京东web 京东e卡绑定 第二部分分析

声明 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01; 有相关问题请第一时间头像私信联系我删…

请求参数中字符串的+变成了空格

前端请求 后端接收到的结果 在URL中&#xff0c;某些字符&#xff08;包括空格、、&、? 等&#xff09;需要被编码。具体而言&#xff0c;在URL中&#xff0c;空格通常被编码为 或 %20。因此&#xff0c;如果你在请求参数中使用 &#xff0c;它会被解释为一个空格。 如果…

2024重生之回溯数据结构与算法系列学习(12)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丟脸好嘛?】

欢迎各位彦祖与热巴畅游本人专栏与博客 你的三连是我最大的动力 以下图片仅代表专栏特色 [点击箭头指向的专栏名即可闪现] 专栏跑道一 ➡️ MYSQL REDIS Advance operation 专栏跑道二➡️ 24 Network Security -LJS ​ ​ ​ 专栏跑道三 ➡️HCIP&#xff1b;H3C-SE;CCIP——…

智能边缘计算 | 项目快速部署指南

在数字化浪潮的推动下&#xff0c;边缘计算与人工智能的深度融合正在成为推动智能社会发展的新动力。 边缘计算通过将数据处理和分析任务从中心服务器转移到更接近数据源的端侧&#xff0c;从而显著降低数据传输延迟&#xff0c;提高了响应速度和安全隐私性。在人工智能的加持…

python的特殊方法——魔术方法

前言 __init__(self[]) ​编辑 __call__(self [, ...]) __getitem__(self, key) __len__(self) __repr__(self) / __str__(self) __add__(self, other) __radd__(self, other) 参考文献 前言 官方定义好的&#xff0c;以两个下划线开头且以两个下划线结尾来命名的方法…

在QT中将Widget提升为自定义的Widget后,无法设置Widget的背景颜色问题解决方法

一、问题 在Qt中将QWidget组件提升为自定义的QWidget后&#xff0c;Widget设置的样式失效&#xff0c;例如设置背景颜色为白色失效。 二、解决方法 将已经提升的QWidget实例对象&#xff0c;脱离父窗体的样式&#xff0c;然后再重新设置自己的样式。

[ComfyUI]太赞了!阿里妈妈发布升级版 Flux 图像修复模型,更强细节生成,更高融合度以及更大分辨率支持

小伙伴们还记得我们之前介绍的阿里妈妈发布的 Flux 的 ControlNet 图像修复模型不&#xff0c;之前发布的是 Alpha 早期测试版本&#xff0c;说实话和 Flux 原生的重绘其实差距不大&#xff0c;有些方面甚至还是原生的效果更好。 但是现在&#xff0c;Alpha 的升级版本 Beta 版…

Stable Diffusion绘画 | 签名、字体、Logo设计

第1步&#xff0c;使用 PS&#xff08;小白推荐使用 可画&#xff09;准备一个 512*768 的签名、字体、Logo图片&#xff1a; 第2步&#xff0c;来到模型网站&#xff0c;搜索&#x1f50d;关键词“电商”&#xff0c;找到一款喜欢的 LoRA&#xff1a; 第3步&#xff0c;选择一…