可視化拖拽頁面編輯器 一

前端技術日益發展,組件化日益成熟,做爲一個前端,天天的工做就是用組件堆砌頁面,有沒有一種方式能夠像CocosCreator,經過組件+腳本綁定的方式來實現咱們的頁面和功能,今天咱們就來實現一個提升生產力的工具 可視化拖拽頁面編輯器, 讓產品和UI經過拖拽編輯頁面,生產本身想要的頁面。javascript

技術框架採用Vue3 + Typescript + ElementPluscss

每一個章節下邊都會貼出對應commit代碼,方便你們對比學習前端

最終效果vue

實現功能:java

  • 主頁面結構:左側可選組件列表、中間容器畫布、右側編輯組件定義好的屬性
  • 從菜單拖拽組件到容器;
  • 單選、多選;
  • 容器內的組件能夠拖拽移動位置;
  • 組件拖拽調整寬高;
  • 組件拖拽貼邊,顯示輔助線;
  • 操做欄按鈕與命令
    • 撤銷、重作;
    • 導入、導出;
    • 置頂、置底;
    • 刪除、清空;
  • 組件綁定值;
  • 根據組件標識,經過做用域插槽自定義某個組件的行爲

預覽地址git

1、項目搭建與頁面佈局

經過vue-cli生成項目

vue create visual-editor-vue
複製代碼
  • 選擇手動配置

選擇配置以下:github

  • 選擇vue3.x版本

  • 這一步選y,使用jsx寫組件,須要添加對應的babel插件

接下來咱們來實現基本的左中右佈局

  • 左側菜單欄放置組件列表
  • 中間是畫布和工具欄,用來編輯預覽頁面
  • 右側是咱們選中某個組件後,顯示的該組件的屬性

第一部分代碼:基本佈局

2、數據結構設計與雙向綁定實現

數據結構設計

  • 定義數據結構以下
    • container 表示畫布容器
    • blocks 表示放置在容器中的組件
    • 每一個block表示一個組件,包含了組件的類型位置、寬高、選中狀態等信息
  • 畫布採用絕對定位,裏面的元素經過top、left來肯定位置
{
  "container": { 
    "height": 500,
    "width": 800
  },
  "blocks": [
    {
      "componentKey": "button",
      "top": 102,
      "left": 136,
      "adjustPosition": false,
      "focus": false,
      "zIndex": 0,
      "width": 0,
      "height": 0
    },
    {
      "componentKey": "input",
      "top": 148,
      "left": 358,
      "adjustPosition": false,
      "focus": false,
      "zIndex": 0,
      "width": 244,
      "height": 0
    }
   ]
 }
複製代碼

數據雙向綁定實現

  • 組件採用vue3中的jsx語法編寫,須要實現數據雙向綁定機制,useModel就是用來處理數據雙向綁定的
import { computed, defineComponent, ref, watch } from "vue";

// 用jsx封裝組件的時候,實現雙向數據綁定
export function useModel<T>(getter: () => T, emitter: (val: T) => void) {
  const state = ref(getter()) as { value: T };

  watch(getter, (val) => {
    if (val !== state.value) {
      state.value = val;
    }
  });

  return {
    get value() {
      return state.value;
    },
    set value(val: T) {
      if (state.value !== val) {
        state.value = val;
        emitter(val);
      }
    },
  };
}
複製代碼

useModel用法vue-cli

// modelValue 外部能夠用v-model綁定
export const TestUseModel = defineComponent({
  props: {
    modelValue: { type: String },
  },
  emits: {
    "update:modelValue": (val?: string) => true,
  },
  setup(props, ctx) {
    const model = useModel(
      () => props.modelValue,
      (val) => ctx.emit("update:modelValue", val)
    );
    return () => (
      <div> 自定義輸入框 <input type="text" v-model={model.value} /> </div>
    );
  },
});
複製代碼

第二部分代碼

3、Block渲染

  • 新建visual-editor-block的組件
  • block來表示在畫布顯示的組件元素
  • block先用文原本顯示
import { computed, defineComponent, PropType } from "vue";
import { VisualEditorBlockData } from "./visual-editor.utils";

export const VisualEditorBlock = defineComponent({
  props: {
    block: {
      type: Object as PropType<VisualEditorBlockData>,
    },
  },
  setup(props) {
    const styles = computed(() => ({
      top: `${props.block?.top}px`,
      left: `${props.block?.left}px`,
    }));
    return () => (
      <div class="visual-editor-block" style={styles.value}> 這是一條block </div>
    );
  },
});

複製代碼
  • 將定義的數據用v-model傳入editor

App.vue文件json

<template>
  <div class="app"> <visual-editor v-model="editorData" /> </div>
</template>

<script lang="ts"> import { defineComponent } from "vue"; import { VisualEditor } from "../src/packages/visual-editor"; export default defineComponent({ name: "App", components: { VisualEditor }, data() { return { editorData: { container: { height: 500, width: 800, }, blocks: [ { top: 100, left: 100 }, { top: 200, left: 200 }, ], }, }; }, }); </script>
複製代碼
  • 引入block組件,並進行渲染

visual-editor.tsx文件babel

import { computed, defineComponent, PropType } from "vue";
import { useModel } from "./utils/useModel";
import { VisualEditorBlock } from "./visual-editor-block";
import "./visual-editor.scss";
import { VisualEditorModelValue } from "./visual-editor.utils";

export const VisualEditor = defineComponent({
  props: {
    modelValue: {
      type: Object as PropType<VisualEditorModelValue>,
    },
  },
  emits: {
    "update:modelValue": (val?: VisualEditorModelValue) => true,
  },

  setup(props, ctx) {
    const dataModel = useModel(
      () => props.modelValue,
      (val) => ctx.emit("update:modelValue", val)
    );
    const containerStyles = computed(() => ({
      width: `${props.modelValue?.container.width}px`,
      height: `${props.modelValue?.container.height}px`,
    }));

    return () => (
      <div class="visual-editor"> <div class="menu">menu</div> <div class="head">head</div> <div class="operator">operator</div> <div class="body"> <div class="content"> <div class="container" style={containerStyles.value}> {(dataModel.value?.blocks || []).map((block, index: number) => ( <VisualEditorBlock block={block} key={index} /> ))} </div> </div> </div> </div>
    );
  },
});

複製代碼
  • 最終效果

  • 畫布會根據咱們定義的editorData對象,來進行展現,container來描述畫布的大小,block來描述在畫布上的每一個組件

第三部分代碼

完整代碼 GitHub

下一節:左側組件菜單、組件拖拽渲染和組件的選中與移動

相關文章
相關標籤/搜索