对应的视频教程:点击进入
前言
都2021年了,作为前端如果还不了解可视化布局的话,赶紧上车吧。下面就是我这次要分享的系统的效果图:
代码实现
一、初始化项目
1、创建文件夹、然后终端进入文件夹,并执行vue create my-app
2、启动项目
二、修改目录和搭建系统框架
1、去掉页面的默认内容
2、新建目录:view/components/templates
3、创建四个视图组件:headView.vue / leftView.vue /centerView.vue / rightView.vue
4、将视图组件引入App.vue中,布局调整
三、创建组件库
1、创建一个文本组件
2、导入并且全局注册组件
import textComp from './textComp'
const obj = {
textComp
}
function install(Vue){
Object.keys(obj).forEach(key=>{
Vue.component(key,obj[key])
})
}
const API = {
version:'1.0',
install
}
export default API
3、测试是否注册成功
四、创建处理器
1、创建对应组件的处理器[将组件的属性、数据、模板整理后对外暴露]
import getAttr from "../utils";
const handle = ()=>{
// 属性
let attribute = [{
name:"宽度",
type:'input',
key:'width',
value:100,
placeholder:'请输入宽度'
},{
name:'高度',
type:'input',
key:'height',
value:40,
placeholder:'请输入高度'
},{
name:'字体大小',
type:'input',
key:'fontSize',
value:15,
placeholder:'请输入字体大小'
},{
name:'字体颜色',
type:'color',
key:'color',
value:'red',
placeholder:'请选择字体颜色'
}]
// 数据
let data = {txt:'这里是默认文本'}
// 模板
let template = `<textComp ${getAttr(attribute)} data=${data.txt}></textComp>`
return {attribute,data,template}
}
export default handle
2、整合处理器统一对外暴露
import textComp from './textComp'
let obj = {
textComp
}
export default getComponent = (info)=>{
let component = obj[info.type]()
return component;
}
五、实现左侧组件菜单
1、定义菜单数据
const menuData = [{
type:'textComp',
name:'文本组件'
}]
export default menuData
2、实现菜单
<template>
<div class="wrapper">
<div v-for="(item,index) in menuData" class="menuItem">{{item.name}}</div>
</div>
</template>
<script>
import menuData from './menuList'
export default {
components: {
},
props: {
},
data() {
return {
menuData:menuData
};
},
computed: {
},
created() {
},
mounted() {
},
methods: {
},
watch: {
},
};
</script>
<style scoped lang="less">
.wrapper{
width: 243px;
padding:8px;
.menuItem{
background: #eee;
width:100px;
height:100px;
float:left;
margin:10px;
text-align: center;
line-height: 100px;
cursor:pointer;
}
}
</style>
六、实现拖拽组件到画布
1、让菜单中的组件可拖拽:
给要拖拽的组件加上属性:draggable=“true”
2、将拖拽的组件信息传递给画布:
(1)给菜单项加挂载组件类型和名称
:data-type=“item.type”
:data-name=“item.name”
(2)给菜单项加上拖拽事件
@dragstart=“dragStart”
dragStart(e) {
let info = {
name: e.target.getAttribute("data-name"),
type: e.target.getAttribute("data-type"), };
e.dataTransfer.setData("info", JSON.stringify(info)); },
3、画布接受传递的数据:
(1)定义两个事件:
@dragover=“dragover”
@drop=“drop”
(2)在拖到画布,阻止默认行为,也就是让拖拽的元素可以让画布接收到
e.preventDefault();
(3)获得传递的数据
e.preventDefault();
let info = JSON.parse(e.dataTransfer.getData(“info”));
4、实现组件的挂载
(1)定义一个数组装当前拖拽到页面的组件
components:[],
(2)给组件设置id
info.id = genId();
(3)获得组件
let component = getTemplate(info);
(4)设置组件的位置, 将拖拽的组件放入数组中
let widthItem = component.attribute.find((item) => item.key === "width");
let heightItem = component.attribute.find((item) => item.key === "height" );
const width = widthItem.value;
const height = heightItem.value;
let left = e.offsetX - width / 2;
let top = e.offsetY - height / 2;
if (left < 0) left = 0;
if (top < 0) top = 0;
const zIndex = this.zIndex++;
component.position = { offsetLeft: left, offsetTop: top, zIndex: zIndex, };
this.components.push(component);
(5)设置组件的挂载点
<div :id="item.info.id" v-for="(item, index) in components"></div>
(6)挂载组件
import Vue from "vue";
const mountComponent = function(component) {
// 整理data数据
let data = {}
let id = component.info.id
let widthItem = component.attribute.find((item)=>item.key==='width')
let heightItem = component.attribute.find((item)=>item.key==='height')
if(component.attribute){
component.attribute.forEach((item,index,arr)=>{data[item.key] = item.value })
}
setTimeout(() => {
let vm = new Vue({
name: id.toString(),
data() {return data; },
template: component.template,
el: document.getElementById(id),
mounted() {
this.$el.id = id;
this.$el.style.position = "absolute";
this.$el.style.cursor = "pointer";
this.$el.style.left = `${component.position.offsetLeft}px`;
this.$el.style.top = `${component.position.offsetTop}px`;
this.$el.style.zIndex = `${component.position.zIndex}`;
this.$el.style.width = `${widthItem.value}px`;
this.$el.style.height = `${heightItem.value}px`; }});
}, 200);
};
export default mountComponent;
七、实现组件的自由移动
1、给画布组件添加选中效果
<!-- 组件选中效果 -->
<div class="borderStyle" v-if="currComp" :style="setStyleOfBorder"></div>
处理选中组件:
// 点击选中组件
checkComp(e) {
// 这里有个技巧,就是循环找到我们点击的组件,这里使用的正则匹配
let reg = /\w{8}-\w{4}/;
let node = e.target;
let count = 0;
// 还有node ,且node的id不是组件的id,就继续寻找
while (node && !reg.test(node.id)) {
count++;
node = node.parentNode;
}
// 获得匹配到的组件的id
if (node && node.id) {
this.currComp = this.components.find((item) => {
if (item.info.id === node.id) {
return item;
}
});
} else {
this.currComp = null;
}
},
设置选中样式:
setStyleOfBorder() {
this.currComp;
let width = 0;
let height = 0;
let left = 0;
let top = 0;
let zIndex = 0
if (this.currComp) {
this.currComp.attribute.forEach((item) => {
if (item.key === "width") {
width = item.value;
}
if (item.key === "height") {
height = item.value;
}
left = this.currComp.position.left;
top = this.currComp.position.top;
zIndex = this.currComp.position.zIndex+1
});
} else {
}
return {
width: `${width}px`,
height: `${height}px`,
left: `${left}px`,
top: `${top}px`,
zIndex:`${zIndex}px`
};
},
2、实现自由移动
//一、设置鼠标按下事件回调
@mousedown='mouseDownStart'
// 二、鼠标按下回调
mouseDownStart(e){
// 记录鼠标按下瞬间的位置
this.startPosition.x = e.clientX;
this.startPosition.y = e.clientY;
// 注册鼠标移动和鼠标松开的事件
document.addEventListener('mousemove',this.mouseMove,true)
document.addEventListener('mouseup',this.mouseUp,true)
},
//三、 鼠标移动事件
mouseMove(e){
// 计算偏移量
let offsetX = e.clientX - this.startPosition.x;
let offsetY = e.clientY - this.startPosition.y;
// 设置组件的位置
let com = document.getElementById(this.currComp.info.id)
Object.assign(com.style,{
left:this.currComp.position.left + offsetX+'px',
top:this.currComp.position.top + offsetY+'px'
})
// 设置选中框位置
let borderComp = document.getElementById('borderBox')
Object.assign(borderComp.style,{
left:this.currComp.position.left + offsetX+'px',
top:this.currComp.position.top + offsetY+'px'
})
},
//四、鼠标松开事件
mouseUp(e){
// 移出事件
document.removeEventListener('mousemove',this.mouseMove,true)
document.removeEventListener('mouseup',this.mouseUp,true)
// 更新组件的数据
this.currComp.position.left = this.currComp.position.left +(e.clientX - this.startPosition.x )
this.currComp.position.top = this.currComp.position.top+(e.clientY - this.startPosition.y )
},
3、边界限制
逻辑: 画布宽度- 组件宽度 = 最大的x偏移
画布高度 - 组件高度 = 最大的y偏移
// 限制边界
boundaryLimit(type, num, comp) {
// 计算出边界值
let canvas = document.getElementById("canvasBox");
let canvasWidth = canvas.clientWidth;
let canvasHeight = canvas.clientHeight;
let compWidth = 0;
let compHeight = 0;
comp.attribute.forEach((item) => {
if (item.key === "width") {
compWidth = item.value;
}
if (item.key === "height") {
compHeight = item.value;
}
});
let maxX = canvasWidth - compWidth;
let maxY = canvasHeight - compHeight;
let lastNum = 0;
if (type === "x") {
if (num < 0) {
lastNum = 0;
} else if (num > maxX) {
lastNum = maxX;
} else {
lastNum = num;
}
} else if (type === "y") {
if (num < 0) {
lastNum = 0;
} else if (num > maxY) {
lastNum = maxY;
} else {
lastNum = num;
}
}
return lastNum
},
八、实现右侧的样式和数据界面
1、样式界面的绘制
<!-- 样式 -->
<div v-if="tabIndex === 0">
<div class="configItem" v-for="(item, index) in myCurrComp.attribute">
<span class="label">{{ item.name }}:</span>
<input
v-if="item.type === 'input'"
type="text"
v-model="item.value"
/>
<input
v-if="item.type === 'color'"
type="color"
v-model="item.value"
/>
</div>
</div>
其中:myCurrComp的值是从外面传进来的
watch: {
currCompData(val) {
this.myCurrComp = val;
this.dataList = val?JSON.stringify(val.data):''
},
},
2、数据界面的绘制
<!-- 数据 -->
<div v-if="tabIndex === 1">
<input type="textarea" v-model="dataList">
</div>
//dataList的数据也是从外面传进来的
watch: {
currCompData(val) {
this.myCurrComp = val;
this.dataList = val?JSON.stringify(val.data):''
},
},
九、实现样式和数据的控制
1、实现样式的响应
//一、设置回调
@blur="updateComp"
// 二、更新组件
updateComp(){
// 1、获取组件数据
let component = getComponent(this.myCurrComp.info,this.myCurrComp.attribute)
// 2、设置组件的位置
component.position = this.myCurrComp.position
// 3、重新挂载组件
mountComp(component)
},
2、实现数据的响应
//给数据组件设置监听
@change="updateComp"
//将数据传递给组件【第三个参数就是数据】
let component = getComponent(this.myCurrComp.info,this.myCurrComp.attribute,this.dataList)
十、删除组件和新增一个组件测试
1、删除组件
1、设置右击回调 @contextmenu.prevent=“rightClick” 2、定义回调方法 rightClick(){
if(confirm(“确定要删除这个组件吗?”)) {
document.getElementById(this.currComp.info.id).remove()
}
this.currComp = null; }
2、新增图片组件
照着文本组件,复制就行了,很简单的
3、新增菜单切换组件
一样拷贝即可