编辑器添加属性和样式配置

main
suixy 1 month ago
parent ec693aad62
commit 3dc02c0ec1

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

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

@ -15,6 +15,19 @@
</template>
<div class="moduleText">线</div>
</el-card>
<el-card
class="moduleCard"
shadow="never"
:draggable="true"
@dragstart="onDragStart($event, 'text1', {})"
: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"
@ -42,12 +55,13 @@
<div class="moduleText">元件2</div>
</el-card>
</div>
<div class="right" @drop="onDrop">
<div class="right" ref="rightRef" @drop="onDrop">
<VueFlow
class="flow"
id="flowA"
:min-zoom="0.01"
ref="flowRef"
:zoom-on-double-click="false"
:snapToGrid="isSnapToGrid"
:panOnDrag="false"
:zoomOnScroll="false"
@ -61,35 +75,161 @@
@pane-click="logEvent('paneClick', $event)"
@node-drag-start="logEvent('nodeDrag', $event)"
@node-contextMenu="logEvent('contextmenu', $event)"
@node-double-click="logEvent('dblclick', $event)"
@dragover="onDragOver"
>
<Background :size="1" :gap="20" pattern-color="#BDBDBD" style="background-color: #fff" />
<template #node-area="props">
<AreaNode v-bind="props" :pageData="{ width: '960px', height: '540px' }"></AreaNode>
</template>
<template #node-circuitComponent="props">
<CircuitComponentNode v-bind="props" />
<CircuitComponentNode :monitorType="monitorType" v-bind="props" :networkData="dataTest" />
</template>
<template #node-deviceDataList="props">
<DeviceDataListNode :monitorType="monitorType" v-bind="props" :networkData="dataTest" :pNode="findNode(props.parentNodeId)" />
</template>
<template #node-line="props">
<LineNode v-bind="props" />
<LineNode :monitorType="monitorType" v-bind="props" />
</template>
<template #node-text1="props">
<TextNode :monitorType="monitorType" v-bind="props" :networkData="dataTest" />
</template>
</VueFlow>
</div>
<el-dialog draggable v-model="nodeOptionVisible" title="节点配置" width="500">
<el-form :model="nodeData">
<el-form-item label="网络地址" label-width="120px" v-if="[].includes(nodeType)">
<el-input v-model="nodeData.network" autocomplete="off" />
</el-form-item>
<el-form-item label="节点id" label-width="120px" v-if="['circuitComponent', 'text1'].includes(nodeType)">
<el-input v-model="nodeData.id" autocomplete="off" />
</el-form-item>
<el-form-item label="节点内容" label-width="120px" v-if="['text1'].includes(nodeType)">
<el-input v-model="nodeData.value" autocomplete="off" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="nodeOptionVisible = false">关闭</el-button>
</div>
</template>
</el-dialog>
<el-dialog :modal="false" :modal-penetrable="true" draggable v-model="nodeStyleVisible" title="节点配置" width="500">
<el-form :model="nodeStyle">
<el-form-item label="对齐方式" label-width="120px" v-if="Object.hasOwn(nodeStyle, 'align')">
<el-select v-model="nodeStyle.align" placeholder="Select">
<el-option label="左对齐" value="left" />
<el-option label="居中" value="center" />
<el-option label="右对齐" value="right" />
</el-select>
</el-form-item>
<el-form-item label="颜色" label-width="120px" v-if="Object.hasOwn(nodeStyle, 'color')">
<el-color-picker v-model="nodeStyle.color" show-alpha />
</el-form-item>
<el-form-item label="是否允许换行" label-width="120px" v-if="Object.hasOwn(nodeStyle, 'whiteSpace')">
<el-switch v-model="nodeStyle.whiteSpace" active-value="nowrap" inactive-value="normal" />
</el-form-item>
<el-form-item label="线类型" label-width="120px" v-if="Object.hasOwn(nodeStyle, 'lineType')">
<el-select v-model="nodeStyle.lineType" placeholder="Select">
<el-option label="横线" :value="0" />
<el-option label="竖线" :value="1" />
</el-select>
</el-form-item>
<el-form-item label="宽度" label-width="120px" v-if="Object.hasOwn(nodeStyle, 'width')">
<el-input-number v-model="nodeStyle.width" :min="1" />
</el-form-item>
<el-form-item label="旋转角度" label-width="120px" v-if="Object.hasOwn(nodeStyle, 'rotate')">
<el-input-number v-model="nodeStyle.rotate" :min="-360" :max="360" />
</el-form-item>
<el-form-item label="旋转中心" label-width="120px" v-if="Object.hasOwn(nodeStyle, 'transformOrigin1')">
<el-select v-model="nodeStyle.transformOrigin1" placeholder="Select" style="width: calc(50% - 20px); margin-right: 20px">
<el-option label="左" value="left" />
<el-option label="中" value="center" />
<el-option label="右" value="right" />
</el-select>
<el-select v-model="nodeStyle.transformOrigin2" placeholder="Select" style="width: calc(50% - 20px)">
<el-option label="上" value="top" />
<el-option label="中" value="center" />
<el-option label="下" value="bottom" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="nodeStyleVisible = false">关闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { v4 as uuidv4 } from 'uuid';
import img1 from '@/assets/electronicComponents/1.png';
import img2 from '@/assets/electronicComponents/2.png';
import AreaNode from './nodes/area.vue';
import CircuitComponentNode from './nodes/circuitComponent.vue';
import DeviceDataListNode from './nodes/deviceDataList.vue';
import LineNode from './nodes/line.vue';
import TextNode from './nodes/text.vue';
import { MarkerType, useVueFlow, VueFlow } from '@vue-flow/core';
import { Background } from '@vue-flow/background';
import tool from './tool.js';
const nodes = ref([]);
const rightRef = ref();
const flowRef = ref();
const pid = ref(`area_${uuidv4().replaceAll('-', '_')}`);
const nodes = ref([
{
id: pid.value,
name: 'area',
type: 'area',
position: {
x: 0,
y: 0
},
selectable: false,
draggable: false,
data: {}
}
]);
const edges = ref([]);
const isSnapToGrid = ref(true);
const nodeOptionVisible = ref(false);
const nodeStyleVisible = ref(false);
const nodeType = ref('');
const nodeData = ref({});
const nodeStyle = ref({});
const dataTest = ref([
{
id: 'test-1',
options: {
tem: 30,
a: 10,
v: 20
}
},
{
id: 'test-2',
options: {
tem: 20,
a: 20,
v: 10
}
}
]);
const monitorType = ref([
{ label: '温度', value: 'tem' },
{ label: '字段1', value: 'a' },
{ label: '字段2', value: 'v' }
]);
const { onDragStart, onDrop, onDragOver } = tool();
const { addEdges, updateNode, removeNodes, addNodes } = useVueFlow({ id: 'flowA' });
const { addEdges, updateNode, removeNodes, addNodes, onNodesChange, findNode } = useVueFlow({ id: 'flowA' });
const logEvent = async (eventname, event) => {
switch (eventname) {
@ -99,14 +239,65 @@ const logEvent = async (eventname, event) => {
break;
case 'nodeDrag':
break;
case 'dblclick':
event.event.preventDefault();
nodeStyleVisible.value = true;
nodeType.value = event.node.type;
nodeStyle.value = event.node.data.style;
break;
case 'contextmenu':
event.event.preventDefault();
nodeOptionVisible.value = true;
nodeType.value = event.node.type;
nodeData.value = event.node.data.options;
break;
}
};
onNodesChange((changes) => {
changes.forEach((e) => {
if (e.type === 'remove') {
const ids = nodes.value.filter((node) => node.parentNode === e.id).map((node) => node.id);
removeNodes(ids);
}
});
});
onMounted(() => {
let scale = 0;
const width = rightRef.value.getBoundingClientRect().width;
rightRef.value.style.width = width + 'px';
rightRef.value.style.height = (width * 540) / 960 + 'px';
scale = width / 960;
flowRef.value.onInit(() => {
flowRef.value.setTransform({
x: 0,
y: 0,
zoom: scale
});
});
nextTick(() => {
let NODES = JSON.parse(localStorage.getItem('NODES') || '[]');
if (NODES.length > 0) {
nodes.value = NODES;
}
});
});
watch(
nodes,
() => {
if (nodes.value.length > 1) {
localStorage.setItem('NODES', JSON.stringify(nodes.value));
}
},
{ deep: true }
);
</script>
<style lang="scss" scoped>
.top{
.top {
width: 100%;
height: 50px;
}
.left {
display: inline-block;
width: 200px;
@ -136,8 +327,8 @@ const logEvent = async (eventname, event) => {
.right {
display: inline-block;
width: 960px;
height: 540px;
width: calc(100% - 200px - 20px);
height: 100%;
position: absolute;
left: 200px;
}

@ -0,0 +1,28 @@
<template>
<div style="pointer-events: none">
<div class="custom-node">
<div
class="area"
:style="`width:${props.pageData?.width || '100px'};height:${props.pageData?.height || '100px'};border: 1px solid #fff;`"
></div>
</div>
</div>
</template>
<script setup>
import { defineProps } from 'vue';
const props = defineProps({
pageData: {
type: Object,
required: false
}
});
</script>
<style scoped>
.area {
background-repeat: no-repeat;
background-size: 100% 100%;
background-color: #0000;
}
</style>

@ -10,7 +10,7 @@
pointerEvents: props.isView ? 'auto' : 'none'
}"
>
<el-image style="width: 100%; height: 100%" :src="props.data.customData.type === 1 ? img1:img2" fit="contain" />
<el-image style="width: 100%; height: 100%" :src="props.data.customData.type === 1 ? img1 : img2" fit="contain" />
</div>
</div>
</template>

@ -0,0 +1,88 @@
<template>
<div :style="{ width: props.dimensions.width * props.ratioWidth + 'px', height: props.dimensions.height * props.ratioHeight + 'px' }">
<NodeResizer @resizeEnd="(e) => $emit('resize', e)" color="#000" v-if="!props.isView && !props.isHideHandle && props.selected" @resize="resize" />
<div
class="custom-node"
:style="{
width: props.dimensions.width * props.ratioWidth + 'px',
lineHeight: props.dimensions.height * props.ratioHeight + 'px',
height: props.dimensions.height * props.ratioHeight + 'px',
pointerEvents: props.isView ? 'auto' : 'none'
}"
>
<template v-for="i in 4"></template>
</div>
</div>
</template>
<script setup>
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
},
networkData: {
type: Array,
required: false
},
pNode: {
type: Object,
required: true
}
});
console.log(props);
const list = ref([{}]);
watch(
props.pNode.data.options,
(obj1, obj2) => {
if (JSON.stringify(obj1) !== JSON.stringify(obj2)) {
}
},
{ deep: true }
);
</script>
<style scoped>
.custom-node {
position: absolute;
}
</style>

