Based on thevue, the following code part is directly added as a component display can be run, is the complete code

Business pain points

  • Repeated development of fixed type data display page (repeated development of active page)
  • Requirements change frequently
  • Short development time
  • Shortage of development manpower
  • Development work is piecemeal and costly to maintain
  • The development process involves high product/operational communication costs

Solution:

  • Component reuse
  • Drag and drop to generate
  • Configuration changes

It’s like a game engine

The main points of

  • componentization
    • Dynamic components Generate components of corresponding types based on component information
  • Dashboard canvas
    • Arrays hold each component
    • Drag and zoom to delete layers to zoom in and out the adsorption line configuration
    • Configure the form JSON Schema
  • Component interaction
    • Eventbus publishes subscriptions
    • Fixed type Fixed event
    • Binding during initialization
  • The component data
    • Configuring the Request API
  • Page preview
    • Modify page state to read cached page data
    • Page mount
      • Real-time rendering fast
      • Possible contamination of editor pages
  • The background render
  • Page to share
    • Page data is saved and the data ID is obtained. According to the ID, the sharing page is generated
  • Real-time data update
    • Websocket corresponds to the ID request

layout

Overall page layout:

  • Toolbar: Quick operations
  • Component list: Components that can be generated
  • Canvas: Dashboard is used to place components
  • Property editing area: Modify component properties

Ideas:

  1. With an arraycomponentDataMaintain component data on the canvas
  2. Drag the component onto the canvas and usepushAdds the component’s data tocomponentData
  3. Use dynamic components andv-forCome and takecomponentDataTo render the component

Drag and drop

Ideas:

  1. dragstartStart dragging and dropping to cache component information
  2. dropDrag end adds cached information tocomponentDataTo triggerdropEvent is used whendataTransfer.getData()It receives the index data, finds the corresponding component data based on the index, and adds it to the canvas to render the component.
  3. First you need to set the layout to a relative positionposition: relative, and then set each component to absolute positioningposition: absolute. In addition to this, there are three events to listen for for movement:
  • mousedownEvent that records the current position of the component when the mouse is pressed
  • mousemoveEvent, each time the mouse moves, with the current latest coordinates minus the beginning coordinates, so as to calculate the moving distance, and then change the position of the component
  • mouseupEvent to end the movement when the mouse is lifted.

<template>
  <div class="content-wrap">
    <! -- Component list -->
    <div @dragstart="handleDragStart" class="component-list">
      <div
        v-for="(item, index) in componentList"
        :key="index"
        class="list"
        draggable
        :data-index="index"
      >
        <! -- Custom components -->
        <span>{{ item.label }}</span>
      </div>
    </div>
    <! - drawing board -- -- >
    <div
      class="content"
      @drop="handleDrop"
      @dragover="handleDragOver"
      @mousedown="handleMouseDown"
      ref="content"
    >
      <div
        v-for="(item, index) in componentData"
        :key="index"
        class="list"
        :data-index="index"
        :style="item.style"
      >
        <! -- Custom components -->
        <span>{{ item.label }}</span>
      </div>
    </div>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        componentList: [{label: "Text 1"}, {label: "Text 2"}, {label: "Text 3"],},componentData: [].operation: ""}; },methods: {
      handleDragStart(e) {
        e.dataTransfer.setData("index", e.target.dataset.index);
        this.operation = "drag";
      },
      handleDrop(e) {
        e.preventDefault();
        e.stopPropagation();

        // Drag the component
        let selected = this.componentList[e.dataTransfer.getData("index")];
        if(! selected)return false;
        const component = JSON.parse(JSON.stringify(selected));
        component.style = {
          top: e.offsetY + "px".left: e.offsetX + "px"};const tmp = [...this.componentData, component];
        // Add component information to the canvas component information queue
        this.$set(this."componentData", tmp);
      },
      handleDragOver(e) {
        // This setting is required to trigger drop
        // 1. Mask default events
        // 2. Set dropEffect = copy
        e.preventDefault();
        if (this.operation === "drag") e.dataTransfer.dropEffect = "copy";
        if (this.operation === "move") {
          return false; }},handleMouseDown(e) {
        e.stopPropagation();
        this.operation = "move";
        // Move the selected component
        let selected = this.componentData[e.target.parentNode.dataset.index];
        if(! selected)return false;
        const component = JSON.parse(JSON.stringify(selected));
        const pos = component.style;
        const startY = e.clientY;
        const startX = e.clientX;
        const startTop = Number(pos.top.replace("px".""));
        const startLeft = Number(pos.left.replace("px".""));

        // mousemove modifies the position
        const move = (moveEvent) = > {
          const currX = moveEvent.clientX;
          const currY = moveEvent.clientY;
          pos.top = currY - startY + startTop;
          pos.left = currX - startX + startLeft;
          // Modify the current component style
          selected.style = {
            top: pos.top + "px".left: pos.left + "px"}; };// mouseup unbinds events
        const up = () = > {
          document.removeEventListener("mousemove", move);
          document.removeEventListener("mouseup", up);
        };

        document.addEventListener("mousemove", move);
        document.addEventListener("mouseup", up); ,}}};</script>

