添加编辑器

main
suixy 3 months ago
parent f762f71144
commit 055f3847c2

@ -22,6 +22,9 @@
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "2.3.2", "@element-plus/icons-vue": "2.3.2",
"@highlightjs/vue-plugin": "2.1.2", "@highlightjs/vue-plugin": "2.1.2",
"@vue-flow/background": "^1.3.2",
"@vue-flow/core": "^1.43.1",
"@vue-flow/node-resizer": "^1.4.0",
"@vueup/vue-quill": "1.2.0", "@vueup/vue-quill": "1.2.0",
"@vueuse/core": "14.2.1", "@vueuse/core": "14.2.1",
"animate.css": "4.1.1", "animate.css": "4.1.1",
@ -38,6 +41,7 @@
"nprogress": "0.2.0", "nprogress": "0.2.0",
"pinia": "3.0.4", "pinia": "3.0.4",
"screenfull": "6.0.2", "screenfull": "6.0.2",
"uuid": "^13.0.0",
"vue": "3.5.30", "vue": "3.5.30",
"vue-cropper": "1.1.4", "vue-cropper": "1.1.4",
"vue-i18n": "11.3.0", "vue-i18n": "11.3.0",

@ -3,6 +3,9 @@ import { createApp } from 'vue';
import 'virtual:uno.css'; import 'virtual:uno.css';
import 'element-plus/theme-chalk/dark/css-vars.css'; import 'element-plus/theme-chalk/dark/css-vars.css';
import '@/assets/styles/index.scss'; import '@/assets/styles/index.scss';
import '@vue-flow/core/dist/style.css';
import '@vue-flow/core/dist/theme-default.css';
import '@vue-flow/node-resizer/dist/style.css';
// App、router、store // App、router、store
import App from './App.vue'; import App from './App.vue';

