• 安装依赖
    • npm i mitt
  • 功能:
    • 撤销与重做
      • 需要在useMenuDragger.js之中抛出事件events.emit('start'); // 拖拽前与events.emit('end'); // 拖拽后
      • 需要在useBlockDragger.js之中 标记一个拖拽的状态dragging=> 用于标记从后台渲染后的组件是否被拖拽的状态、并且在mousemove和mouseup之中抛出事件
      • useCommand.js 之中 处理撤销和重做功能

    • mitt 为bus总线

src / package / useCommand.js

// 菜单拖拽功能
import { events } from "./events"
export function useMenuDragger (containerRef,data){
  let currentComponent = null // 当前拖拽的组件
  const dragstart = (e,component)=>{
    // dragenter 进入元素中,添加一个移动标识
    // dragover 在目标元素经过 必须要组织默认事件 否则不能触发drop
    // dragleave 离开元素的时候 需要增加一个禁用的标识
    // drop 松手的时候 根据拖拽的组件 添加一个组件
    containerRef.value.addEventListener('dragenter',dragenter)
    containerRef.value.addEventListener('dragover',dragover)
    containerRef.value.addEventListener('dragleave',dragleave)
    containerRef.value.addEventListener('drop',drop)
    currentComponent = component;
    events.emit('start'); // 拖拽前
  }
  // 拖拽结束 去掉事件 + 清空当前组件
  const dragend = (e)=>{
    containerRef.value.removeEventListener('dragenter',dragenter)
    containerRef.value.removeEventListener('dragover',dragover)
    containerRef.value.removeEventListener('dragleave',dragleave)
    containerRef.value.removeEventListener('drop',drop)
    currentComponent = null;
    events.emit('end'); // 拖拽后
  }
  const dragenter = (e)=>{
    e.dataTransfer.dropEffect = 'move'; // 移动标识
  }
  const dragover = (e)=>{
    e.preventDefault(); // 阻止默认行为
  }
  const dragleave = (e)=>{
    e.dataTransfer.dropEffect = 'none'; // 离开 设置禁用
  }
  const drop = (e)=>{
    let blocks = data.value.blocks; // 内部渲染的组件
    // 更新data的数据 触发更新app传递进来的v-model数据
    data.value = {
      ...data.value, 
      blocks:[
        ...blocks,
        // 拿到拖拽入容器的组件 位置 与 key值
        { top:e.offsetY, left:e.offsetX ,zIndex:1,
          key:currentComponent.key,
          alignCenter:true // 松手时居中 需要在editor-blick.jsx之中配置
        }
      ]
    }
    currentComponent = null
  }
  return {
    dragstart,
    dragend
  }
}

src / package / events.js

import mitt from "mitt";
export const events = mitt(); // 导出一个发布订阅的对象 (类型vue2的bus)

src / package / useCommand.js