<style>
  .content-wrap {
    display: flex;
    flex-direction: row;
  }
  .component-list {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    padding: 10px;
    height: 200px;
    width: 100px;
    border: 1px solid lightblue;
  }
  .content {
    height: 200px;
    flex: 1;
    border: 1px solid lightblue;
    padding: 10px;
    margin-left: 10px;
    position: relative;
  }
  .list {
    height: fit-content;
    width: fit-content;
    border: 1px solid grey;
    cursor: grab;
    margin-bottom: 10px;
    text-align: center;
    color: # 333;
    padding: 2px 5px;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .content .list {
    position: absolute;
  }
</style>
Copy the code

Delete components and adjust layers

Since dragging components onto the canvas is sequential, you can assign layers in order of data.

Changing the layer hierarchy changes the order of the componentData in the componentData array.

<template> <! <div class="content" @mousedown="handleMouseDown" ref="content" @click="showMenu = false" >  index) in componentData" :key="index" class="list" :data-index="index" :style="Object.assign(item.style, { zIndex: index })" @contextmenu="handleMenu" > <! <span> </span> </div> <div class="menu" v-show="showMenu" :style="menuPos" @click="handleCommand"> <div data-command="up"> move </div> <div data-command="down"> move </div> <div Data-command ="bottom"> </div> <div data-command="top"> </div> <div data-command="delete"> </div> </div> </template> <script> export default { data() { return { componentData: [ { style: { top: "0", left: "0", background: }, {style: {top: "20px", left: "10px", BACKGROUND: "lightgrey",}, label: }, {style: {top: "40px", left: "20px", BACKGROUND: "lightgreen",}, label: "lightgreen",},], showMenu: false, menuPos: {}, currentComponent: null, }; }, methods: { handleMouseDown(e) { e.stopPropagation(); this.operation = "move"; / / move the selected components selected = this.com ponentData [e. arget. ParentNode. Dataset. The index]; if (! selected) return false; const component = JSON.parse(JSON.stringify(selected)); const pos = component.style; const startY = e.clientY; const startX = e.clientX; const startTop = Number(pos.top.replace("px", "")); const startLeft = Number(pos.left.replace("px", "")); // mousemove const move = (moveEvent) => {const currX = moveEvent. ClientX; const currY = moveEvent.clientY; pos.top = currY - startY + startTop; pos.left = currX - startX + startLeft; Style = {top: pos.top + "px", left: pos.left + "px", background: pos.background,}; }; / / remove mouseup event binding const up = () = > {document. The removeEventListener (" mousemove ", a move); document.removeEventListener("mouseup", up); }; document.addEventListener("mousemove", move); document.addEventListener("mouseup", up); }, handleMenu(e) { e.preventDefault(); this.currentComponent = e.currentTarget.dataset.index; this.menuPos = { top: e.offsetY + "px", left: e.offsetX + "px", }; this.showMenu = true; }, handleCommand(e) { const command = e.target.dataset.command; const len = this.componentData.length - 1; const tmp = [...this.componentData]; const curIndex = +this.currentComponent; switch (command) { case "up": if (curIndex ! = len) { [tmp[curIndex + 1], tmp[curIndex]] = [ tmp[curIndex], tmp[curIndex + 1], ]; this.$set(this, "componentData", tmp); } break; case "down": if (curIndex ! = 0) { [tmp[curIndex - 1], tmp[curIndex]] = [ tmp[curIndex], tmp[curIndex - 1], ]; this.$set(this, "componentData", tmp); } break; case "bottom": if (curIndex ! = 0) { tmp.unshift(... tmp.splice(curIndex, 1)); this.$set(this, "componentData", tmp); } break; case "top": if (curIndex ! = len) { tmp.push(... tmp.splice(curIndex, 1)); this.$set(this, "componentData", tmp); } break; case "delete": tmp.splice(curIndex, 1); this.$set(this, "componentData", tmp); break; ,}}}}; </script> <style> .content { height: 200px; border: 1px solid lightblue; padding: 10px; margin-left: 10px; position: relative; } .list { height: fit-content; width: fit-content; border: 1px solid grey; cursor: grab; margin-bottom: 10px; text-align: center; color: #333; padding: 2px 5px; display: flex; align-items: center; justify-content: center; } .content .list { position: absolute; } .menu { border: 1px solid #000; width: fit-content; z-index: 999; position: absolute; background: lightcyan; } .menu div { border: 1px solid #000; cursor: pointer; padding: 0 3px; } .menu div:hover { background: lightblue; } </style>Copy the code

