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:
- With an array
componentData
Maintain component data on the canvas - Drag the component onto the canvas and use
push
Adds the component’s data tocomponentData
- Use dynamic components and
v-for
Come and takecomponentData
To render the component
Drag and drop
Ideas:
dragstart
Start dragging and dropping to cache component informationdrop
Drag end adds cached information tocomponentData
To triggerdrop
Event 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.- First you need to set the layout to a relative position
position: relative
, and then set each component to absolute positioningposition: absolute
. In addition to this, there are three events to listen for for movement:
mousedown
Event that records the current position of the component when the mouse is pressedmousemove
Event, 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 componentmouseup
Event 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:
- Each component is covered by a layer of components, which contains eight dots and slots for placing components
- Click on the component to display the dot via style control
- 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 are
w
) :
- Upper left
left:0-w top:0-w
- The upper right
left:width top:0-w
- The lower left
left:0-w top:height
- The lower right
left:width top:height
- In the middle of the points
width/2 height/2
In the same way to calculate
- 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 height
movement
Mouse 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:
- Click component to bind the component’s style object to the property Settings
- 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 border
div
To display the mouseDown
- The canvas determines the starting position setting
showArea = true
- The canvas determines the starting position setting
mouseMove
- Update region size basis
offet-start
Positive and negative cases modify the positioning style
- Update region size basis
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 Settings
showArea = false
- Determines the selected component to display the selected region
- 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 style
pointer-events: none;
Makes the area element never a mouse eventtarget
- OffsetY sometimes becomes half when moving. At first I thought it was caused by bubbling, but it didn’t work to prevent bubbling
- An added border
- 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