import deepcopy from "deepcopy";
import { onUnmounted } from "vue";
import { events } from "./events";
export function useCommand(data) {
  const state = {
    // 前进和后退都需要指针
    current: -1, // 前进后退的索引
    queue: [], // 存放所有的操作指令
    commands: {}, // 制作命令和执行功能一个映射表 undo: ()=>{} ; redo: ()=>{}
    commandArray: [], // 存放所有的命令
    destoryArray: [], // 销毁的命令
  };

  const regeister = (command) => {
    state.commandArray.push(command);
    state.commands[command.name] = () => {
      // 命令名字 映射 执行函数
      const { redo, undo } = command.execute();
      redo();
      if (!command.pushQueue) {
        // 判断是否需要放入队列之中 不需要直接return
        return;
      }
      let { queue, current } = state;
      // 如果 放入了组件1 组件2 点击撤回 放入组件3 => 最终结果为 组件1 组件3
      if (queue.length > 0) {
        queue = queue.slice(0, current + 1); // 以当前的位置 截取最新的数据(可能撤销了几个),以当前最新的current来计算值
        state.queue = queue;
      }
      queue.push({
        // 保存指令的前进和后退
        redo,
        undo,
      });
      state.current = current + 1;
    };
  };

  // 注册我们需要的命令
  // 重做命令
  regeister({
    name: "redo",
    keyboard: "ctrl+y",
    execute() {
      return {
        redo() {
          let item = state.queue[state.current + 1]; // 找到当前的下一步操作
          if (item) {
            // 若是有值 则撤销
            item.redo && item.redo();
            state.current++;
          }
        },
      };
    },
  });
  // 撤销命令
  regeister({
    name: "undo",
    keyboard: "ctrl+z",
    execute() {
      return {
        redo() {
          if (state.current == -1) return; // 若是没有 则退出
          let item = state.queue[state.current]; // 找到当前的上一步操作
          if (item) {
            // 若是有值 则撤销
            item.undo && item.undo();
            state.current--;
          }
        },
      };
    },
  });
  // 拖拽命令 如果希望将操作放到队列中 可以增加一个属性 标识等会操作的要放到队列中
  regeister({
    name: "drag",
    pushQueue: true,
    init() {
      // 初始化操作 默认就执行
      this.before = null; // 操作前的数据
      // 监控拖拽开始事件,保存当前状态
      const start = () => (this.before = deepcopy(data.value.blocks));
      // 拖拽后 需要触发的对应指令
      const end = () => state.commands.drag();
      events.on("start", start);
      events.on("end", end);
      return () => {
        // 卸载函数
        events.off("start", start);
        events.off("end", end);
      };
    },
    execute() {
      let before = this.before; // 之前的
      let after = data.value.blocks; // 最新的
      return {
        redo() {
          // 更新最新的值 默认一松手的时候 把当前的事情处理了
          data.value = { ...data.value, blocks: after };
        },
        undo() {
          // 撤销 往前面退一个操作
          data.value = { ...data.value, blocks: before };
        },
      };
    },
  });

  const keyboardEvent = (() => {
    const init = () => {
      // 初始化事件
      const keyCodes = {
        90: "z",
        89: "y",
      };
      const onKeydown = (e) => {
        const { ctrlKey, keyCode } = e;
        let keyStrings = []; // ctrl+z 或者 ctrl+y
        if (ctrlKey) keyStrings.push("ctrl");
        keyStrings.push(keyCodes[keyCode]);
        keyStrings = keyStrings.join("+");
        state.commandArray.forEach(({keyboard, name}) => {
          if (!keyboard) return;
          if (keyboard === keyStrings) {
            state.commands[name]();
            e.preventDefault(); // 阻止默认行为
          }
        });
      };
      window.addEventListener("keydown", onKeydown);
      return () => {
        // 销毁事件
        window.removeEventListener("keydown", onKeydown);
      };
    };
    return init;
  })();

  // 查看是否有拖拽后的效果 有立即执行更新操作 并且存放当前数据(用于销毁撤销)
  (() => {
    // 监听键盘事件 ctrl + z 实现撤销
    state.destoryArray.push(keyboardEvent());
    // 查看是否有拖拽后的效果 有立即执行更新操作 并且存放当前数据(用于销毁撤销)
    state.commandArray.forEach(
      (command) => command.init && state.destoryArray.push(command.init())
    );
  })();

  onUnmounted(() => {
    // 清理绑定的事件
    state.destoryArray.forEach((fn) => fn && fn());
  });

  return state;
}

src / package / editor.jsx