Zoom in on

Select the component on the canvas, the component outer circle will appear 8 small dots can be dragged to zoom in and out.

Ideas:

  1. Each component is covered by a layer of components, which contains eight dots and slots for placing components
  2. Click on the component to display the dot via style control
  3. Calculate the position of each dot (to display on the outside of the component, also calculate the size of the dot, remember that the length and width of the dot arew) :
  • Upper leftleft:0-w top:0-w
  • The upper rightleft:width top:0-w
  • The lower leftleft:0-w top:height
  • The lower rightleft:width top:height
  • In the middle of the pointswidth/2 height/2In the same way to calculate
  1. Click on the dot to zoom in and out
  • Click on the dot to record the initial coordinates
  • Drag down to get the distance moved by subtracting the initial coordinates from the new y-coordinate and adding the distance to the height of the component to get the new heightmovementMouse movement distance can also be calculated)
  • Only the height can be changed up and down. Only the width can be changed left and right

<template> <! <div class="content" @click="contentClick"> <! <div v-for="(item, index) in componentData" :key="index" class="component" :style="item.style" :data-index="index" @click=" (e) => { e.stopPropagation(); selected = index; } "> <! - custom components -- -- > < div > custom components < / div > < div class = "dots" v - for = "(dot, I) in dots" : the key = "I" is "style =" {left: dot [0] + 'p', the top: dot[1] + 'px', width: dotSize + 'px', height: dotSize + 'px', cursor: dot[2], }" @mousedown="handleMouseDown" :data-type="dot[2].split('-')[0]" ></div> </div> </div> </template> <script> export default { data() { return { componentData: [ { style: { top: "30px", left: "30px", height: "100px", width: "100px", background: "lightblue", }, }, ], selected: null, dotSize: 4, }; }, methods: { contentClick() { this.selected = null; }, // componentClick(index) { // return function(e) { // e.stopPropagation(); // this.selected = index; / /}; // }, handleMouseDown(e) { const component = this.componentData[this.selected]; const type = e.target.dataset.type; const move = (me) => { let t = { ... component.style }; Replace ("px", "")) + me. MovementX + "px"; if (type.indexof ("e") > -1). If (type.indexof ("s") > -1) t.eight = Number(t.eight. Replace ("px", "")) + me.movementy + "px"; If (type.indexof ("w") > -1) {t.eft = Number(t.eft.replace ("px", "")) + me.movementX + "px"; t.width = Number(t.width.replace("px", "")) - me.movementX + "px"; } if (type.indexOf("n") > -1) { t.top = Number(t.top.replace("px", "")) + me.movementY + "px"; t.height = Number(t.height.replace("px", "")) - me.movementY + "px"; } this.$set(this.componentData[this.selected], "style", t); }; const up = () => { document.removeEventListener("mousemove", move); document.removeEventListener("mouseup", up); }; document.addEventListener("mousemove", move); document.addEventListener("mouseup", up); }, }, computed: { dots() { if (this.selected ! == null) { const component = this.componentData[this.selected]; const width = +component.style.width.replace("px", ""); const height = +component.style.height.replace("px", ""); return [ [0 - this.dotSize, 0 - this.dotSize, "nw-resize"], [0 - this.dotSize, height, "sw-resize"], [width, 0 - this.dotSize, "ne-resize"], [width, height, "se-resize"], [width / 2, 0 - this.dotSize, "n-resize"], [width / 2, height, "s-resize"], [0 - this.dotSize, height / 2, "w-resize"], [width, height / 2, "e-resize"], ]; } return []; ,}}}; </script> <style> .content { height: 200px; flex: 1; border: 1px solid lightblue; padding: 10px; margin-left: 10px; position: relative; } .component { position: absolute; height: fit-content; width: fit-content; text-align: center; color: #333; display: flex; align-items: center; justify-content: center; } .dots { border: 1px solid lightblue; position: absolute; } </style>Copy the code

