• VisualDrag低代码拖拽模板


    背景

    在这里插入图片描述

    接到一个需求做一个拖拽模板低代码生成界面(如上图),就是可以自定义界面元素拖拽生成页面,该页面需要可以存储,并且一比一还原。

    因此得研究实现一个拖拽生成低代码平台,通过查询了各种资料,找到了以下比较合适的开源的低代码平台:

    1. visual-drag-demo:https://github.com/woai3c/visual-drag-demo
    2. 拖拽大屏:https://gitee.com/gist006/vue-visual-drag
    3. 专题制作工具:https://gitee.com/Maxfengyan/visual-drag
    4. GoView低代码数据可视化:https://www.mtruning.club/
    5. 鲁班H5:https://ly525.gitee.io/luban-h5/zh/
    6. quark-h5: https://github.com/huangwei9527/quark-h5

    根据自己的需求,选择了visual-drag-demo为模板进行了二开。

    在线预览:
    预览地址:https://qkongtao.gitee.io/visual-drag-demo
    在这里插入图片描述

    最后集成在后台系统中:
    在这里插入图片描述

    技术&文档

    使用到的技术功能点:

    1. 编辑器
    2. 自定义组件(文本、图片、矩形、圆形、直线、星形、三角形、按钮、表格、组合)
    3. 接口请求(通过接口请求组件数据)
    4. 组件联动
    5. 拖拽
    6. 删除组件、调整图层层级
    7. 放大缩小
    8. 撤消、重做
    9. 组件属性设置
    10. 吸附
    11. 预览、保存代码
    12. 绑定事件
    13. 绑定动画
    14. 拖拽旋转
    15. 复制粘贴剪切
    16. 多个组件的组合和拆分
    17. 锁定组件
    18. 网格线

    可以参考原作者大大的文档:
    可视化拖拽组件库一些技术要点原理分析(一):https://github.com/woai3c/Front-end-articles/issues/19
    可视化拖拽组件库一些技术要点原理分析(二):https://github.com/woai3c/Front-end-articles/issues/20
    可视化拖拽组件库一些技术要点原理分析(三):https://github.com/woai3c/Front-end-articles/issues/21
    可视化拖拽组件库一些技术要点原理分析(四):https://github.com/woai3c/Front-end-articles/issues/22

    在作者的这几篇文章中把技术点介绍的很详细,虽然还是有很多不懂的,,,

    二开优化方案

    由于个人的能力有限,只能在作者的基础上优化成满足自己需求的拖拽模板

    1. 优化侧边栏

    修改侧边栏的样式
    src\components\ComponentList.vue

    <template>
      <div class="component-list" @dragstart="handleDragStart">
        <div
          v-for="(item, index) in componentList"
          :key="index"
          class="list"
          draggable
          :data-index="index"
        >
          <span class="iconfont" :class="'icon-' + item.icon">span>
          <span class="btn_name">{{ item.label }}span>
        div>
      div>
    template>
    
    <script>
    import componentList from "@/custom-component/component-list";
    
    export default {
      data() {
        return {
          componentList,
        };
      },
      methods: {
        handleDragStart(e) {
          e.dataTransfer.setData("index", e.target.dataset.index);
        },
      },
    };
    script>
    
    <style lang="scss" scoped>
    .component-list {
      width: 200px;
      height: 55%;
      margin: 10px auto 0;
      display: grid;
      grid-gap: 10px 30px;
      grid-template-columns: repeat(auto-fill, 66px);
      grid-template-rows: repeat(auto-fill, 56px);
    
      .list {
        width: 66px;
        height: 56px;
        border: 1px solid #ddd;
        cursor: grab;
        text-align: center;
        color: #333;
        // background-color: #f3f3f3;
        border-radius: 8px;
        box-shadow: rgb(168 168 168 / 30%) 0px 2px 4px 0px;
        padding: 2px 5px;
        margin-left: 20px;
    
        &:active {
          cursor: grabbing;
        }
    
        .iconfont {
          display: block;
          font-size: 24px;
          margin-top: 3px;
          margin-bottom: 0px;
        }
    
        .icon-wenben,
        .icon-biaoge {
          font-size: 24px;
        }
    
        .icon-tupian {
          font-size: 24px;
        }
        .btn_name {
          font-size: 10px;
          line-height: 20px;
          color: rgb(31, 62, 104);
          width: 56px;
          padding: 0px 5px;
          word-break: keep-all;
          white-space: nowrap;
          overflow: hidden;
          text-overflow: ellipsis;
        }
      }
    }
    style>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88

    src\components\RealTimeComponentList.vue

    <template>
      <div class="real-time-component-list">
        <div
          v-for="(item, index) in componentData"
          :key="index"
          class="list"
          :class="{ actived: transformIndex(index) === curComponentIndex }"
          @click="onClick(transformIndex(index))"
        >
          <span class="iconfont" :class="'icon-' + getComponent(index).icon">span>
          <span class="label">{{ getComponent(index).label }}span>
          <div class="icon-container">
            <span
              class="iconfont icon-shangyi"
              @click="upComponent(transformIndex(index))"
            >span>
            <span
              class="iconfont icon-xiayi"
              @click="downComponent(transformIndex(index))"
            >span>
            <span
              class="iconfont icon-shanchu"
              @click="deleteComponent(transformIndex(index))"
            >span>
          div>
        div>
      div>
    template>
    
    <script>
    import { mapState } from "vuex";
    
    export default {
      computed: mapState(["componentData", "curComponent", "curComponentIndex"]),
      methods: {
        getComponent(index) {
          return this.componentData[this.componentData.length - 1 - index];
        },
    
        transformIndex(index) {
          return this.componentData.length - 1 - index;
        },
    
        onClick(index) {
          this.setCurComponent(index);
        },
    
        deleteComponent() {
          setTimeout(() => {
            this.$store.commit("deleteComponent");
            this.$store.commit("recordSnapshot");
          });
        },
    
        upComponent() {
          setTimeout(() => {
            this.$store.commit("upComponent");
            this.$store.commit("recordSnapshot");
          });
        },
    
        downComponent() {
          setTimeout(() => {
            this.$store.commit("downComponent");
            this.$store.commit("recordSnapshot");
          });
        },
    
        setCurComponent(index) {
          this.$store.commit("setCurComponent", {
            component: this.componentData[index],
            index,
          });
        },
      },
    };
    script>
    
    <style lang="scss" scoped>
    .real-time-component-list {
      height: 45%;
    
      .list {
        height: 50px;
        cursor: grab;
        text-align: center;
        color: #333;
        background-color: #f3f3f3;
        display: flex;
        align-items: center;
        font-size: 12px;
        padding: 0 15px;
        position: relative;
        user-select: none;
    
        &:active {
          cursor: grabbing;
        }
    
        &:hover {
          background-color: #d2d2d2;
    
          .icon-container {
            display: block;
          }
        }
    
        .label {
          font-size: 16px;
          margin-left: 5px;
        }
        .iconfont {
          margin-right: 5px;
          font-size: 30px;
        }
    
        .icon-shangyi,
        .icon-xiayi,
        .icon-shanchu {
          font-size: 28px;
          margin-right: 0px;
        }
    
        .icon-wenben,
        .icon-tupian {
          font-size: 28px;
        }
    
        .icon-container {
          position: absolute;
          right: 10px;
          display: none;
    
          .iconfont {
            cursor: pointer;
          }
        }
      }
    
      .actived {
        background: #ecf5ff;
        color: #409eff;
      }
    }
    style>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145

    2. 优化图片插入

    插入图片时的大小优化(插入图片分辨率过大时自定义缩放图片)
    src\components\Toolbar.vue
    修改 handleFileChange(e) 方法

    handleFileChange(e) {
          const file = e.target.files[0];
          if (!file.type.includes("image")) {
            toast("只能插入图片");
            return;
          }
    
          const reader = new FileReader();
          reader.onload = (res) => {
            const fileResult = res.target.result;
            const img = new Image();
            img.onload = () => {
              const component = {
                ...commonAttr,
                id: generateID(),
                component: "Picture",
                label: "图片",
                icon: "",
                propValue: {
                  url: fileResult,
                  flip: {
                    horizontal: false,
                    vertical: false,
                  },
                },
                style: {
                  ...commonStyle,
                  top: 0,
                  left: 0,
                  width:
                    img.width > 1000
                      ? img.width * 0.3
                      : img.width < 300
                      ? img.width
                      : img.width * 0.5,
                  height:
                    img.width > 1000
                      ? img.height * 0.3
                      : img.width < 300
                      ? img.height
                      : img.height * 0.5,
                },
              };
    
              // 根据画面比例修改组件样式比例
              changeComponentSizeWithScale(component);
    
              this.$store.commit("addComponent", { component });
              this.$store.commit("recordSnapshot");
    
              // 修复重复上传同一文件,@change 不触发的问题
              $("#input").setAttribute("type", "text");
              $("#input").setAttribute("type", "file");
            };
    
            img.src = fileResult;
          };
    
          reader.readAsDataURL(file);
        },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    3. 新增可插入画布的组件

    可以在通过自定义封装组件,插入画布,因为在demo中,新增了几个常用的组件:

    • 音频
    • 视频
    • 浏览器

    新增步骤如下:
    1). 在 src\custom-component 目录下新建需要新增的组件文件夹
    2). 在该文件夹下面新建两个vue文件Component.vue、Attr.vue(示例浏览器):
    Component.vue
    组件的具体代码内容

    <template>
      <div style="overflow: hidden">
        <div class="iframe-container">
          <iframe
            name="myiframe"
            id="myiframe"
            :src="propValue.url"
            align="center"
            frameborder="0"
            allowfullscreen
          >
            <p>你的浏览器不支持iframe标签p>
          iframe>
        div>
      div>
    template>
    
    <script>
    export default {
      props: {
        propValue: {
          type: Object,
          require: true,
          default: "",
        },
        element: {
          type: Object,
          default: () => {},
        },
      },
      methods: {},
    };
    script>
    
    <style lang="scss" scoped>
    .iframe-container {
      width: 100%;
      height: 100%;
      position: relative;
    }
    .iframe-container iframe {
      // pointer-events: none;
      position: absolute;
      left: 0;
      top: 0;
      margin: 0px;
      width: 100%;
      height: 100%;
    }
    style>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    Attr.vue
    组件的侧边栏动态功能(修改浏览器链接、上传文件等)

    <template>
      <div class="attr-list">
        <CommonAttr>CommonAttr>
        <el-form>
          <el-form-item label="网址链接">
            <el-input
              v-model="curComponent.propValue.url"
              type="textarea"
              :rows="3"
              style="clear: both"
            />
          el-form-item>
        el-form>
      div>
    template>
    
    <script>
    import CommonAttr from "@/custom-component/common/CommonAttr.vue";
    
    export default {
      components: { CommonAttr },
      computed: {
        curComponent() {
          return this.$store.state.curComponent;
        },
      },
    };
    script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    3). 在 src\custom-component\component-list.js组件列表中添加对应的组件信息
    注意组件名称等信息要对应刚刚建立组件的名称

    // 编辑器左侧组件列表
    const list = [{
    	... ...
     	... ...
     	... ...
    {
            component: 'Browser',
            label: '浏览器',
            icon: 'hulianwang',
            propValue: {
                url: "https://mytab.qkongtao.cn/",
                flip: {
                    horizontal: false,
                    vertical: false,
                },
            },
            style: {
                width: 325,
                height: 560,
            },
     },
     	... ...
     	... ...
     	... ...
     }]
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    4). 封装完成后,即可以在页面中看到新增的组件

    4. 解决组件鼠标默认事件冲突的问题

    在插入audio 和 iframe等组件时,在画布上的拖拽失效,原因时鼠标事件和audio、iframe标签的原有事件冲突,外部无法对iframe内部进行操作。因此采用默认禁止有鼠标事件冲突组件的鼠标事件,等到预览展示时恢复鼠标事件。

    使用css禁止元素的鼠标事件
    pointer-events: none;

    在src\components\Editor\index.vue 页面组件列表展示设置对应组件的样式 pointer-events: none;进行控制组件是否可以拖拽。
    在src\components\Editor\ComponentWrapper.vue 预览或者导出的时候需要设置对应组件的样式style:pointer-events: auto; 恢复该组件原有的鼠标事件

    src\components\Editor\index.vue

    	... ...
     	... ...
     	... ...
    
    
        <Shape
          v-for="(item, index) in componentData"
          :key="item.id"
          :default-style="item.style"
          :style="getShapeStyle(item.style)"
          :active="item.id === (curComponent || {}).id"
          :element="item"
          :index="index"
          :class="{ lock: item.isLock }"
        >
          <component
            :is="item.component"
            v-if="item.component.startsWith('SVG')"
            :id="'component' + item.id"
            :style="getSVGStyle(item.style)"
            class="component"
            :prop-value="item.propValue"
            :element="item"
            :request="item.request"
          />
          <component
            :is="item.component"
            v-else-if="item.component == 'VText'"
            :id="'component' + item.id"
            class="component"
            :style="getComponentStyle(item.style)"
            :prop-value="item.propValue"
            :element="item"
            :request="item.request"
            @input="handleInput"
          />
    
          <component
            :is="item.component"
            v-else-if="item.component == 'Video'"
            :id="'component' + item.id"
            class="component"
            :style="getComponentStyle(item.style)"
            :prop-value="item.propValue"
            :element="item"
            :request="item.request"
          />
    
          <component
            :is="item.component"
            v-else
            :id="'component' + item.id"
            class="component"
            :style="getComponentStyle(item.style)"
            :prop-value="item.propValue"
            :element="item"
            :request="item.request"
            style="pointer-events: none"
          />
        Shape>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    src\components\Editor\ComponentWrapper.vue

    <template>
      <div @click="onClick" @mouseenter="onMouseEnter">
        <component
          :is="config.component"
          v-if="config.component.startsWith('SVG')"
          ref="component"
          class="component"
          :style="getSVGStyle(config.style)"
          :prop-value="config.propValue"
          :element="config"
          :request="config.request"
          :linkage="config.linkage"
        />
        <component
          :is="config.component"
          v-if="
            config.component.startsWith('Music') ||
            config.component.startsWith('Browser')
          "
          ref="component"
          class="component"
          :style="getSVGStyle(config.style)"
          :prop-value="config.propValue"
          :element="config"
          :request="config.request"
          :linkage="config.linkage"
          style="pointer-events: auto !important"
        />
    
        <component
          :is="config.component"
          v-else
          ref="component"
          class="component"
          :style="getStyle(config.style)"
          :prop-value="config.propValue"
          :element="config"
          :request="config.request"
          :linkage="config.linkage"
        />
      div>
    template>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    数据保存对接&页面生成预览

    保存对接

    本项目中在记录和传递数据中频繁的使用vuex,最后保存的数据为:
    画布数据:this.$store.state.canvasStyleData;
    画布内容数据:this.$store.state.componentData;
    保存示例如下:

    {
    	"canvasStyleData": {
    		"width": 1280,
    		"height": 720,
    		"scale": 90,
    		"color": "#000",
    		"opacity": 1,
    		"background": "#fff",
    		"fontSize": 14
    	},
    	"componentData": [{
    		"animations": [],
    		"events": {},
    		"groupStyle": {},
    		"isLock": false,
    		"collapseName": "style",
    		"linkage": {
    			"duration": 0,
    			"data": [{
    				"id": "",
    				"label": "",
    				"event": "",
    				"style": [{
    					"key": "",
    					"value": ""
    				}]
    			}]
    		},
    		"component": "Picture",
    		"label": "图片",
    		"icon": "charutupian",
    		"propValue": {
    			"url": "http://localhost:8000/api/files/getImage/5865ef7d990e40a88a08ceca3e7c118c",
    			"flip": {
    				"horizontal": false,
    				"vertical": false
    			}
    		},
    		"style": {
    			"rotate": 0,
    			"opacity": 1,
    			"width": 270,
    			"height": 180,
    			"borderRadius": "",
    			"top": 89,
    			"left": 72
    		},
    		"id": "fcn3XAGtR50D_JcImnBbc"
    	}, {
    		"animations": [],
    		"events": {},
    		"groupStyle": {},
    		"isLock": false,
    		"collapseName": "style",
    		"linkage": {
    			"duration": 0,
    			"data": [{
    				"id": "",
    				"label": "",
    				"event": "",
    				"style": [{
    					"key": "",
    					"value": ""
    				}]
    			}]
    		},
    		"component": "Video",
    		"label": "视频",
    		"icon": "shipin",
    		"propValue": {
    			"url": "https://qiniu.qkongtao.cn/2022/10/20221016134256839.mp4?_\u003d1",
    			"flip": {
    				"horizontal": false,
    				"vertical": false
    			}
    		},
    		"style": {
    			"rotate": 0,
    			"opacity": 1,
    			"width": 360,
    			"height": 270,
    			"top": 89,
    			"left": 722
    		},
    		"id": "1HqDupYn-KA-1Xl4gorHA"
    	}, {
    		"animations": [],
    		"events": {},
    		"groupStyle": {},
    		"isLock": false,
    		"collapseName": "style",
    		"linkage": {
    			"duration": 0,
    			"data": [{
    				"id": "",
    				"label": "",
    				"event": "",
    				"style": [{
    					"key": "",
    					"value": ""
    				}]
    			}]
    		},
    		"component": "Browser",
    		"label": "浏览器",
    		"icon": "hulianwang",
    		"propValue": {
    			"url": "https://qkongtao.cn/",
    			"flip": {
    				"horizontal": false,
    				"vertical": false
    			}
    		},
    		"style": {
    			"rotate": 0,
    			"opacity": 1,
    			"width": 397,
    			"height": 265,
    			"top": 352,
    			"left": 63
    		},
    		"id": "DyIYmOGLRgUt1iKCuoloC"
    	}, {
    		"animations": [],
    		"events": {},
    		"groupStyle": {},
    		"isLock": false,
    		"collapseName": "style",
    		"linkage": {
    			"duration": 0,
    			"data": [{
    				"id": "",
    				"label": "",
    				"event": "",
    				"style": [{
    					"key": "",
    					"value": ""
    				}]
    			}]
    		},
    		"component": "CircleShape",
    		"label": "圆形",
    		"propValue": "\u0026nbsp;",
    		"icon": "24gl-circle",
    		"style": {
    			"rotate": 0,
    			"opacity": 1,
    			"width": 180,
    			"height": 180,
    			"fontSize": "",
    			"fontWeight": 400,
    			"lineHeight": "",
    			"letterSpacing": 0,
    			"textAlign": "center",
    			"color": "rgba(213, 148, 27, 1)",
    			"borderColor": "rgba(110, 204, 17, 1)",
    			"borderWidth": 10,
    			"backgroundColor": "rgba(186, 104, 104, 1)",
    			"borderStyle": "solid",
    			"borderRadius": "",
    			"verticalAlign": "middle",
    			"top": 188,
    			"left": 497
    		},
    		"id": "hOIKf550JqWwA1uM3KGtD"
    	}]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167

    如果需要对接后端,记录canvasStyleData、componentData即可。

    生成预览

    本项目中有一个页面预览的封装组件
    src\components\Editor\Preview.vue
    预览的方案就是先根据画布数据(canvasStyleData)新建一个总container,然后在该container中遍历组件数据(componentData),然后通过component组件和is属性实现动态组件的渲染还原。
    在这里插入图片描述

    源码下载

    源码链接:https://gitee.com/qkongtao/visual-drag-demo

  • 相关阅读:
    开环模块化多电平换流器仿真(MMC)N=6(Simulink仿真)
    机器学习(一)数据的预处理
    Windows 10下安装labelImg标注工具!
    【Java从入门到精通】这也许就是Java火热的原因吧!
    jupyter 挂掉的内核和transform=torchvision.transforms.ToTensor()
    数据结构与算法分析1
    开启深度学习之门—《深度学习》
    【Java集合】ArrayDeque源码解读
    现在的湖仓一体像是个伪命题
    Windows虚拟机部署Docker
  • 原文地址:https://blog.csdn.net/qq_42038623/article/details/127963112