基于 Vue 3 的可视化编辑器开发:拖拽、渲染、数据响应与辅助线实现

4,411 阅读5分钟

以下是一个基于 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 响应式变量

使用 refcomputed 实现 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;
});

image.png

当用户拖拽组件时,会自动检测与其他组件的对齐关系,并显示横向或纵向的对齐线。代码通过以下步骤完成:

  1. 获取当前拖拽组件的旋转后样式。
  2. 遍历所有未被选中的组件,检测对齐条件。
  3. 如果满足对齐条件,调整当前组件的位置,并记录辅助线位置。

关键变量与函数

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,用于在画布上渲染。

代码亮点

  1. 旋转兼容性:通过 getComponentRotatedStyle 计算旋转后的样式,确保辅助线适用于旋转组件。
  2. 多对齐条件支持:支持顶部/底部/中心、左侧/右侧/中心的对齐检测。
  3. 响应式更新:通过 compnentsStore.setShapeStyle 实时更新组件位置,触发 Vue 的响应式渲染。

最终效果

屏幕录制2025-05-16 15.58.33_20250516_165554.gif

屏幕录制2025-05-16 15.58.33_20250516_170538.gif

代码地址 : github.com/superWangqq…

总结

本项目通过 Vue 3 的响应式系统和组件化设计,实现了高效的可视化编辑器。拖拽交互、动态渲染、数据绑定和辅助线功能相辅相成,为用户提供流畅的编辑体验。后续可通过优化性能、扩展功能进一步提升实用性。