Adsorption of alignment,

Refer to Sketch ink knife

As you can see, a component in the canvas can from 6 line (vt/vm/vb | hl/hm/hr), component alignment is in the process of moving components of 6 line to other assembly line in the collection to find near the line, found after considering the adsorption + alignment process.

Align adsorption: take horizontal left movement as an example, its HL/hm/HR will constantly look for the line closest to these three lines.

  • When no adjacent lines are found, the component moves with the mouse
  • When first found, the component is moved a large distance to adsorb
  • When moving on the adsorption line again, continue to look for the adjacent line to see if there is a next adsorption line
    • If so, move to the next adsorption line
    • If not, the component leaves after the mouse moves a certain distance

Specific (A is mobile component B is fixed component) :

  • If you move down, the following will happen
    • A bottom = B top shows horizontal bottom line
    • A top = B top shows horizontal top line
    • A middle = B Middle shows horizontal middle line
    • A bottom = B bottom shows horizontal bottom line
    • A top = B Bottom shows horizontal top line
  • Same thing with left and right
  • Move up and appear down in reverse order

Construct two arrays to hold:

  • Determine whether the conditions meet the adsorption
  • The type of line displayed
  • The position of the line after adsorption
  • Position of component after adsorption

The array is iterated sequentially, exiting as long as conditions are met, and only one line is displayed in each direction