@ -10,16 +10,13 @@
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 :style="lineStyle"></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 { defineProps } from 'vue';
import { NodeResizer } from '@vue-flow/node-resizer';
import { Handle, Position } from '@vue-flow/core';
const props = defineProps({
ratioWidth: {
@ -61,6 +58,20 @@ const props = defineProps({
required: true
}
});
const lineStyle = computed(() => {
return {
position: 'absolute',
top: props.data.style.lineType === 0 ? '50%' : '0%',
left: props.data.style.lineType === 0 ? '0%' : '50%',
transform: `${props.data.style.lineType === 0 ? 'translateY(-50%)' : 'translateX(-50%)'} rotate(${props.data.style.rotate}deg)`,
transformOrigin: `${props.data.style.transformOrigin1} ${props.data.style.transformOrigin2}`,
backgroundColor: props.data.style.color,
width: props.data.style.lineType === 0 ? '100%' : `${props.data.style.width}px`,
height: props.data.style.lineType === 0 ? `${props.data.style.width}px` : '100%'
};
});
onMounted(() => {
console.log(props);
});

@ -0,0 +1,107 @@
<template>
<div :style="{ width: props.dimensions.width * props.ratioWidth + 'px', height: props.dimensions.height * props.ratioHeight + 'px' }">
<NodeResizer @resizeEnd="(e) => $emit('resize', e)" color="#000" v-if="!props.isView && !props.isHideHandle && props.selected" @resize="resize" />
<div
class="custom-node"
:style="{
textAlign: props.data.style.align || 'center',
width: props.dimensions.width * props.ratioWidth + 'px',
lineHeight: props.dimensions.height * props.ratioHeight + 'px',
height: props.dimensions.height * props.ratioHeight + 'px',
pointerEvents: props.isView ? 'auto' : 'none'
}"
>
<span
:style="{
whiteSpace: props.data.style.whiteSpace || 'nowrap',
color: props.data.style.color,
fontSize: props.dimensions.height * props.ratioHeight + 'px'
}"
>
{{ format(props.data.options.value) || (!props.isView ? '文字' : '123') }}</span
>
</div>
</div>
</template>
<script setup>
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
},
networkData: {
type: Array,
required: false
}
});
const format = (e) => {
if (!e) return '';
return e.replace(/\$\{(.*?)\}/g, (match, key) => {
const keys = key.split(',');
console.log(props.networkData);
let result = props.networkData.find((v) => v.id === props.data.options.id)?.options || {};
for (let k of keys) {
result = result?.[k];
if (result === undefined) return '';
}
return result;
});
};
watch(
() => [JSON.parse(JSON.stringify(''))],
async (obj1, obj2) => {
if (JSON.stringify(obj1) !== JSON.stringify(obj2)) {
}
},
{ deep: true, immediate: true }
);
</script>
<style scoped>
.custom-node {
position: absolute;
}
</style>

