• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

低代码信创开发核心技术二手撕灵活Vue拖拉拽布局系统

武飞扬头像
杨若瑜
帮助5

前言

随着信息化时代的到来,软件已经成为企业和个人不可或缺的工具。然而,许多人在开发软件时遇到了各种问题,比如开发周期长、技术门槛高、成本高昂等等。为了解决这些问题,低代码平台应运而生。低代码平台是一种快速开发工具,它可以帮助开发者快速构建应用程序,从而提高开发效率和降低成本。

近年来,国产软件市场蓬勃发展,越来越多的企业开始关注自主创新和信息化发展。低代码平台作为信息化创新的重要工具,也逐渐受到了广泛关注。同时,随着Vue.js等前端框架的普及,拖拉拽布局系统也成为了低代码平台的核心技术之一。灵活好用的拖拉拽布局系统,能够帮助开发者快速搭建界面,提高开发效率。

在这篇博客中,我们将深入探究基于Vue.js的拖拽布局的实现方法,以及如何使用它来快速构建应用程序的界面。

效果预览

学新通
通过动图我们可以了解,这个布局功能已经可以实现在元素前添加、元素后添加、元素内添加,并能支持复杂的父子关系嵌套的添加。
学新通
甚至结合Ant Design Vue的选项卡,也可以轻松使用。

知识储备

1、您需要先掌握Vue.js 3.0版本的相关知识。
2、您需要先了解浏览器内置的window对象、事件驱动机制和拖拽相关的API。
3、您需要先了解HTML5和CSS3的相关知识。

整体界面布局

学新通

这里我们设想左边是一个工具栏,通过拖拽到中间工作空间可以把控件拖拽上去,与此同时未来在选中控件之后,在右侧还可以设置控件的相关属性。因为写这篇博客的时候没有找美工,所以尽管丑了点,功能还是设想的比较全面的。

这里我们用FrontendBlocks设计一下,一键生成出来界面布局代码:

文件名:Designer.vue

<template>
	<div class="root">
		<div class="DesignerControls">
			<div class="BtnControl">Label</div>
		</div>
		<div class="DesignerWorkSpace">
		</div>
		<div class="DesignerAttrs">
			<div class="attrItem">
				<div class="attrName">属性名</div>
				<input class="attrValue" />
			</div>
		</div>
	</div>
</template>

<script>
	export default {
		props: ['id', 'text', 'context', 'initData'],
		data() {
			return {}
		},
		mounted() {
			this.context.initControl(this)
		}
	}
</script>

<style>
	html,
	body,
	.root {
		padding: 0;
		margin: 0;
		width: 100%;
		height: 100%;
	}

	.root {
		box-sizing: border-box;
		font-weight: normal;
		font-style: normal;
		text-align: left;
		display: flex;
		position: relative;
		width: 100%;
		flex-direction: row;
		justify-content: flex-start;
		align-items: flex-start;
	}

	.DesignerControls {
		box-sizing: border-box;
		background-color: rgba(243, 243, 243, 1);
		font-weight: normal;
		font-style: normal;
		text-align: left;
		flex-shrink: 0;
		display: flex;
		position: relative;
		width: 180px;
		height: 100%;
		padding: 8px;
		flex-direction: column;
		justify-content: flex-start;
		align-items: flex-start;
		overflow: hidden scroll;
	}

	.BtnControl {
		box-sizing: border-box;
		border: 1px solid rgba(0, 0, 0, 1);
		border-top: 1px solid rgba(0, 0, 0, 1);
		border-bottom: 1px solid rgba(0, 0, 0, 1);
		border-left: 1px solid rgba(0, 0, 0, 1);
		border-right: 1px solid rgba(0, 0, 0, 1);
		font-size: 12px;
		font-weight: normal;
		font-style: normal;
		text-align: left;
		display: flex;
		position: relative;
		width: 100%;
		padding: 8px;
		margin: 0px 0px 8px 0px;
		flex-direction: column;
		justify-content: flex-start;
		align-items: flex-start;
	}

	.DesignerWorkSpace {
		box-sizing: border-box;
		font-weight: normal;
		font-style: normal;
		text-align: left;
		position: relative;
		width: 100%;
		height: 100%;
		flex-direction: column;
		justify-content: flex-start;
		align-items: flex-start;
	}

	.DesignerAttrs {
		box-sizing: border-box;
		background-color: rgba(243, 243, 243, 1);
		font-weight: normal;
		font-style: normal;
		text-align: left;
		flex-shrink: 0;
		display: flex;
		position: relative;
		width: 240px;
		height: 100%;
		flex-direction: column;
		justify-content: flex-start;
		align-items: flex-start;
		overflow: hidden scroll;
	}

	.attrItem {
		box-sizing: border-box;
		border: 1px solid rgba(167, 167, 167, 1);
		border-top: none;
		border-bottom: 1px solid rgba(167, 167, 167, 1);
		border-left: none;
		border-right: none;
		font-weight: normal;
		font-style: normal;
		text-align: left;
		display: flex;
		position: relative;
		width: 100%;
		padding: 4px;
		flex-direction: row;
		justify-content: flex-start;
		align-items: center;
	}

	.attrName {
		box-sizing: border-box;
		font-size: 12px;
		font-weight: normal;
		font-style: normal;
		text-align: left;
		flex-shrink: 0;
		display: flex;
		position: relative;
		width: 30%;
		flex-direction: column;
		justify-content: flex-start;
		align-items: flex-start;
	}

	.attrValue {
		box-sizing: border-box;
		border: 1px solid rgba(0, 0, 0, 1);
		border-top: 1px solid rgba(0, 0, 0, 1);
		border-bottom: 1px solid rgba(0, 0, 0, 1);
		border-left: 1px solid rgba(0, 0, 0, 1);
		border-right: 1px solid rgba(0, 0, 0, 1);
		font-weight: normal;
		font-style: normal;
		text-align: left;
		display: flex;
		position: relative;
		width: 100%;
		padding: 5px;
		flex-direction: column;
		justify-content: flex-start;
		align-items: flex-start;
	}