<template>
  <!-- 画板 -->
  <div class="content" @click="selected = null">
    <!-- 模拟外层包裹组件 -->
    <div
      v-for="(item, index) in componentData"
      :key="index"
      class="component"
      :style="item.style"
      :data-index="index"
      @mousedown="handleMouseDown"
    >
      <!-- 自定义组件 -->
      <!-- <div>自定义组件</div> -->
    </div>

    <!-- 标线 -->
    <div class="mark-line">
      <div
        v-for="line in lines"
        :key="line"
        class="line"
        :class="line.includes('x') ? 'xline' : 'yline'"
        :ref="line"
        v-show="lineStatus[line] || false"
      ></div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      componentData: [
        {
          style: {
            top: "0",
            left: "0",
            width: "75px",
            height: "75px",
            background: "lightblue",
          },
        },
        {
          style: {
            top: "100px",
            left: "100px",
            width: "100px",
            height: "100px",
            background: "lightgrey",
          },
        },
      ],
      currentComponent: null,
      lines: ["xt", "xc", "xb", "yl", "yc", "yr"], // 分别对应三条横线和三条竖线
      diff: 5, // 相距 dff 像素将自动吸附
      lineStatus: {
        xt: false, // 水平顶部
        xc: false, // 水平中间
        xb: false, // 水平底部
        yl: false, // 垂直左边
        yc: false, // 垂直中间
        yr: false, // 垂直右边
      },
    };
  },
  methods: {
    hideLine() {
      Object.keys(this.lineStatus).forEach((line) => {
        this.lineStatus[line] = false;
      });
    },
    showLine(currentComponent, index, top, left, toDown, toRight) {
      const rest = this.componentData.filter((c, i) => i != index)[0];
      const rwidth = +rest.style.width.replace("px", "");
      const rheight = +rest.style.height.replace("px", "");
      const rtop = +rest.style.top.replace("px", "");
      const rleft = +rest.style.left.replace("px", "");
      const width = +currentComponent.style.width.replace("px", "");
      const height = +currentComponent.style.height.replace("px", "");
      this.hideLine();
      const lines = this.$refs;
      let changeLeft = left,
        changeTop = top;

      const rules = {
        updown: [
          {
            condition: top + height < rtop && top + height + this.diff >= rtop,
            line: "xb",
            ltop: rtop,
            top: rtop - height,
          },
          {
            condition: top < rtop && top + this.diff >= rtop,
            line: "xt",
            ltop: rtop,
            top: rtop,
          },
          {
            condition:
              top + height / 2 < rtop + rheight / 2 &&
              top + height / 2 + this.diff >= rtop + rheight / 2,
            line: "xc",
            ltop: rtop + rheight / 2,
            top: rtop + rheight / 2 - height / 2,
          },
          {
            condition:
              top + height < rtop + rheight &&
              top + height + this.diff >= rtop + rheight,
            line: "xb",
            ltop: rtop + rheight,
            top: rtop + rheight - height,
          },
          {
            condition:
              top < rtop + rheight && top + this.diff >= rtop + rheight,
            line: "xt",
            ltop: rtop + rheight,
            top: rtop + rheight,
          },
        ],
        leftright: [
          {
            condition:
              left + width < rleft && left + width + this.diff >= rleft,
            line: "yr",
            lleft: rleft,
            left: rleft - width,
          },
          {
            condition: left < rleft && left + this.diff >= rleft,
            line: "yl",
            lleft: rleft,
            left: rleft,
          },
          {
            condition:
              left + width / 2 < rleft + rwidth / 2 &&
              left + width / 2 + this.diff >= rleft + rwidth / 2,
            line: "yc",
            lleft: rleft + rwidth / 2,
            left: rleft + rwidth / 2 - width / 2,
          },
          {
            condition:
              left + width < rleft + rwidth &&
              left + width + this.diff >= rleft + rwidth,
            line: "yr",
            lleft: rleft + rwidth,
            left: rleft + rwidth - width,
          },
          {
            condition:
              left < rleft + rwidth && left + this.diff >= rleft + rwidth,
            line: "yl",
            lleft: rleft + rwidth,
            left: rleft + rwidth,
          },
        ],
      };

      // 向上顺序相反
      if (!toDown) {
        rules.updown.reverse();
      }
      for (let t of rules.updown) {
        if (t.condition) {
          this.lineStatus[t.line] = true;
          lines[t.line][0].style.left = 0;
          lines[t.line][0].style.top = t.ltop + "px";
          changeTop = t.top;
          break;
        }
      }
      // 向左相反
      if (!toRight) {
        rules.leftright.reverse();
      }
      for (let t of rules.leftright) {
        if (t.condition) {
          this.lineStatus[t.line] = true;
          lines[t.line][0].style.left = t.lleft + "px";
          lines[t.line][0].style.top = 0;
          changeLeft = t.left;
          break;
        }
      }

      const style = currentComponent.style;

      this.$set(this.componentData, index, {
        style: Object.assign(style, {
          left: changeLeft + "px",
        }),
      });

      this.$set(this.componentData, index, {
        style: Object.assign(style, {
          top: changeTop + "px",
        }),
      });
    },
    handleMouseDown(e) {
      e.stopPropagation();
      // 移动选中的组件
      let selected = this.componentData[e.target.dataset.index];
      if (!selected) return false;
      const component = JSON.parse(JSON.stringify(selected));
      const pos = component.style;
      const startY = e.clientY;
      const startX = e.clientX;
      const startTop = Number(pos.top.replace("px", ""));
      const startLeft = Number(pos.left.replace("px", ""));

      // mousemove 修改位置
      const move = (moveEvent) => {
        const currX = moveEvent.clientX;
        const currY = moveEvent.clientY;
        const toDown = currY - startY > 0 ? true : false;
        const toRight = currX - startX > 0 ? true : false;
        pos.top = currY - startY + startTop;
        pos.left = currX - startX + startLeft;
        this.showLine(
          selected,
          e.target.dataset.index,
          pos.top,
          pos.left,
          toDown,
          toRight
        );
        // 修改当前组件样式
        selected.style = {
          top: pos.top + "px",
          left: pos.left + "px",
          background: pos.background,
          width: pos.width,
          height: pos.height,
        };
      };

      // mouseup 解除事件绑定
      const up = () => {
        document.removeEventListener("mousemove", move);
        document.removeEventListener("mouseup", up);
        this.hideLine();
      };

      document.addEventListener("mousemove", move);
      document.addEventListener("mouseup", up);
    },
  },
};
</script>

