以下是一个基于 Vue 3 的可视化编辑器的实现方案,涵盖 拖拽交互、组件渲染、数据响应式更新 和 辅助线功能,并结合代码示例解析核心逻辑。
1. 核心功能概述
本项目实现了一个可视化编辑器,用户可以通过以下方式操作画布:
- 拖拽组件:从组件库拖拽元素到画布。
- 动态渲染:实时显示拖拽后的组件。
- 数据响应:通过 Vue 3 的响应式系统更新组件状态。
- 辅助线:移动或调整组件时显示对齐辅助线。
2. 拖拽交互实现
2.1 拖拽事件绑定
在画布容器上绑定 @dragover 和 @drop 事件:
<div class="canvas" @dragover="handleDragOver" @drop="handleDrop">
-
handleDragOver:阻止默认行为,允许拖放。const handleDragOver = (e) => { e.preventDefault(); e.dataTransfer.dropEffect = "copy"; // 设置拖拽效果为“复制” }; -
handleDrop:读取拖拽数据并创建组件。const handleDrop = (e) => { e.preventDefault(); const index = e.dataTransfer.getData("index"); // 从 dataTransfer 获取组件索引 if (index) { const component = deepCopy(componentList[index]); // 深拷贝组件模板 // 计算组件位置(基于鼠标坐标) component.style.top = e.clientY - rectInfo.y - component.style.height / 2; component.style.left = e.clientX - rectInfo.x - component.style.width / 2; compnentsStore.addComponent(component); // 存入状态管理 compnentsStore.createSnapShot(); // 创建快照用于撤销/重做 } };
2.2 拖拽组件的动态绑定
通过 dataTransfer 传递组件索引,确保拖拽后能正确生成对应组件。
3. 动态渲染与样式绑定
3.1 组件渲染
使用 v-for 遍历 compnentsStore.componentData,动态生成画布元素:
<div v-for="(item, index) in compnentsStore.componentData" :key="index">
<component :is="item.component" :component="item"></component>
</div>
-
component标签:动态渲染传入的组件(如按钮、文本框等)。 -
itemStyle方法:将组件样式对象转换为 CSS 字符串。const itemStyle = (style) => { return { position: "absolute", ...styleSplit(style).boxStyle }; };
3.2 交互点(形状点)渲染
通过 v-for 生成 8 个可拖动的控制点(左上、右上、中心等):
<div v-for="it in pointList" :key="it" class="shape-point" :style="getPointStyle(it, item)">
-
getPointStyle方法:根据方向计算控制点位置和光标样式。const getPointStyle = (point, item) => { // 计算 left/top 值 const style = { left: `${newLeft}px`, top: `${newTop}px`, cursor: cursors[point], // 设置光标样式(如 nw-resize) }; return style; };
4. 数据响应式更新
4.1 状态管理
通过 useCompnentsStore 管理组件数据和操作:
addComponent:添加新组件。setShapeStyle:更新组件样式(如位置、大小)。createSnapShot:记录操作快照,用于撤销/重做。
4.2 响应式变量
使用 ref 和 computed 实现 UI 与状态同步:
-
选中状态高亮:
:class="{ active: compnentsStore.curComponentData.map((it) => it.id).includes(item.id) }" -
画布样式绑定:
const canvasStyleData = computed(() => { return { height: compnentsStore.canvasStyleData.height + "px", width: compnentsStore.canvasStyleData.width + "px", }; });
5. 辅助线功能
5.1 辅助线显示逻辑
当移动或调整组件时,通过 mitter 事件触发辅助线更新:
mitter.emit("move", markLineComponent.value);
MarkLine.vue组件:根据传入的组件位置和大小绘制横向/纵向辅助线。- 对齐逻辑:通过比较组件边界与画布其他组件的边界,计算是否需要显示辅助线。
5.2 示例代码
在 MarkLine.vue 中,监听 move 事件并计算对齐位置:
mitter.on("move", (component) => {
const { top, left, width, height } = component.style;
const lines = calculateAlignmentLines(component, allComponents); // 计算对齐线
this.lines = lines;
});
当用户拖拽组件时,会自动检测与其他组件的对齐关系,并显示横向或纵向的对齐线。代码通过以下步骤完成:
- 获取当前拖拽组件的旋转后样式。
- 遍历所有未被选中的组件,检测对齐条件。
- 如果满足对齐条件,调整当前组件的位置,并记录辅助线位置。
关键变量与函数
1. 核心变量
markLineComponent: 当前正在拖拽的组件(触发辅助线的组件)。newLeft, newTop, newWidth, newHeight: 当前组件旋转后的样式属性。y, x: 记录对齐线的垂直/水平位置。markline.value: 存储最终的辅助线位置,用于渲染。
2. 关键函数
getComponentRotatedStyle: 计算组件旋转后的样式(如位置、尺寸)。isNearly(a, b): 判断两个数值是否接近(用于对齐条件判断)。compnentsStore.setShapeStyle: 更新组件样式(如位置)。
代码逻辑详解
1. 获取当前组件的旋转后样式
const { newLeft, newWidth, newHeight, newTop } = getComponentRotatedStyle(markLineComponent.style);
- 通过
getComponentRotatedStyle获取组件旋转后的样式属性(考虑了旋转角度的影响)。
2. 遍历未被选中的组件
compnentsStore.unCurComponentData.forEach((it) => {
const styleObj = getComponentRotatedStyle(it.style);
it = { ...it, ...styleObj };
- 遍历所有 未被选中的组件,并获取它们的旋转后样式(
newLeft, newTop, newWidth, newHeight)。
3. 定义对齐条件
let conditions = {
top: [
// 顶部对齐条件:上方边、中心、下方边
{ line: "xt", dragShift: it.newTop - newHeight, lineTop: it.newTop },
{ line: "xt", dragShift: it.newTop, lineTop: it.newTop },
{ line: "xc", dragShift: it.newTop + it.newHeight / 2 - newHeight / 2, lineTop: it.newTop + it.newHeight / 2 },
{ line: "xb", dragShift: it.newTop + it.newHeight - newHeight, lineTop: it.newTop + it.newHeight },
{ line: "xb", dragShift: it.newTop + it.newHeight, lineTop: it.newTop + it.newHeight },
],
left: [
// 左侧对齐条件:左边界、中心、右边界
{ line: "yl", dragShift: it.newLeft - newWidth, lineTop: it.newLeft },
{ line: "yl", dragShift: it.newLeft, lineTop: it.newLeft },
{ line: "yc", dragShift: it.newLeft + it.newWidth / 2 - newWidth / 2, lineTop: it.newLeft + it.newWidth / 2 },
{ line: "yr", dragShift: it.newLeft + it.newWidth - newWidth, lineTop: it.newLeft + it.newWidth },
{ line: "yr", dragShift: it.newLeft + it.newWidth, lineTop: it.newLeft + it.newWidth },
],
};
top条件:检测当前组件的顶部与未选中组件的顶部、中心、底部的对齐。left条件:检测当前组件的左侧与未选中组件的左侧、中心、右侧的对齐。dragShift: 当前组件需要调整的位置偏移量。lineTop: 辅助线的实际位置。
4. 检查对齐条件并调整位置
for (let i = 0; i < conditions.top.length; i++) {
let { dragShift, lineTop } = conditions.top[i];
if (isNearly(dragShift, newTop)) {
compnentsStore.curComponentData.forEach((it) => {
let i = findIndex(it.id, compnentsStore.componentData);
compnentsStore.setShapeStyle(i, { top: it.style.top + dragShift - newTop });
});
y = lineTop;
break;
}
}
isNearly: 判断当前组件的newTop是否接近dragShift(即是否满足对齐条件)。- 调整位置: 如果满足条件,将当前组件的
top调整为与未选中组件对齐。 - 记录辅助线位置: 设置
y = lineTop,表示需要显示横向辅助线。
5. 类似逻辑处理左侧对齐
for (let i = 0; i < conditions.left.length; i++) {
let { dragShift, lineTop } = conditions.left[i];
if (isNearly(dragShift, newLeft)) {
compnentsStore.curComponentData.forEach((it) => {
let i = findIndex(it.id, compnentsStore.componentData);
compnentsStore.setShapeStyle(i, { left: it.style.left + dragShift - newLeft });
});
x = lineTop;
break;
}
}
- 检测左侧对齐条件,调整当前组件的
left并记录辅助线位置x。
6. 更新辅助线位置
markline.value.top = y;
markline.value.left = x;
- 将计算出的辅助线位置赋值给
markline.value,用于在画布上渲染。
代码亮点
- 旋转兼容性:通过
getComponentRotatedStyle计算旋转后的样式,确保辅助线适用于旋转组件。 - 多对齐条件支持:支持顶部/底部/中心、左侧/右侧/中心的对齐检测。
- 响应式更新:通过
compnentsStore.setShapeStyle实时更新组件位置,触发 Vue 的响应式渲染。
最终效果
代码地址 : github.com/superWangqq…
总结
本项目通过 Vue 3 的响应式系统和组件化设计,实现了高效的可视化编辑器。拖拽交互、动态渲染、数据绑定和辅助线功能相辅相成,为用户提供流畅的编辑体验。后续可通过优化性能、扩展功能进一步提升实用性。