,
20 | required: true,
21 | },
22 | },
23 | setup(props) {
24 | const { width, height } = props.component.resize || {};
25 |
26 | const onMousedown = (() => {
27 | let data = {
28 | startX: 0,
29 | startY: 0,
30 | startWidth: 0,
31 | startHeight: 0,
32 | startLeft: 0,
33 | startTop: 0,
34 | direction: {} as { horizontal: Direction; vertical: Direction },
35 | };
36 |
37 | const mousemove = (e: MouseEvent) => {
38 | const {
39 | startX,
40 | startY,
41 | startWidth,
42 | startHeight,
43 | direction,
44 | startLeft,
45 | startTop,
46 | } = data;
47 | let { clientX: moveX, clientY: moveY } = e;
48 | if (direction.horizontal === Direction.center) {
49 | moveX = startX;
50 | }
51 | if (direction.vertical === Direction.center) {
52 | moveY = startY;
53 | }
54 |
55 | let durX = moveX - startX;
56 | let durY = moveY - startY;
57 | const block = props.block as VisualEditorBlockData;
58 |
59 | if (direction.vertical === Direction.start) {
60 | durY = -durY;
61 | block.top = startTop - durY;
62 | }
63 | if (direction.horizontal === Direction.start) {
64 | durX = -durX;
65 | block.left = startLeft - durX;
66 | }
67 |
68 | const width = startWidth + durX;
69 | const height = startHeight + durY;
70 |
71 | block.width = width;
72 | block.height = height;
73 | block.hasResize = true;
74 | };
75 |
76 | const mouseup = (e: MouseEvent) => {
77 | console.log(e);
78 | document.body.removeEventListener("mousemove", mousemove);
79 | document.body.removeEventListener("mouseup", mouseup);
80 | };
81 | const mousedown = (
82 | e: MouseEvent,
83 | direction: { horizontal: Direction; vertical: Direction }
84 | ) => {
85 | e.stopPropagation();
86 | document.body.addEventListener("mousemove", mousemove);
87 | document.body.addEventListener("mouseup", mouseup);
88 | data = {
89 | startX: e.clientX,
90 | startY: e.clientY,
91 | direction,
92 | startWidth: props.block.width,
93 | startHeight: props.block.height,
94 | startLeft: props.block.left,
95 | startTop: props.block.top,
96 | };
97 | };
98 |
99 | return mousedown;
100 | })();
101 |
102 | return () => (
103 | <>
104 | {height && (
105 | <>
106 |
109 | onMousedown(e, {
110 | horizontal: Direction.center,
111 | vertical: Direction.start,
112 | })
113 | }
114 | >
115 |
118 | onMousedown(e, {
119 | horizontal: Direction.center,
120 | vertical: Direction.end,
121 | })
122 | }
123 | >
124 | >
125 | )}
126 |
127 | {width && (
128 | <>
129 |
132 | onMousedown(e, {
133 | horizontal: Direction.start,
134 | vertical: Direction.center,
135 | })
136 | }
137 | >
138 |
141 | onMousedown(e, {
142 | horizontal: Direction.end,
143 | vertical: Direction.center,
144 | })
145 | }
146 | >
147 | >
148 | )}
149 |
150 | {width && height && (
151 | <>
152 |
155 | onMousedown(e, {
156 | horizontal: Direction.start,
157 | vertical: Direction.start,
158 | })
159 | }
160 | >
161 |
164 | onMousedown(e, {
165 | horizontal: Direction.end,
166 | vertical: Direction.start,
167 | })
168 | }
169 | >
170 |
171 |
174 | onMousedown(e, {
175 | horizontal: Direction.start,
176 | vertical: Direction.end,
177 | })
178 | }
179 | >
180 |
183 | onMousedown(e, {
184 | horizontal: Direction.end,
185 | vertical: Direction.end,
186 | })
187 | }
188 | >
189 | >
190 | )}
191 | >
192 | );
193 | },
194 | });
195 |
--------------------------------------------------------------------------------
/src/lib/iconfont/iconfont.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
63 |
--------------------------------------------------------------------------------
/src/lib/iconfont/iconfont.js:
--------------------------------------------------------------------------------
1 | !function(t){var c,e,o,a,h,l,s='',i=(i=document.getElementsByTagName("script"))[i.length-1].getAttribute("data-injectcss");if(i&&!t.__iconfont__svg__cssinject__){t.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(t){console&&console.log(t)}}function n(){h||(h=!0,o())}c=function(){var t,c,e,o;(o=document.createElement("div")).innerHTML=s,s=null,(e=o.getElementsByTagName("svg")[0])&&(e.setAttribute("aria-hidden","true"),e.style.position="absolute",e.style.width=0,e.style.height=0,e.style.overflow="hidden",t=e,(c=document.body).firstChild?(o=t,(e=c.firstChild).parentNode.insertBefore(o,e)):c.appendChild(t))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(c,0):(e=function(){document.removeEventListener("DOMContentLoaded",e,!1),c()},document.addEventListener("DOMContentLoaded",e,!1)):document.attachEvent&&(o=c,a=t.document,h=!1,(l=function(){try{a.documentElement.doScroll("left")}catch(t){return void setTimeout(l,50)}n()})(),a.onreadystatechange=function(){"complete"==a.readyState&&(a.onreadystatechange=null,n())})}(window);
--------------------------------------------------------------------------------
/src/packages/visual-editor.tsx:
--------------------------------------------------------------------------------
1 | import { computed, defineComponent, PropType, ref, reactive } from "vue";
2 | import { useModel } from "./utils/useModel";
3 | import { VisualEditorBlock } from "./visual-editor-block";
4 | import "./visual-editor.scss";
5 | import {
6 | createNewBlock,
7 | VisualEditorBlockData,
8 | VisualEditorComponent,
9 | VisualEditorConfig,
10 | VisualEditorMarkLine,
11 | VisualEditorModelValue,
12 | } from "./visual-editor.utils";
13 | import { VisualOperatorEditor } from "./visual-editor-operator";
14 | import deepcopy from "deepcopy";
15 |
16 | export const VisualEditor = defineComponent({
17 | props: {
18 | modelValue: {
19 | type: Object as PropType,
20 | require: true,
21 | },
22 | config: {
23 | type: Object as PropType,
24 | require: true,
25 | },
26 | },
27 | emits: {
28 | "update:modelValue": (val?: VisualEditorModelValue) => true,
29 | },
30 | setup(props, ctx) {
31 | // 双向数据绑定
32 | const dataModel = useModel(
33 | () => props.modelValue,
34 | (val) => ctx.emit("update:modelValue", val)
35 | );
36 | // container样式
37 | const containerStyles = computed(() => ({
38 | width: `${props.modelValue?.container.width}px`,
39 | height: `${props.modelValue?.container.height}px`,
40 | }));
41 | // container dom引用
42 | const containerRef = ref({} as HTMLElement);
43 |
44 | // 计算选中与未选中的block数据
45 | const focusData = computed(() => {
46 | const focus: VisualEditorBlockData[] =
47 | dataModel.value?.blocks.filter((v) => v.focus) || [];
48 | const unfocus: VisualEditorBlockData[] =
49 | dataModel.value?.blocks.filter((v) => !v.focus) || [];
50 | return {
51 | focus, // 此时选中的数据
52 | unfocus, // 此时未选中的数据
53 | };
54 | });
55 |
56 | // 对外暴露的一些方法
57 | const methods = {
58 | clearFocus: (block?: VisualEditorBlockData) => {
59 | let blocks = dataModel.value?.blocks || [];
60 | if (blocks.length === 0) return;
61 |
62 | if (block) {
63 | blocks = blocks.filter((v) => v !== block);
64 | }
65 | blocks.forEach((block) => (block.focus = false));
66 | },
67 | updateBlocks: (blocks: VisualEditorBlockData[]) => {
68 | dataModel.value!.blocks = blocks;
69 | },
70 | };
71 |
72 | // 处理菜单拖拽进容器
73 | const menuDragger = (() => {
74 | let component = null as null | VisualEditorComponent;
75 |
76 | const containerHandler = {
77 | /**
78 | * 拖拽组件进入容器,设置鼠标可放置状态
79 | */
80 | dragenter: (e: DragEvent) => {
81 | e.dataTransfer!.dropEffect = "move";
82 | },
83 | dragover: (e: DragEvent) => {
84 | e.preventDefault();
85 | },
86 | /**
87 | * 拖拽组件离开容器,设置鼠标禁用状态
88 | */
89 | dragleave: (e: DragEvent) => {
90 | e.dataTransfer!.dropEffect = "none";
91 | },
92 | /**
93 | * 在容器中放置组件
94 | */
95 | drop: (e: DragEvent) => {
96 | console.log("drop", component);
97 | const blocks = dataModel.value?.blocks || [];
98 | blocks.push(
99 | createNewBlock({
100 | component: component!,
101 | top: e.offsetY,
102 | left: e.offsetX,
103 | })
104 | );
105 | console.log("x", e.offsetX);
106 | console.log("y", e.offsetY);
107 | dataModel.value = {
108 | ...dataModel.value,
109 | blocks,
110 | } as VisualEditorModelValue;
111 | },
112 | };
113 |
114 | const blockHandler = {
115 | dragstart: (e: DragEvent, current: VisualEditorComponent) => {
116 | containerRef.value.addEventListener(
117 | "dragenter",
118 | containerHandler.dragenter
119 | );
120 | containerRef.value.addEventListener(
121 | "dragover",
122 | containerHandler.dragover
123 | );
124 | containerRef.value.addEventListener(
125 | "dragleave",
126 | containerHandler.dragleave
127 | );
128 | containerRef.value.addEventListener("drop", containerHandler.drop);
129 | component = current;
130 | },
131 | dragend: (e: DragEvent) => {
132 | containerRef.value.removeEventListener(
133 | "dragenter",
134 | containerHandler.dragenter
135 | );
136 | containerRef.value.removeEventListener(
137 | "dragover",
138 | containerHandler.dragover
139 | );
140 | containerRef.value.removeEventListener(
141 | "dragleave",
142 | containerHandler.dragleave
143 | );
144 | containerRef.value.removeEventListener("drop", containerHandler.drop);
145 | component = null;
146 | },
147 | };
148 |
149 | return blockHandler;
150 | })();
151 |
152 | // 当前选中的block
153 | const state = reactive({
154 | selectBlock: null as null | VisualEditorBlockData,
155 | });
156 |
157 | // 处理组件在画布上他拖拽
158 | const blockDragger = (() => {
159 | let dragState = {
160 | startX: 0,
161 | startY: 0,
162 | startPos: [] as { left: number; top: number }[],
163 |
164 | startLeft: 0,
165 | startTop: 0,
166 | markLines: {} as VisualEditorMarkLine,
167 | };
168 |
169 | // 用于视图展示的辅助线
170 | const mark = reactive({
171 | x: null as null | number,
172 | y: null as null | number,
173 | });
174 |
175 | const mousemove = (e: MouseEvent) => {
176 | let { clientX: moveX, clientY: moveY } = e;
177 |
178 | const { startX, startY } = dragState;
179 |
180 | // 按下shift键时,组件只能横向或纵向移动
181 | if (e.shiftKey) {
182 | // 当鼠标横向移动的距离 大于 纵向移动的距离,将纵向的偏移置为0
183 | if (Math.abs(e.clientX - startX) > Math.abs(e.clientY - startY)) {
184 | moveY = startY;
185 | } else {
186 | moveX = startX;
187 | }
188 | }
189 |
190 | const currentLeft = dragState.startLeft + moveX - startX;
191 | const currentTop = dragState.startTop + moveY - startY;
192 | const currentMark = {
193 | x: null as null | number,
194 | y: null as null | number,
195 | };
196 |
197 | for (let i = 0; i < dragState.markLines.y.length; i++) {
198 | const { top, showTop } = dragState.markLines.y[i];
199 | if (Math.abs(top - currentTop) < 5) {
200 | moveY = top + startY - dragState.startTop;
201 | currentMark.y = showTop;
202 | break;
203 | }
204 | }
205 |
206 | for (let i = 0; i < dragState.markLines.x.length; i++) {
207 | const { left, showLeft } = dragState.markLines.x[i];
208 | if (Math.abs(left - currentLeft) < 5) {
209 | moveX = left + startX - dragState.startLeft;
210 | currentMark.x = showLeft;
211 | break;
212 | }
213 | }
214 |
215 | const durY = moveY - startY;
216 | const durX = moveX - startX;
217 |
218 | focusData.value.focus.forEach((block, i) => {
219 | block.top = dragState.startPos[i].top + durY;
220 | block.left = dragState.startPos[i].left + durX;
221 | });
222 |
223 | mark.x = currentMark.x;
224 | mark.y = currentMark.y;
225 | };
226 | const mouseup = (e: MouseEvent) => {
227 | document.removeEventListener("mousemove", mousemove);
228 | document.removeEventListener("mouseup", mouseup);
229 | };
230 |
231 | const mousedown = (e: MouseEvent) => {
232 | dragState = {
233 | startX: e.clientX,
234 | startY: e.clientY,
235 | startPos: focusData.value.focus.map(({ top, left }) => ({
236 | top,
237 | left,
238 | })),
239 | startTop: state.selectBlock!.top,
240 | startLeft: state.selectBlock!.left,
241 | markLines: (() => {
242 | const { focus, unfocus } = focusData.value;
243 | // 当前选中的block
244 | const { top, left, width, height } = state.selectBlock!;
245 | let lines = { x: [], y: [] } as VisualEditorMarkLine;
246 | unfocus.forEach((block) => {
247 | const { top: t, left: l, width: w, height: h } = block;
248 |
249 | // y轴对齐方式
250 | lines.y.push({ top: t, showTop: t }); // 顶对顶
251 | lines.y.push({ top: t + h, showTop: t + h }); // 底对底
252 | lines.y.push({ top: t + h / 2 - height / 2, showTop: t + h / 2 }); // 中对中
253 | lines.y.push({ top: t - height, showTop: t }); // 顶对底
254 | lines.y.push({ top: t + h - height, showTop: t + h }); //
255 |
256 | // x轴对齐方式
257 | lines.x.push({ left: l, showLeft: l }); // 顶对顶
258 | lines.x.push({ left: l + w, showLeft: l + w }); // 底对底
259 | lines.x.push({
260 | left: l + w / 2 - width / 2,
261 | showLeft: l + w / 2,
262 | }); // 中对中
263 | lines.x.push({ left: l - width, showLeft: l }); // 顶对底
264 | lines.x.push({ left: l + w - width, showLeft: l + w }); // 中对中
265 | });
266 |
267 | return lines;
268 | })(),
269 | };
270 | document.addEventListener("mousemove", mousemove);
271 | document.addEventListener("mouseup", mouseup);
272 | };
273 |
274 | return { mousedown, mark };
275 | })();
276 |
277 | // 处理组件的选中状态
278 | const focusHandler = (() => {
279 | return {
280 | container: {
281 | onMousedown: (e: MouseEvent) => {
282 | e.stopPropagation();
283 | methods.clearFocus();
284 | state.selectBlock = null;
285 | },
286 | },
287 | block: {
288 | onMousedown: (e: MouseEvent, block: VisualEditorBlockData) => {
289 | e.stopPropagation();
290 | // e.preventDefault();
291 | // 只有元素未选中状态下, 才去处理
292 | if (!block.focus) {
293 | if (!e.shiftKey) {
294 | block.focus = !block.focus;
295 | methods.clearFocus(block);
296 | } else {
297 | block.focus = true;
298 | }
299 | }
300 | state.selectBlock = block;
301 | // 处理组件的选中移动
302 | blockDragger.mousedown(e);
303 | },
304 | },
305 | };
306 | })();
307 |
308 | const toolButtons = [
309 | {
310 | label: "撤销",
311 | icon: "icon-back",
312 | tip: "ctrl+z",
313 | },
314 | {
315 | label: "重做",
316 | icon: "icon-forward",
317 | tip: "ctrl+y, ctrl+shift+z",
318 | },
319 | {
320 | label: "删除",
321 | icon: "icon-delete",
322 | handler: () => {
323 | // 删除选中状态的 block
324 | dataModel.value!.blocks = [
325 | ...focusData.value.unfocus,
326 | ] as VisualEditorBlockData[];
327 | },
328 | tip: "ctrl+d, backspance, delete,",
329 | },
330 | ];
331 |
332 | // 更新block属性
333 | const updateBlockProps = (
334 | newBlock: VisualEditorBlockData,
335 | oldBlock: VisualEditorBlockData
336 | ) => {
337 | const blocks = [...dataModel.value!.blocks];
338 | const index = dataModel.value!.blocks.indexOf(state.selectBlock!);
339 | if (index > -1) {
340 | blocks.splice(index, 1, newBlock);
341 | dataModel.value!.blocks = deepcopy(blocks);
342 | state.selectBlock = dataModel.value!.blocks[index];
343 | }
344 | };
345 |
346 | // 更新容器属性值
347 | const updateModelValue = (newVal: VisualEditorModelValue) => {
348 | props.modelValue!.container = { ...newVal.container };
349 | };
350 |
351 | return () => (
352 |
353 |
366 |
367 | {toolButtons.map((btn, index) => (
368 |
369 |
370 | {btn.label}
371 |
372 | ))}
373 |
374 |
375 |
376 |
382 | {(dataModel.value?.blocks || []).map((block, index: number) => (
383 |
389 | focusHandler.block.onMousedown(e, block),
390 | }}
391 | />
392 | ))}
393 | {blockDragger.mark.x && (
394 |
398 | )}
399 | {blockDragger.mark.y && (
400 |
404 | )}
405 |
406 |
407 |
408 |
415 |
416 | );
417 | },
418 | });
419 |
--------------------------------------------------------------------------------