@ -11,7 +11,7 @@ import { usePermissionStore } from '@/store/modules/permission';
import { ElMessage } from 'element-plus/es'; import { ElMessage } from 'element-plus/es';
NProgress.configure({ showSpinner: false }); NProgress.configure({ showSpinner: false });
const whiteList = ['/login', '/register', '/social-callback', '/register*', '/register/*']; const whiteList = ['/login', '/register', '/social-callback', '/register*', '/register/*', '/visualEditor'];
const isWhiteList = (path: string) => { const isWhiteList = (path: string) => {
return whiteList.some((pattern) => isPathMatch(pattern, path)); return whiteList.some((pattern) => isPathMatch(pattern, path));

@ -26,6 +26,11 @@ import Layout from '@/layout/index.vue';
// 公共路由 // 公共路由
export const constantRoutes: RouteRecordRaw[] = [ export const constantRoutes: RouteRecordRaw[] = [
{
path: '/visualEditor',
hidden: true,
component: () => import('@/views/visualEditor/index.vue')
},
{ {
path: '/redirect', path: '/redirect',
component: Layout, component: Layout,

@ -0,0 +1,144 @@
<template>
<div style="width: 100%; height: 100vh">
<div class="top"></div>
<div class="left">
<el-card
class="moduleCard"
shadow="never"
:draggable="true"
@dragstart="onDragStart($event, 'line', {})"
:style="{ display: 'inline-block', margin: '0 4px 4px 0' }"
:body-style="{ padding: '4px 0' }"
>
<template #header>
<div style="position: absolute; top: 50%; transform: translateY(-50%); width: 64px; height: 1px; background-color: #0002"></div>
</template>
<div class="moduleText">线</div>
</el-card>
<el-card
class="moduleCard"
shadow="never"
:draggable="true"
@dragstart="onDragStart($event, 'circuitComponent', { type: 1 })"
:style="{ display: 'inline-block', margin: '0 4px 4px 0' }"
:body-style="{ padding: '4px 0' }"
>
<template #header>
<el-image style="width: 100%; height: 100%" :src="img1" fit="contain" />
</template>
<div class="moduleText">元件1</div>
</el-card>
<el-card
class="moduleCard"
shadow="never"
:draggable="true"
@dragstart="onDragStart($event, 'circuitComponent', { type: 2 })"
:style="{ display: 'inline-block', margin: '0 4px 4px 0' }"
:body-style="{ padding: '4px 0' }"
>
<template #header>
<el-image style="width: 100%; height: 100%" :src="img2" fit="contain" />
</template>
<div class="moduleText">元件2</div>
</el-card>
</div>
<div class="right" @drop="onDrop">
<VueFlow
class="flow"
id="flowA"
:min-zoom="0.01"
ref="flowRef"
:snapToGrid="isSnapToGrid"
:panOnDrag="false"
:zoomOnScroll="false"
:autoPanSpeed="0"
:snapGrid="[10, 10]"
v-model:nodes="nodes"
v-model:edges="edges"
fit-view-on-init
default-marker-color="#409EFF"
@node-click="logEvent('click', $event)"
@pane-click="logEvent('paneClick', $event)"
@node-drag-start="logEvent('nodeDrag', $event)"
@node-contextMenu="logEvent('contextmenu', $event)"
@dragover="onDragOver"
>
<Background :size="1" :gap="20" pattern-color="#BDBDBD" style="background-color: #fff" />
<template #node-circuitComponent="props">
<CircuitComponentNode v-bind="props" />
</template>
<template #node-line="props">
<LineNode v-bind="props" />
</template>
</VueFlow>
</div>
</div>
</template>
<script setup>
import img1 from '@/assets/electronicComponents/1.png';
import img2 from '@/assets/electronicComponents/2.png';
import CircuitComponentNode from './nodes/circuitComponent.vue';
import LineNode from './nodes/line.vue';
import { MarkerType, useVueFlow, VueFlow } from '@vue-flow/core';
import { Background } from '@vue-flow/background';
import tool from './tool.js';
const nodes = ref([]);
const edges = ref([]);
const isSnapToGrid = ref(true);
const { onDragStart, onDrop, onDragOver } = tool();
const { addEdges, updateNode, removeNodes, addNodes } = useVueFlow({ id: 'flowA' });
const logEvent = async (eventname, event) => {
switch (eventname) {
case 'paneClick':
break;
case 'click':
break;
case 'nodeDrag':
break;
}
};
</script>
<style lang="scss" scoped>
.top{
width: 100%;
height: 50px;
}
.left {
display: inline-block;
width: 200px;
height: 100%;
vertical-align: top;
.moduleCard {
width: calc(50% - 4px);
display: inline-block;
height: 150px;
:deep(.el-card__header) {
width: 100%;
height: 100px;
position: relative;
}
:deep(.el-card__body) {
padding: 0 20px 0 20px !important;
.moduleText {
line-height: 50px;
}
}
}
}
.right {
display: inline-block;
width: 960px;
height: 540px;
position: absolute;
left: 200px;
}
</style>

@ -0,0 +1,73 @@
<template>
<div :style="{ width: props.dimensions.width * props.ratioWidth + 'px', height: props.dimensions.height * props.ratioHeight + 'px' }">
<NodeResizer color="#000" v-if="!props.isView && !props.isHideHandle && props.selected" />
<div
class="custom-node"
:style="{
width: props.dimensions.width * props.ratioWidth + 'px',
height: props.dimensions.height * props.ratioHeight + 'px',
pointerEvents: props.isView ? 'auto' : 'none'
}"
>
<el-image style="width: 100%; height: 100%" :src="props.data.customData.type === 1 ? img1:img2" fit="contain" />
</div>
</div>
</template>
<script setup>
import img1 from '@/assets/electronicComponents/1.png';
import img2 from '@/assets/electronicComponents/2.png';
import { defineEmits, defineProps, ref } from 'vue';
import { NodeResizer } from '@vue-flow/node-resizer';
import { Handle, Position } from '@vue-flow/core';
const props = defineProps({
ratioWidth: {
type: Number,
required: false,
default: 1
},
ratioHeight: {
type: Number,
required: false,
default: 1
},
isView: {
type: Boolean,
required: false
},
inputData: {
type: Object,
required: false
},
id: {
type: String,
required: true
},
isHideHandle: {
type: Boolean,
required: false
},
selected: {
type: Boolean,
required: false
},
data: {
type: Object,
required: true
},
dimensions: {
type: Object,
required: true
}
});
onMounted(() => {
console.log(props);
});
</script>
<style scoped>
.custom-node {
width: 100%;
height: 100%;
}
</style>

@ -0,0 +1,72 @@
<template>
<div :style="{ width: props.dimensions.width * props.ratioWidth + 'px', height: props.dimensions.height * props.ratioHeight + 'px' }">
<NodeResizer color="#000" v-if="!props.isView && !props.isHideHandle && props.selected" />
<div
class="custom-node"
:style="{
width: props.dimensions.width * props.ratioWidth + 'px',
height: props.dimensions.height * props.ratioHeight + 'px',
pointerEvents: props.isView ? 'auto' : 'none'
}"
>
<div style="position: absolute;top: 50%;left: 0;transform: translateY(-50%);width: 100%;height: 1px;background-color: #0002"></div>
</div>
</div>
</template>
<script setup>
import img1 from '@/assets/electronicComponents/1.png';
import img2 from '@/assets/electronicComponents/2.png';
import { defineEmits, defineProps, ref } from 'vue';
import { NodeResizer } from '@vue-flow/node-resizer';
import { Handle, Position } from '@vue-flow/core';
const props = defineProps({
ratioWidth: {
type: Number,
required: false,
default: 1
},
ratioHeight: {
type: Number,
required: false,
default: 1
},
isView: {
type: Boolean,
required: false
},
inputData: {
type: Object,
required: false
},
id: {
type: String,
required: true
},
isHideHandle: {
type: Boolean,
required: false
},
selected: {
type: Boolean,
required: false
},
data: {
type: Object,
required: true
},
dimensions: {
type: Object,
required: true
}
});
onMounted(() => {
console.log(props);
});
</script>
<style scoped>
.custom-node {
position: relative;
}
</style>

@ -0,0 +1,87 @@
import { useVueFlow } from '@vue-flow/core';
import { ref, watch } from 'vue';
import { v4 as uuidv4 } from 'uuid';
const getId = (type) => {
return `${type}_${uuidv4().replaceAll('-', '_')}`;
};
const getOption = (e) => {
if (e === 'circuitComponent') {
return {
title: ''
};
}
};
const getNodeSize = (e) => {
if (e === 'circuitComponent') {
return { width: 50, height: 50 };
} else if (e === 'line') {
return { width: 100, height: 20 };
}
};
const nameEnum = {};
const tool = () => {
const nodeType = ref('');
const customData = ref({ type: 1 });
const { addNodes, screenToFlowCoordinate, onNodesInitialized, updateNode } = useVueFlow({ id: 'flowA' });
const onDragStart = (event, type, data) => {
if (event.dataTransfer) {
event.dataTransfer.setData('application/vueflow', type);
event.dataTransfer.effectAllowed = 'move';
}
nodeType.value = type;
customData.value = data || { type: 1 };
document.addEventListener('drop', onDragEnd);
};
const onDragEnd = () => {
nodeType.value = null;
document.removeEventListener('drop', onDragEnd);
};
const onDrop = (event) => {
const dimensions = getNodeSize(nodeType.value);
const position = screenToFlowCoordinate({
x: event.clientX,
y: event.clientY
});
const nodeId = getId(nodeType.value);
const newNode = {
id: nodeId,
name: nameEnum[nodeType.value],
draggable: true,
type: nodeType.value,
dimensions,
position,
data: { options: getOption(nodeType.value), outputData: {}, customData: customData.value }
};
const { off } = onNodesInitialized(() => {
updateNode(nodeId, (node) => ({
position: { x: node.position.x - node.dimensions.width / 2, y: node.position.y - node.dimensions.height / 2 }
}));
off();
});
addNodes(newNode);
};
const onDragOver = (event) => {
event.preventDefault();
if (nodeType.value) {
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'move';
}
}
};
return {
onDragStart,
onDragEnd,
onDrop,
onDragOver
};
};
export default tool;
Loading…
Cancel
Save