</style>
学新通

JavaScript拖拽事件简述

整体拖拽流程如下:
学新通

给需要拖拽的元素绑定 dragstart 事件,该事件在开始拖拽时触发。在该事件中,我们可以设置拖拽数据和拖拽效果。
当鼠标移动到其他元素上时,会触发 dragenter 事件。在该事件中,我们可以设置拖拽目标元素的样式,以反馈给用户当前的拖拽位置。
接着,dragover 事件会持续触发,直到拖拽元素离开了拖拽目标元素。在该事件中,我们可以阻止默认行为,以便能够将元素放置到拖拽目标元素上。
当拖拽元素被放置到拖拽目标元素上时,会触发 drop 事件。在该事件中,我们可以获取拖拽数据,并进行相应的处理操作。
最后,当拖拽完成时,会触发 dragend 事件。在该事件中,我们可以进行一些清理工作,比如重置拖拽元素的样式。

需要注意的是:
拖拽过程中,需要设置拖拽元素和拖拽目标元素的 draggable 属性为 true。
在 dragover 事件中,需要阻止默认行为,以便能够将元素放置到拖拽目标元素上。
在 drop 事件中,需要阻止默认行为,并且需要确保拖拽元素和拖拽目标元素都支持相应的拖拽类型。
在 dragstart 事件中,可以设置拖拽数据和拖拽效果。在 drop 事件中,可以获取拖拽数据,并进行相应的处理操作。
在 dragend 事件中,需要重置拖拽元素的样式,并进行一些清理工作。

是不是很简单呢?

控件栏发起拖拽事件

接下来我们稍作改造,先把Label那个可以拖拽的按钮改成由JSON维护的:

<div v-for="(item) in controlTools" class="BtnControl"
    draggable="true"
    @dragstart="dragStart($event,item.value)"
	@dragend="dragLeaveWorkSpace($event)">
	{{item.name}}
</div>
		data() {
			return {
				controlTools:[{
					name:"Label",
					value:'文本说明'
				},{
					name:"TextBox",
					value:'<input type="text" />'
				},{
					name:"Button",
					value:'<button type="button">搜索</button>'
				}]
			}
		}

这里的我们定义了两个事件处理,一个是dragStart,另一个是dragLeaveWorkSpace。先在methods里把这两个函数实现了,不过我写的时候也不知道dragLeaveWorkSpace应该处理什么,反正先留着空:

dragStart(e,data) {
	e.dataTransfer.setData("content", data);
},
dragLeaveWorkSpace(e) {
	e.preventDefault();
}

然后为了方便测试,我们把工作区改造一下:

<DropTarget class="DesignerWorkSpace">
	<DropTarget class="DemoBox">
		<div class="DemoBox">
			<DropTarget class="DemoBox">
				<div class="DemoBox">
					<DropTarget class="DemoBox">
					</DropTarget>
				</div>
			</DropTarget>
		</div>
	</DropTarget>
	<div class="DemoBox" style="display: flex;flex-direction: row;margin-top: 10px;">
		<DropTarget class="DemoBox" style="width: 100%;padding: 20px;"></DropTarget>
		<DropTarget class="DemoBox" style="width: 100%;padding: 20px;"></DropTarget>
		<DropTarget class="DemoBox" style="width: 100%;padding: 20px;"></DropTarget>
		<DropTarget class="DemoBox" style="width: 100%;padding: 20px;"></DropTarget>
		<DropTarget class="DemoBox" style="width: 100%;padding: 20px;"></DropTarget>
	</div>