@ -9,8 +9,35 @@ const getId = (type) => {
const getOption = (e) => {
if (e === 'circuitComponent') {
return {
title: ''
network: '',
id: '',
value: ''
};
} else {
return {};
}
};
const getStyle = (e) => {
if (e === 'circuitComponent') {
return {};
} else if (e === 'line') {
return {
width: 1,
lineType: 0,
rotate: 0,
transformOrigin1: 'center',
transformOrigin2: 'center',
color: '#000'
};
} else if (e === 'text1') {
return {
align: 'center',
whiteSpace: 'nowrap',
color: '#000'
};
} else {
return {};
}
};
const getNodeSize = (e) => {
@ -18,6 +45,10 @@ const getNodeSize = (e) => {
return { width: 50, height: 50 };
} else if (e === 'line') {
return { width: 100, height: 20 };
} else if (e === 'deviceDataList') {
return { width: 100, height: 20 };
} else if (e === 'text1') {
return { width: 100, height: 20 };
}
};
const nameEnum = {};
@ -55,7 +86,7 @@ const tool = () => {
type: nodeType.value,
dimensions,
position,
data: { options: getOption(nodeType.value), outputData: {}, customData: customData.value }
data: { options: getOption(nodeType.value), style: getStyle(nodeType.value), customData: customData.value }
};
const { off } = onNodesInitialized(() => {
@ -67,6 +98,28 @@ const tool = () => {
});
addNodes(newNode);
if (nodeType.value === 'circuitComponent') {
const childrenNodeId = getId('deviceDataList');
const childrenNode = {
id: childrenNodeId,
parentNode: nodeId,
name: nameEnum['deviceDataList'],
draggable: true,
type: 'deviceDataList',
dimensions: getNodeSize('deviceDataList'),
position: { x: newNode.dimensions.width, y: 0 },
data: { options: getOption('deviceDataList'), style: getStyle('deviceDataList') }
};
const { off } = onNodesInitialized(() => {
updateNode(childrenNodeId, (node) => ({
position: { x: node.position.x - node.dimensions.width / 2, y: node.position.y - node.dimensions.height / 2 }
}));
off();
});
addNodes(childrenNode);
}
};
const onDragOver = (event) => {
event.preventDefault();

@ -0,0 +1,80 @@
<template>
<div>
<div class="center" ref="centerRef">
<div
class="flow"
:style="{
transform: `scale(${scaleX}, ${scaleY})`,
transformOrigin: 'left top'
}"
>
<div
style="position: absolute"
v-for="i in nodes"
:style="{
left: (i.position?.x / 960) * 100 + '%',
top: (i.position?.y / 540) * 100 + '%'
}"
>
<CircuitComponentNode :ratioWidth="ratioWidth" :ratioHeight="ratioHeight" v-bind="i" :is-view="true" v-if="i.type === 'circuitComponent'" />
<LineNode :ratioWidth="ratioWidth" :ratioHeight="ratioHeight" v-bind="i" :is-view="true" v-if="i.type === 'line'" />
</div>
</div>
<div class="textFlow">
<div
style="position: absolute"
v-for="i in nodes"
:style="{
left: (i.position?.x / 960) * 100 + '%',
top: (i.position?.y / 540) * 100 + '%'
}"
>
<TextNode :ratioWidth="ratioWidth" :ratioHeight="ratioHeight" v-bind="i" :is-view="true" v-if="i.type === 'text1'" />
</div>
</div>
</div>
</div>
</template>
<script setup>
import CircuitComponentNode from './nodes/circuitComponent.vue';
import LineNode from './nodes/line.vue';
import TextNode from './nodes/text.vue';
const nodes = ref([]);
const centerRef = ref(null);
const ratioWidth = ref(1);
const ratioHeight = ref(1);
const scaleX = ref(1);
const scaleY = ref(1);
onMounted(() => {
nodes.value = JSON.parse(localStorage.getItem('NODES') || '[]');
const rect = centerRef.value.getBoundingClientRect();
scaleX.value = rect.width / 960;
scaleY.value = rect.height / 540;
// const { offsetWidth, offsetHeight } = centerRef.value;
// ratioWidth.value = offsetWidth / 960;
// ratioHeight.value = offsetHeight / 540;
});
</script>
<style lang="scss" scoped>
.center {
width: 1000px;
height: 400px;
position: absolute;
}
.flow {
position: absolute;
width: 960px;
height: 540px;
}
.textFlow {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
</style>
Loading…
Cancel
Save