import { computed, defineComponent, inject, ref } from "vue";
import "./editor.scss";
import EditorBlock from "./editor-block";
import deepcopy from "deepcopy";
import { useMenuDragger } from "./useMenuDragger";
import { useFocus } from "./useFocus";
import { useBlockDragger } from "./useBlockDragger";
import { useCommand } from "./useCommand";
export default defineComponent({
  props: {
    modelValue: {
      type: Object,
    },
  },
  emits: ["update:modelValue"], // 1:菜单拖拽功能-03:触发事件 更新app的数据 set之中更新
  setup(props, ctx) {
    // console.log('props',props.modelValue);
    const data = computed({
      get() {
        return props.modelValue;
      },
      set(newValue) {
        ctx.emit("update:modelValue", deepcopy(newValue));
      },
    });
    const contentStyle = computed(() => ({
      width: data.value.container.width + "px",
      height: data.value.container.height + "px",
    }));
    const config = inject("config");

    //  1:菜单拖拽功能-02:实现h5的拖拽放入组件容器形成被拖拽的组件 useMenuDragger实现左侧菜单拖拽功能
    const containerRef = ref(null);
    const { dragstart, dragend } = useMenuDragger(containerRef, data);
    // 2:容器内获取焦点功能-01:点击容器时候聚焦与按住shift时候支持多个聚焦;选中后拖拽
    const { blockMousedown, containerMousedown, focusData,lastSelectBlock } = useFocus(
      data,
      (e) => {
        // 3:获取焦点后 进行拖拽-02
        mousedown(e, focusData);
      }
    );
    // 3:实现组件拖拽-01:
    const { mousedown,markLine } = useBlockDragger(focusData,lastSelectBlock,data);

    // 4:每一次操作的记录 撤销与重做功能
    const {commands} = useCommand(data)
    const buttons = [
      {lable:'撤销',icon:'',handler:()=>commands.undo()},
      {lable:'重做',icon:'',handler:()=>commands.redo()}
    ]

    return () => (
      <div class="editor">
        <div class="editor-left">
          {/** 根据config的注册列表 渲染出左侧的物料区域、:1:菜单拖拽功能-01:实现h5的拖拽-draggable */}
          {config.componetsList.map((component) => (
            <div
              class="editor-left-item"
              draggable
              onDragstart={(e) => dragstart(e, component)}
              onDragend={dragend}
            >
              <span class="editor-left-item-label">{component.label}</span>
              <div>{component.preview()}</div>
            </div>
          ))}
        </div>
        <div className="editor-center">
          <div class="editor-top">
            {
              buttons.map((btn,idx)=>{
                return <div class={'editor-top-btn'} onClick={btn.handler}>
                  <i class={btn.icon}></i>
                  <span>{btn.lable}</span>
                </div>
              })
            }
          </div>
          <div class="editor-content">
            {/* 负责尝试滚动条 */}
            <div class="editor-content-canvas">
              {/* 产生内容区域 */}
              <div
                class="editor-content-canvas_content"
                style={contentStyle.value}
                ref={containerRef}
                onMousedown={containerMousedown}
              >
                {data.value.blocks.map((block,index) => (
                  <EditorBlock
                    class={block.focus ? "editoe-blick-focus" : ""}
                    block={block}
                    onMousedown={(e) => blockMousedown(e, block,index)}
                  ></EditorBlock>
                ))}
              {/**辅助线 */}
              { markLine.x !== null && <div class='line-x' style={{left:markLine.x + 'px'}}></div>}
              { markLine.y !== null && <div class='line-y' style={{top:markLine.y + 'px'}}></div>}
              </div>
            </div>
          </div>
        </div>
        <div class="editor-right">right</div>
      </div>
    );
  },
});

src / package / editor.scss

.editor {
  width: 100%;
  height: 100%;
  overflow: hidden;
  display: flex;
  .editor-left ,.editor-right{
    width: 270px;
    background: yellow;
    height: 100%;
  }
  .editor-left {
    .editor-left-item {
      position: relative;
      width: 250px;
      margin: 20px auto;
      display: flex;
      justify-content: center;
      align-items: center;
      background: #ccc;
      padding: 20px;
      box-sizing: border-box;
      cursor: move;
      user-select: none; // 无法操作
      min-height: 100px;
      .editor-left-item-label {
        position: absolute;
        left: 0;
        top: 0;
        background: rgb(96, 205, 224);
        color: #fff;
        padding: 4px;
      }
      // 设置 item 项目不可点击等
      &::after {
        content: '';
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background: #ccc;
        opacity: 0.2;
      }
    }
  }
  .editor-center {
    width: calc(100% - 270px - 270px - 20px);
    padding: 0 10px;
    background: orange;
    height: 100%;
  }
  .editor-top {
    height: 80px;
    background: pink;
    display: flex;
    justify-content: center;
    align-items: center;
    .editor-top-btn {
      width: 60px;
      height: 60px;
      display: flex;
      justify-content: center;
      align-items: center;
      background: rgba(0,0,0,.3);
      color: #fff;
      user-select: none;
      cursor: pointer;
      margin-left: 3px;
    }
  }
  .editor-content {
    height: calc(100% - 80px);
    background: orange;
    .editor-content-canvas {
      overflow: scroll;
      height: 100%;
    }
    .editor-content-canvas_content {
      position: relative;
      margin: 20px auto;
      // height: calc(100% - 40px);
      // width: 550px;
      // height: 550px;
      background: #ccc;
    }
  }
}
.editor-block {
  position: absolute;
  &::after {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
  }
}
.editoe-blick-focus {
  &::after {
    border: 2px dashed red;
  }
}
// 移动的left值
.line-x {
  position: absolute;
  top: 0;
  bottom: 0;
  border-left:1px dashed red;
}
// 移动的top值
.line-y {
  position: absolute;
  left: 0;
  right: 0;
  border-top:1px dashed red;
}

Logo

一站式 AI 云服务平台

更多推荐