</DropTarget>
学新通

把DemoBox的样式写上

.DemoBox {
	border: 1px solid #a3a3a3;
	padding: 50px;
	box-sizing: border-box;
	position: relative;
}

实现DropTarget 可拖放区域

我们写个新组件DropTarget.vue,因为上面工作区我们要往里放子元素,所以自然就要用到slot。

<div class="DropTarget"
	@dragenter="dragEnter($event)"
	@dragover="dragOver($event)"
	@drop="drop($event)">
	<slot></slot>
</div>

然后给DropTarget上个背景色,这个不是必须的

.DropTarget {
	background-color: #96969632;
}

接下来写dragEnter方法,考虑到有可能有子元素父元素之间的关系处理,那么就先写个递归函数放methods里,向上不断找父级,只要碰到class里有DropTarget的,那一定是目标组件。

calcRealTarget(element) {
	let target = element;
	if (target.classList.contains("DropTarget")) {
		return target;
	}
	if (!element.parentElement) return null;
	return this.calcRealTarget(element.parentElement)
}

然后我们正式开始写dragEnter方法:

dragEnter(e) {
	let target = this.calcRealTarget(e.target)
	if (target == null) return false;
	e.preventDefault();
	e.stopPropagation();
	// 处理添加
	target.originBgColor = target.style.backgroundColor
	target.style.outline = "1px dashed #74c3ff"
	target.style.outlineOffset = "-1px"
	target.classList.add('draging')
	if (window.currentDropTarget && window.currentDropTarget != target) {
		let oldDropTarget = window.currentDropTarget
		oldDropTarget.style.backgroundColor = oldDropTarget.originBgColor
		oldDropTarget.style.outline = null
		oldDropTarget.classList.remove('draging')
	}
	window.currentDropTarget = target
}
学新通

这里有个技巧,使用outline样式,相比于border来说,它不会因为占文档流的像素而使界面位置发生位移,特别是复杂界面处理的过程中,向外差一两个像素都是灾难性的。这里outlineOffset设置-1px,可以有效防止和边框border重叠,观感更好。
代码中,我们每次DragEnter事件中都会往window对象写一个属性currentDropTarget,把当前对象传过去。这里判断一遍,主要是为了防止鼠标从父容器移动到子容器的时候会再次触发DragEnter事件,为了不出现卡顿,展现出丝般顺滑的感觉,这里需要判断一下,一旦发生了同级别之间的目标转移,则立即把前一个组件的样式清掉。
为什么我这里不用dragLeave事件呢?那是因为鼠标拖拽划过父子容器的时候势必会触发一次dragLeave,从而导致样式被莫名清空。

为了配合鼠标动作,有一个直观的展现,那么这里我们用伪类做一个不会吃掉鼠标事件的半透明遮罩,测试一下,瞬间这感觉就上来了。不过要注意的是,可拖放区域一定要显式声明position样式,否则这个遮罩会超出边界。

.draging::before {
	content: ' ';
	width: 100%;
	height: 100%;
	left: 0px;
	top: 0px;
	opacity: 0.3;
	position: absolute;
	background-color: #74c3ff;
	z-index: 99999;
	pointer-events: none;
}

接下来我们编写dragOver事件的函数:

dragOver(e) {
	let target = this.calcRealTarget(e.target)
	if (target == null) return false;

	// 判断什么时候可以显示排序:1、当前事件对象不是容器 2、当前事件对象虽然是容器,但父组件也是容器
	let showOrderLine = false;
	if (target != e.target) {
		showOrderLine = true;
	}
	if (target.parentElement.classList.contains("DropTarget")) {
		showOrderLine = true
	}

	// 清除原有提示状态
	if (window.currentDropBefore && window.currentDropBefore != e.target) {
		let oldDropBefore = window.currentDropBefore
		oldDropBefore.style.borderInlineStart = null
		window.currentDropBefore = null;
	}
	if (window.currentDropAfter && window.currentDropAfter != e.target) {
		let oldDropAfter = window.currentDropAfter
		oldDropAfter.style.borderInlineEnd = null
		window.currentDropAfter = null
	}
	// 需要显示则显示
	if (showOrderLine) {
		if (e.offsetX < (e.target.offsetWidth * 0.25)) {
			e.target.style.borderInlineStart = "2px solid #ff6600"
			window.currentDropBefore = e.target
			if (window.currentDropAfter) {
				window.currentDropAfter.style.borderInlineEnd = null
			}
		} else if (e.offsetX > (e.target.offsetWidth * 0.75)) {
			e.target.style.borderInlineEnd = "2px solid #ff6600"
			window.currentDropAfter = e.target
			if (window.currentDropBefore) {
				window.currentDropBefore.style.borderInlineStart = null
			}
		} else {
			e.target.style.borderInlineStart = null
			e.target.style.borderInlineEnd = null
			window.currentDropBefore = null
			window.currentDropAfter = null
		}
	}
	e.preventDefault();
},
学新通