<style>
.content {
  height: 200px;
  flex: 1;
  border: 1px solid lightblue;
  margin-left: 10px;
  position: relative;
}

.component {
  position: absolute;
  height: fit-content;
  width: fit-content;
  text-align: center;
  color: #333;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.mark-line {
  height: 100%;
}
.line {
  background: #59c7f9;
  position: absolute;
  z-index: 1000;
}
.xline {
  width: 100%;
  height: 1px;
}
.yline {
  width: 1px;
  height: 100%;
}
</style>
Copy the code

Attribute set

Click the component to display the corresponding property Settings, modify the property component style to apply the changes.

Ideas:

  1. Click component to bind the component’s style object to the property Settings
  2. Use bidirectional binding to modify the style as the property is modified

<template> <div class="prop-wrapper"> <! <div class="prop-content" ref="content" @click="currentComponent = null"> index) in componentData" :key="index" class="list" :data-index="index" :style="Object.assign(item.style)" @click="handleClick" > <! <div class="prop-div"> <div v-show="currentComponent" v-for=" index) in Object.keys( currentComponent ? Currentcomponent. style: {})" :key="index" > <label> {{key}} : </label> <! <input V-model ="currentComponent.style[key]" /> </div> </div> </template> <script> export default { data() { return { componentData: [ { style: { top: "20px", left: "20px", background: "lightblue", height: "100px", width: "100px", }, }, { style: { top: "20px", left: "110px", background: "lightgrey", height: "100px", width: "100px", }, }, ], currentComponent: null, }; }, methods: { handleClick(e) { e.stopPropagation(); console.log(e.target); this.currentComponent = this.componentData[e.target.dataset.index]; ,}}}; </script> <style> .prop-wrapper { border: 1px solid lightblue; display: flex; flex-direction: row; padding: 10px; } .prop-div { margin: auto; width: 300px; height: 200px; border: 1px solid lightblue; } .prop-div div { display: flex; flex-direction: row; justify-content: center; margin-top: 5px; } .prop-div div label { width: 120px; } .prop-content { height: 200px; border: 1px solid lightblue; margin: 0 20px; flex: 1; position: relative; } .prop-content .list { position: absolute; height: fit-content; width: fit-content; border: 1px solid grey; cursor: grab; margin-bottom: 10px; text-align: center; color: #333; padding: 2px 5px; display: flex; align-items: center; justify-content: center; } </style>Copy the code

Data request

Default custom data can be added as a property in componentData

Remote data can be configured with a URL property that subscribes to updates based on the component ID using websocket

Combination of splitting

Technical points:

  • Select the area
    • An added borderdivTo display the
    • mouseDown
      • The canvas determines the starting position settingshowArea = true
    • mouseMove
      • Update region size basisoffet-startPositive and negative cases modify the positioning style
    • mouseUp
      • Determines the selected component to display the selected region
        • Add the array according to whether the upper-left corner and width are fully wrapped around the component
        • Iterate over the number group to update the four vertices based on the vertex build selected region style
      • Remove event binding SettingsshowArea = false
    • The problem
      • OffsetY sometimes becomes half when moving. At first I thought it was caused by bubbling, but it didn’t work to prevent bubbling
        • The reason is that during Mousemove, the triggered event source element may become a region element, resulting in a change in relative position
        • Solution: Add on the region element stylepointer-events: none;Makes the area element never a mouse eventtarget
  • Movement after combination,rotating
  • Zoom in and out after combination
  • Recovery of child component styles after splitting

