低代码4之实现json导入导出

  • 功能
  • 导入与导出
    • 1:editor.jsx 文件 在 buttons添加导出导入按钮
    • 2:useCommand.jsx 文件之中 添加更新updateContainer的命令( 这个命令需要加入消息队列之中,并且从外面commands.updateContainer传递的参数,更新最新的值 ) …args
    • 3: component / Dailog.jsx 实现弹窗组件

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";
import { $dialog } from "@/components/Dailog";
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()},
      {lable:'导出',icon:'',handler:()=>{
        $dialog({
          title:'导出json使用',
          content: JSON.stringify(data.value)
        })
      }},
      {lable:'导入',icon:'',handler:()=>{
        $dialog({
          title:'导入json使用',
          content:'',
          footer:true,
          onComfirm:(text)=>{
            // 需要在useCommand.js文件之中 调用消息队列 实现撤销重做等操作
            commands.updateContainer( JSON.parse(text) )
          }
        })
      }}
    ]

    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 / useCommand.jsx

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] = (...args) => { // 更新的时候 传递的参数 args
      // 命令名字 映射 执行函数
      const { redo, undo } = command.execute(...args);
      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 };
        },
      };
    },
  });

  // 更新 带有历史记录的模式
  regeister({
    name:'updateContainer', // 更新整个容器
    pushQueue: true, // 丢到消息队列
    execute(newValue){ // newValue 拿到调用 commands.updateContainer传递的参数
      let stete = {
        before: data.value, // 当前的值
        after: newValue // 导入后最新的值
      }
      return {
        redo:()=>{// 重做
          data.value = stete.after
        },
        undo:()=>{// 撤销
          data.value = stete.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 / component / Dailog.jsx

import { ElButton, ElDialog, ElInput } from "element-plus";
import { createVNode, defineComponent, reactive, render } from "vue";

const DialogComponent = defineComponent({
  props: {
    option: {
      type: Object,
    },
  },
  setup(props, ctx) {
    const state = reactive({
      option:props.option, // 用户给组件的属性
      isShow: false,
    });
    // 当前组件 想要暴露那些方法
    ctx.expose({
      showDialog(option) {
        state.option = option
        state.isShow = true;
      },
    });
    const onCancel = ()=>{
      state.isShow = false
    }
    const onComfirm = ()=>{
      state.isShow = false;
      state.option.onComfirm && state.option.onComfirm(state.option.content)
    }
    return () => {
      return <ElDialog v-model={state.isShow} title={state.option.title}>
        {{
          // 默认插槽
          default:()=> <ElInput type="textarea" v-model={state.option.content} rows={10}></ElInput>,
          // 底部插槽 若是有footer则展示 div
          footer:()=> state.option.footer && <div>
            <ElButton onClick={onCancel}>取消</ElButton>
            <ElButton type='primary' onClick={onComfirm}>确定</ElButton>
          </div>
        }}
      </ElDialog>;
    };
  },
});

let vm;
export function $dialog(option) {
  console.log("option", option);
  // ele-plus之中的 el-dialog组件
  // 手动挂载组件
  if (!vm) {
    let el = document.createElement("div");
    // 讲DialogComponent组件 渲染为 虚拟节点
    vm = createVNode(DialogComponent, { option });
    document.body.appendChild((render(vm, el), el)); // 把虚拟节点渲染为真实的节点 并且添加为body子节点
  }

  // 将组件渲染到 el 元素之中
  let { showDialog } = vm.component.exposed;
  showDialog(option);
}

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