因为我们在实现拖放的同时还要实现排序,所以这里我们就约定一个可拖放区域的左侧25%的区域是同级别向前插入一个元素,右侧25%区域是向同级别后面追加一个元素,只有中间的50%区域是向其中填充子元素,如果前后无需排序,则整片区域都是当做添加子元素,鼠标拖拽到哪个区域,便会有对应区域的样式展示,这里使用borderInlineStart和borderInlineEnd可以很方便的展现样式,为什么用到Inline呢?这是因为outline不支持分别设置外框线。
然后我们实现drop方法:

drop(e) {
	let target = this.calcRealTarget(e.target)
	if (target == null) return false;
	e.preventDefault();
	e.stopPropagation();
	let data = e.dataTransfer.getData('content');

	// 清除因拖拽产生的样式
	if (window.currentDropTarget) {
		window.currentDropTarget.style.backgroundColor = window.currentDropTarget.originBgColor
		window.currentDropTarget.style.outline = null
		window.currentDropTarget.classList.remove('draging')
	}
	if (window.currentDropBefore) window.currentDropBefore.style.borderInlineStart = null
	if (window.currentDropAfter) window.currentDropAfter.style.borderInlineEnd = null

	// 完成拖拽操作并添加DOM元素
	// TODO:这里需要修改一下,我们可以通过DOM元素的.__vnode.ctx.proxy属性获取到VUE的vnode对象
	// 或者是通过VUE当前组件的.$el和实际拿到的DOM元素进行匹配。两种方式都可以找到VUE的代理对象
	// 通过代理对象,可以将这里面的slot换成通过数组来维护,通过DDR方式,递归将JSON渲染成组件
	// 如果不了解数组和控件系统的思想,可以看上一篇文章
	let newNode=document.createElement("div")
	newNode.innerHTML=data
	if (window.currentDropBefore) {
		window.currentDropBefore.parentElement.insertBefore(newNode, window.currentDropBefore)
	} else if (window.currentDropAfter) {
		window.currentDropAfter.after(newNode)
	} else {
		window.currentDropTarget.appendChild(newNode)
	}
}
学新通

dragEnd事件处理

最后就是我们回到Designer.vue里,照着刚才的drag最后清理的逻辑,当拖拽结束时还原样式。这里后续要做二次拖拽(工具栏拖拽到工作区,从工作区一个组件拖拽到另一个组件里),我们可以在这个事件处理中销毁原组件。

dragLeaveWorkSpace(e) {
	e.preventDefault();
	if (window.currentDropTarget) {
		let oldDropTarget = window.currentDropTarget
		oldDropTarget.style.backgroundColor = oldDropTarget.originBgColor
		oldDropTarget.style.outline = null
		oldDropTarget.classList.remove('draging')
		window.currentDropTarget = null
	}
}

总结

本文主要介绍了基于Vue.js的拖拽布局的实现方法和如何使用它来快速构建应用程序的界面。深入探究了基于Vue.js的拖拽布局的实现方法,并展示了其效果预览和整体界面布局。最后,通过包括Vue.js 3.0版本、浏览器内置的window对象、事件驱动机制、拖拽相关API非常简单的实现了拖拽布局机制。
当我们能够创造出快速完成布局的系统之后,我们就可以结合上文所说的控件系统完成控件属性的设置、生成JSON代码保存到后台,然后再从后台读出JSON来渲染界面。
当我们能够快速批量的制造页面之后,就可以开始考虑结合后台整体实现模型驱动架构(MDA:Model Driven Architecture,它是一种软件设计方法论,通过将系统的业务逻辑和技术实现分离,将系统的关注点从技术层面转移到业务层面,提高了软件的可维护性和可重用性。在MDA架构中,模型是软件开发的核心,程序员通过定义模型来描述系统的业务逻辑和功能需求,然后使用模型转换工具将模型转换成最终的代码。),从而向使用部门或客户提供能够支撑其完成信息化创新的基础工具。

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhfhjfja
系列文章
更多 icon
同类精品
更多 icon
继续加载