After combination, the component information is deleted from the array, the selected component information is re-generated composite components, and then added to the array split, each component information of the composite components is recalculated, and then added to the component array

Note: The following code only does the selected part

<template> <! @click="selected = null" @mousedown="contentMouseDown" > <div class="content" ref="content" @click="selected = null" @mousedown="contentMouseDown" > <div v-for="(item, index) in componentData" :key="index" class="combine-component" :style="item.style" :data-index="index" @mousedown="handleMouseDown" > <! </div> <! Area - - - > < div class = "area" v - show = "showArea:" style = "areaStyle" > < / div > <! <div class="area" V-show ="showSelectArea" :style="selectAreaStyle"></div> </div> </template> <script> export  default { data() { return { componentData: [ { style: { top: "30px", left: "30px", width: "50px", height: "50px", background: "lightblue", }, }, { style: { top: "30px", left: "100px", width: "50px", height: "50px", background: "lightgrey", }, }, ], currentComponent: null, showArea: false, showSelectArea: false, areaStyle: {}, selectAreaStyle: {}, result: [], }; }, methods: { handleMouseDown() {}, contentMouseDown(e) { this.result = []; const startX = e.offsetX; const startY = e.offsetY; // Const width = this.$refs.content.clientWidth; const height = this.$refs.content.clientHeight; const move = (e) => { const curX = e.offsetX; const curY = e.offsetY; this.areaStyle = Object.assign( {}, { position: "absolute", width: Math.abs(curX - startX) + "px", height: Math.abs(curY - startY) + "px", }, curX - startX >= 0 ? { left: startX + "px" } : { right: width - startX + "px" }, curY - startY >= 0 ? { top: startY + "px" } : { bottom: height - startY + "px" } ); this.showArea = true; }; const up = (e) => { console.log(e); const style = Object.assign({}, this.areaStyle); for (let k in style) { style[k] = +style[k].replace("px", ""); } const x = style.left || width - (style.right + style.width); const y = style.top || height - (style.bottom + style.height); const awidth = style.width; const aheight = style.height; this.componentData.forEach((component) => { let { left, top } = component.style; let cwidth = component.style.width; let cheight = component.style.height; left = +left.replace("px", ""); top = +top.replace("px", ""); cwidth = +cwidth.replace("px", ""); cheight = +cheight.replace("px", ""); if ( x <= left && y <= top && left + cwidth <= x + awidth && top + cheight <= y + aheight ) { this.result.push(component); }}); this.showArea = false; this.areaStyle = {}; this.$refs.content.removeEventListener("mousemove", move); this.$refs.content.removeEventListener("mouseup", up); }; this.$refs.content.addEventListener("mousemove", move); this.$refs.content.addEventListener("mouseup", up); }, }, watch: { result(val) { if (val.length) { let aleft = Infinity, atop = Infinity, aright = -Infinity, abottom = -Infinity; val.forEach((v) => { let { left, top, width, height } = v.style; left = +left.replace("px", ""); top = +top.replace("px", ""); height = +height.replace("px", ""); width = +width.replace("px", ""); if (left < aleft) aleft = left; if (top < atop) atop = top; if (left + width > aright) aright = left + width; if (top + height > abottom) abottom = top + height; }); this.selectAreaStyle = { position: "absolute", top: atop + "px", left: aleft + "px", width: aright - aleft + "px", height: abottom - atop + "px", }; this.showSelectArea = true; } else { this.showSelectArea = false; this.selectAreaStyle = {}; ,}}}}; </script> <style> .content { height: 200px; flex: 1; border: 1px solid lightblue; margin-left: 10px; position: relative; } .combine-component { position: absolute; height: fit-content; width: fit-content; text-align: center; color: #333; display: flex; align-items: center; justify-content: center; cursor: pointer; pointer-events: none; } .area { border: 1px solid lightseagreen; pointer-events: none; } </style>Copy the code

reference

  • Visual drag component library some technical points of principle analysis
  • How does Cloud Butterfly create a free canvas comparable to Sketch
  • Domestic low code platform