main
suixy 3 days ago
parent 50abab6cb2
commit 716f6797bb

@ -0,0 +1,40 @@
{
"name": "scrin-visual-editor",
"version": "1.0.0",
"type": "module",
"license": "MIT",
"main": "dist/scrin-visual-editor.umd.js",
"module": "dist/scrin-visual-editor.es.js",
"style": "dist/style.css",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"exports": {
".": {
"import": "./dist/scrin-visual-editor.es.js",
"require": "./dist/scrin-visual-editor.umd.js"
}
},
"scripts": {
"build": "vite build",
"dev": "vite build --watch"
},
"dependencies": {
"@vue-flow/background": "^1.3.2",
"@vue-flow/core": "^1.48.2",
"@vue-flow/node-resizer": "^1.5.1",
"uuid": "^11.0.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.4",
"element-plus": "^2.13.3",
"less": "^4.5.1",
"vite": "^7.3.1",
"vue": "^3.5.29"
},
"peerDependencies": {
"element-plus": "^2.13.3",
"vue": "^3.3.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1011 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1000 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 955 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 632 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 632 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 B

@ -0,0 +1,470 @@
<template>
<div class="hw-root">
<div class="visual-editor">
<div class="visual-editor-top">
<div style="display: inline-block;">
<el-popover
:width="880"
trigger="click"
placement="bottom-start">
<template #reference>
<div style="margin-left: 20px;display: inline-block">
<svg viewBox="0 0 1024 1024" width="20" height="20" style="margin-top: 15px;">
<path
d="M629.333333 160v768h-213.333333v-768h213.333333z m-277.333333 192v576h-213.333333v-576h213.333333z m554.666667 213.333333v362.666667h-213.333334v-362.666667h213.333334z m-341.333334-341.333333h-85.333333v640h85.333333v-640z m-277.333333 192h-85.333333v448h85.333333v-448z m554.666667 213.333333h-85.333334v234.666667h85.333334v-234.666667z"
fill="#ddd" p-id="10516"></path>
</svg>
<el-icon color="#eee" :size="10" style="margin-top: 20px;margin-left: 8px;;vertical-align: top">
<component :is="icon.ArrowDown"/>
</el-icon>
</div>
</template>
<div style="max-height: 60vh;overflow:auto;">
<BoardNodes/>
</div>
</el-popover>
<el-popover
:width="620"
trigger="click"
placement="bottom-start">
<template #reference>
<div style="margin-left: 30px;display: inline-block">
<svg viewBox="0 0 1024 1024" width="20" height="20" style="margin-top: 15px;">
<path
d="M108.8 224v576C108.8 902.4 313.6 960 512 960s403.2-57.6 403.2-160v-576C915.2 12.8 108.8 12.8 108.8 224z m742.4 390.4c0 32-121.6 96-339.2 96s-339.2-64-339.2-96V518.4C249.6 563.2 384 588.8 512 588.8s262.4-25.6 339.2-70.4v96z m0-300.8v108.8c0 32-121.6 96-339.2 96s-339.2-64-339.2-96V313.6h6.4c6.4 6.4 12.8 12.8 19.2 12.8h6.4c12.8 6.4 19.2 6.4 32 12.8 19.2 12.8 32 12.8 44.8 19.2 70.4 19.2 147.2 25.6 230.4 25.6s160-6.4 230.4-25.6c12.8-6.4 25.6-6.4 38.4-12.8 12.8-6.4 25.6-6.4 32-12.8h6.4c12.8-6.4 19.2-12.8 32-19.2-6.4 0 0 0 0 0zM512 128c217.6 0 339.2 64 339.2 96S729.6 320 512 320 172.8 256 172.8 224 294.4 128 512 128z m0 768c-217.6 0-339.2-64-339.2-96V704c76.8 44.8 211.2 70.4 339.2 70.4s262.4-25.6 339.2-70.4v89.6c0 38.4-121.6 102.4-339.2 102.4z"
fill="#ddd" p-id="22021"></path>
</svg>
<el-icon color="#eee" :size="10" style="margin-top: 20px;margin-left: 8px;;vertical-align: top">
<component :is="icon.ArrowDown"/>
</el-icon>
</div>
</template>
<div style="max-height: 60vh;overflow:auto;">
<CustomData/>
</div>
</el-popover>
<el-popover
:width="620"
trigger="click"
placement="bottom-start"
>
<template #reference>
<div style="margin-left: 30px;display: inline-block">
<svg viewBox="0 0 1024 1024" width="20" height="20" style="margin-top: 15px;">
<path
d="M959.825022 384.002258V191.939717C959.825022 121.2479 902.517291 63.940169 831.825474 63.940169H191.939717C121.2479 63.940169 63.940169 121.2479 63.940169 191.939717v639.885757C63.940169 902.517291 121.2479 959.825022 191.939717 959.825022h639.885757c70.691817 0 127.999548-57.307731 127.999548-127.999548V384.002258zM146.66502 146.66502a63.737872 63.737872 0 0 1 45.336109-18.784682h639.997742A63.961844 63.961844 0 0 1 895.884854 192.001129V320.062089H127.880338V192.001129A63.737872 63.737872 0 0 1 146.66502 146.66502z m269.1267 461.308451v-223.971213h192.181751v223.971213h-192.181751z m192.181751 63.940169v223.971214h-192.181751v-223.971214h192.181751z m-256.12192-63.940169H127.880338v-223.971213h223.971213v223.971213z m-205.186531 269.235073a63.466939 63.466939 0 0 1-18.784682-45.209673V671.91364h223.971213v223.971214H192.001129a63.625887 63.625887 0 0 1-45.336109-18.67631z m749.219834-45.209673A63.763159 63.763159 0 0 1 831.998871 895.884854H671.91364v-223.971214h223.971214v160.085231z m0-224.0254h-223.971214v-223.971213h223.971214v223.971213z"
fill="#ddd" p-id="23072"></path>
</svg>
<el-icon color="#eee" :size="10" style="margin-top: 20px;margin-left: 8px;;vertical-align: top">
<component :is="icon.ArrowDown"/>
</el-icon>
</div>
</template>
<div style="max-height: 60vh;overflow:auto;">
<FormNodes/>
</div>
</el-popover>
<el-popover
:width="620"
trigger="click"
placement="bottom-start"
>
<template #reference>
<div style="margin-left: 30px;display: inline-block;vertical-align: top">
<el-icon color="#fff" style="margin-top: 15px;">
<component :is="icon.Opportunity"/>
</el-icon>
<el-icon color="#eee" :size="10" style="margin-top: 20px;margin-left: 8px;;vertical-align: top">
<component :is="icon.ArrowDown"/>
</el-icon>
</div>
</template>
<div style="max-height: 60vh;overflow:auto;">
<OtherNodes :icons="icon"/>
</div>
</el-popover>
</div>
<div class="options-menu">
<el-icon color="#ddd" :size="20" style="margin-top: 15px;margin-left: 20px;vertical-align: top"
@click="pageSetting">
<component :is="icon.Setting"/>
</el-icon>
<el-icon color="#ddd" :size="20" style="margin-top: 15px;margin-left: 20px;vertical-align: top" @click="save">
<component :is="icon.DocumentChecked"/>
</el-icon>
</div>
</div>
<div class="visual-editor-left">
<div class="title">
图层
</div>
<div class="levels">
<el-dropdown
style="min-width: 100%;display: inline-block"
v-for="i in nodes.filter(e => e.type !== 'area').reverse()"
:key="i.id"
trigger="contextmenu"
@command="nodeOperate">
<div class="level" :class="{isSelect: i.selected}" @contextmenu.prevent @click="pitchOnNode(i)">
<div class="isLock">
<el-icon style="cursor: pointer" color="#fff" :size="16" @click="i.draggable = !i.draggable">
<Unlock v-if="i.draggable"/>
<Lock v-if="!i.draggable"/>
</el-icon>
</div>
<div class="icon">
<svg viewBox="0 0 1024 1024" width="16" height="16" style="margin-left: 10px;">
<path
d="M629.333333 160v768h-213.333333v-768h213.333333z m-277.333333 192v576h-213.333333v-576h213.333333z m554.666667 213.333333v362.666667h-213.333334v-362.666667h213.333334z m-341.333334-341.333333h-85.333333v640h85.333333v-640z m-277.333333 192h-85.333333v448h85.333333v-448z m554.666667 213.333333h-85.333334v234.666667h85.333334v-234.666667z"
fill="#ddd"
></path>
</svg>
</div>
<div class="boardName">
<span style="display: inline-block;">{{ i.name }}</span>
</div>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :command="{ type: 'toTop', node: i }">置顶</el-dropdown-item>
<el-dropdown-item :command="{ type: 'toBottom', node: i }">置底</el-dropdown-item>
<el-dropdown-item :command="{ type: 'moveUp', node: i }">上移一层</el-dropdown-item>
<el-dropdown-item :command="{ type: 'moveDown', node: i }">下移一层</el-dropdown-item>
<el-dropdown-item :command="{ type: 'copy', node: i }">复制</el-dropdown-item>
<el-dropdown-item :command="{ type: 'del', node: i }">删除</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
<div class="visual-editor-center" @drop="onDrop" v-loading="loading">
<FlowRuler>
<VueFlow
id="flowA"
:min-zoom="0.01"
ref="flowRef"
:snapToGrid="isSnapToGrid"
:snapGrid="[10, 10]"
v-model:nodes="nodes"
v-model:edges="edges"
fit-view-on-init
default-marker-color="#409EFF"
@connect="logEvent('connect', $event)"
@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: #000"/>
<template #node-area="props">
<AreaNode v-bind="props" :pageData="pageData" @resize="(e) => handleResizeStop(e, props)"/>
</template>
<template #node-customBoard="props">
<CustomBoardNode v-bind="props" :inputData="getInputData(props.id)"
@resize="(e) => handleResizeStop(e, props)"/>
</template>
<template #node-customData="props">
<CustomDataNode v-bind="props" :inputData="getInputData(props.id)"
@resize="(e) => handleResizeStop(e, props)"/>
</template>
<template #node-line="props">
<LineNode :colors="pageSettingForm.colors" v-bind="props" :inputData="getInputData(props.id)"
@resize="(e) => handleResizeStop(e, props)"/>
</template>
<template #node-multiLines="props">
<MultiLinesNode :colors="pageSettingForm.colors" v-bind="props" :inputData="getInputData(props.id)"
@resize="(e) => handleResizeStop(e, props)"/>
</template>
<template #node-curve="props">
<CurveNode :colors="pageSettingForm.colors" v-bind="props" :inputData="getInputData(props.id)"
@resize="(e) => handleResizeStop(e, props)"/>
</template>
<template #node-multiCurves="props">
<MultiCurvesNode :colors="pageSettingForm.colors" v-bind="props" :inputData="getInputData(props.id)"
@resize="(e) => handleResizeStop(e, props)"/>
</template>
<template #node-lineBar="props">
<LineBarNode :colors="pageSettingForm.colors" v-bind="props" :inputData="getInputData(props.id)"
@resize="(e) => handleResizeStop(e, props)"/>
</template>
<template #node-bar="props">
<BarNode :colors="pageSettingForm.colors" v-bind="props" :inputData="getInputData(props.id)"
@resize="(e) => handleResizeStop(e, props)"/>
</template>
<template #node-backgroundBar="props">
<BackgroundBarNode :colors="pageSettingForm.colors" v-bind="props" :inputData="getInputData(props.id)"
@resize="(e) => handleResizeStop(e, props)"/>
</template>
<template #node-multiBars="props">
<MultiBarsNode :colors="pageSettingForm.colors" v-bind="props" :inputData="getInputData(props.id)"
@resize="(e) => handleResizeStop(e, props)"/>
</template>
<template #node-pie="props">
<PieNode :colors="pageSettingForm.colors" v-bind="props" :inputData="getInputData(props.id)"
@resize="(e) => handleResizeStop(e, props)"/>
</template>
<template #node-annular="props">
<AnnularNode :colors="pageSettingForm.colors" v-bind="props" :inputData="getInputData(props.id)"
@resize="(e) => handleResizeStop(e, props)"/>
</template>
<template #node-radar="props">
<RadarNode :colors="pageSettingForm.colors" v-bind="props" :inputData="getInputData(props.id)"
@resize="(e) => handleResizeStop(e, props)"/>
</template>
<template #node-dashboard="props">
<DashboardNode :colors="pageSettingForm.colors" v-bind="props" :inputData="getInputData(props.id)"
@resize="(e) => handleResizeStop(e, props)"/>
</template>
<template #node-props="props">
<NightingaleRoseDiagramNode :colors="pageSettingForm.colors" v-bind="props"
:inputData="getInputData(props.id)"
@resize="(e) => handleResizeStop(e, props)"/>
</template>
<template #node-carousel="props">
<CarouselNode :colors="pageSettingForm.colors" v-bind="props" :inputData="getInputData(props.id)"
@resize="(e) => handleResizeStop(e, props)"/>
</template>
<template #node-data="props">
<DataNode :colors="pageSettingForm.colors" v-bind="props" :inputData="getInputData(props.id)"
@resize="(e) => handleResizeStop(e, props)"/>
</template>
<template #node-text="props">
<TextNode :colors="pageSettingForm.colors" v-bind="props" :inputData="getInputData(props.id)"
@resize="(e) => handleResizeStop(e, props)"/>
</template>
<template #node-img="imgNodeProps">
<ImgNode :colors="pageSettingForm.colors" v-bind="imgNodeProps" :inputData="getInputData(imgNodeProps.id)"
@resize="(e) => handleResizeStop(e, imgNodeProps)"/>
</template>
<template #node-icon="iconNodeProps">
<IconNode :colors="pageSettingForm.colors" v-bind="iconNodeProps"
:inputData="getInputData(iconNodeProps.id)"
@resize="(e) => handleResizeStop(e, iconNodeProps)"/>
</template>
<template #node-video="videoNodeProps">
<VideoNode :colors="pageSettingForm.colors" v-bind="videoNodeProps"
:inputData="getInputData(videoNodeProps.id)"
@resize="(e) => handleResizeStop(e, videoNodeProps)"/>
</template>
<template #node-timeline="timelineNodeProps">
<TimelineNode :colors="pageSettingForm.colors" v-bind="timelineNodeProps"
:inputData="getInputData(timelineNodeProps.id)"
@resize="(e) => handleResizeStop(e, timelineNodeProps)"/>
</template>
<template #node-digitalFlop="digitalFlopNodeProps">
<DigitalFlopNode :colors="pageSettingForm.colors" v-bind="digitalFlopNodeProps"
:inputData="getInputData(digitalFlopNodeProps.id)"
@resize="(e) => handleResizeStop(e, digitalFlopNodeProps)"/>
</template>
<template #node-inputNode="inputNodeProps">
<InputNode :colors="pageSettingForm.colors" v-bind="inputNodeProps"
:inputData="getInputData(inputNodeProps.id)"
@resize="(e) => handleResizeStop(e, inputNodeProps)"/>
</template>
<template #node-buttonNode="buttonNodeProps">
<ButtonNode :colors="pageSettingForm.colors" v-bind="buttonNodeProps"
:inputData="getInputData(buttonNodeProps.id)"
@resize="(e) => handleResizeStop(e, buttonNodeProps)"/>
</template>
<template #node-selectNode="selectNodeProps">
<SelectNode :colors="pageSettingForm.colors" v-bind="selectNodeProps"
:inputData="getInputData(selectNodeProps.id)"
@resize="(e) => handleResizeStop(e, selectNodeProps)"/>
</template>
<template #node-time="props">
<TimeNode :colors="pageSettingForm.colors" v-bind="props" :inputData="getInputData(props.id)"
@resize="(e) => handleResizeStop(e, props)"/>
</template>
<template #node-map="mapNodeProps">
<MapNode :colors="pageSettingForm.colors" v-bind="mapNodeProps" :inputData="getInputData(mapNodeProps.id)"
@resize="(e) => handleResizeStop(e, mapNodeProps)"/>
</template>
<template #node-staticData="staticDataNodeProps">
<StaticDataNode :colors="pageSettingForm.colors" v-bind="staticDataNodeProps"
:inputData="getInputData(staticDataNodeProps.id)"
@resize="(e) => handleResizeStop(e, staticDataNodeProps)"/>
</template>
<template #node-table="tableNodeProps">
<TableNode :colors="pageSettingForm.colors" v-bind="tableNodeProps"
:inputData="getInputData(tableNodeProps.id)"
@resize="(e) => handleResizeStop(e, tableNodeProps)"/>
</template>
<template #node-scrollTable="scrollTableNodeProps">
<ScrollTableNode :colors="pageSettingForm.colors" v-bind="scrollTableNodeProps"
:inputData="getInputData(scrollTableNodeProps.id)"
@resize="(e) => handleResizeStop(e, scrollTableNodeProps)"/>
</template>
<template #node-background="backgroundNodeProps">
<BackgroundNode :colors="pageSettingForm.colors" v-bind="backgroundNodeProps"
:inputData="getInputData(backgroundNodeProps.id)"
@resize="(e) => handleResizeStop(e, backgroundNodeProps)"/>
</template>
<template #node-pagination="paginationNodeProps">
<PaginationNode :colors="pageSettingForm.colors" v-bind="paginationNodeProps"
:inputData="getInputData(paginationNodeProps.id)"
@resize="(e) => handleResizeStop(e, paginationNodeProps)"/>
</template>
<template #node-menu="props">
<MenuNode :colors="pageSettingForm.colors" v-bind="props" :inputData="getInputData(props.id)"
@resize="(e) => handleResizeStop(e, props)"/>
</template>
<template #node-tree="props">
<TreeNode :colors="pageSettingForm.colors" v-bind="props" :inputData="getInputData(props.id)"
@resize="(e) => handleResizeStop(e, props)"/>
</template>
</VueFlow>
</FlowRuler>
</div>
</div>
</div>
</template>
<script setup>
import {MarkerType, useVueFlow, VueFlow} from '@vue-flow/core';
import tool from '../../util/tool.js';
import {Background} from '@vue-flow/background';
import * as icon from '@element-plus/icons-vue';
import BoardNodes from '../inside/boardNodes/index.vue'
import CustomData from '../inside/customData/index.vue'
import FormNodes from '../inside/formNodes/index.vue'
import OtherNodes from '../inside/otherNodes/index.vue'
import FlowRuler from '../inside/FlowRuler.vue'
const nodes = ref([])
const {onDragStart, onDrop, onDragOver} = tool();
</script>
<style lang="less" scoped>
.hw-root {
width: 100%;
height: 100%;
min-height: 80vh;
--visual-editor-top-height: 50px;
}
.visual-editor {
width: 100%;
height: 100%;
overflow: hidden;
.visual-editor-top {
width: 100%;
height: var(--visual-editor-top-height);
background-color: #1C1F20;
border-bottom: 1px solid #000;
position: relative;
.options-menu {
vertical-align: top;
display: inline-block;
position: absolute;
right: 20px;
}
}
.visual-editor-left {
width: 200px;
height: calc(100% - var(--visual-editor-top-height));
display: inline-block;
overflow: auto;
background-color: #1C1F20;
border-right: 1px solid #000;
.title {
width: 100%;
height: 50px;
line-height: 50px;
text-align: center;
color: #FFF;
background-color: #293133;
border-bottom: 1px solid #000;
}
.levels {
width: 100%;
height: calc(100% - 50px);
overflow: auto;
background-color: #1C1F20;
&::-webkit-scrollbar {
display: none;
}
.level {
display: flex;
align-items: center;
height: 50px;
padding-left: 20px;
border-bottom: 1px solid #000;
.icon, .boardName {
display: flex;
align-items: center;
}
.icon {
width: 40px;
min-width: 40px;
}
.boardName {
font-size: 14px;
color: #fff;
}
}
.isSelect {
background-color: #494949;
.isShow {
border-right: 1px solid #373737;
}
}
}
}
.visual-editor-center {
width: calc(100% - 500px);
display: inline-block;
background-color: #1C1F20;
height: calc(100vh - var(--visual-editor-top-height));
overflow: auto;
}
}
</style>

@ -0,0 +1,158 @@
<template>
<div class="hw-flow-ruler-container">
<div class="hw-flow-ruler-x">
<canvas ref="rulerXCanvas" class="hw-flow-ruler-canvas"></canvas>
</div>
<div class="hw-flow-ruler-main-content">
<div class="hw-flow-ruler-y">
<canvas ref="rulerYCanvas" class="hw-flow-ruler-canvas"></canvas>
</div>
<slot/>
</div>
</div>
</template>
<script setup>
import {ref, onMounted} from 'vue';
import {useVueFlow} from '@vue-flow/core';
const rulerXCanvas = ref(null);
const rulerYCanvas = ref(null);
const {viewport} = useVueFlow();
let animationFrameId = null;
let lastRenderTime = 0;
const FRAME_INTERVAL = 1000 / 60;
const getNiceStep = (target) => {
const steps = [1, 2, 5, 10, 20, 25, 50, 100, 200, 500, 1000];
return steps.find(s => s * viewport.value.zoom >= target) || 1000;
};
const drawRulers = () => {
const scale = viewport.value.zoom;
const offsetX = viewport.value.x;
const offsetY = viewport.value.y;
const step = getNiceStep(10);
const bigStep = step * 10;
const leftOffset = 30;
const rulerSize = 30;
const xCanvas = rulerXCanvas.value;
const xCtx = xCanvas.getContext('2d');
xCanvas.width = xCanvas.offsetWidth * window.devicePixelRatio;
xCanvas.height = xCanvas.offsetHeight * window.devicePixelRatio;
xCtx.scale(window.devicePixelRatio, window.devicePixelRatio);
xCtx.clearRect(0, 0, xCanvas.offsetWidth, xCanvas.offsetHeight);
xCtx.fillStyle = '#999';
xCtx.font = '10px sans-serif';
const canvasWidth = xCanvas.offsetWidth;
const startWorldX = Math.floor((-offsetX - leftOffset) / scale / step) * step;
const endWorldX = (-offsetX + canvasWidth) / scale;
for (let worldX = startWorldX; worldX < endWorldX; worldX += step) {
const screenX = worldX * scale + offsetX + leftOffset;
xCtx.beginPath();
if (Math.round(worldX) % bigStep === 0) {
xCtx.strokeStyle = '#fff';
xCtx.moveTo(screenX, rulerSize - 20);
xCtx.lineTo(screenX, rulerSize);
xCtx.stroke();
xCtx.fillStyle = '#ccc';
xCtx.fillText(Math.round(worldX).toString(), screenX + 2, 10);
} else {
xCtx.strokeStyle = '#666';
xCtx.moveTo(screenX, rulerSize - 10);
xCtx.lineTo(screenX, rulerSize);
xCtx.stroke();
}
}
const yCanvas = rulerYCanvas.value;
const yCtx = yCanvas.getContext('2d');
yCanvas.width = yCanvas.offsetWidth * window.devicePixelRatio;
yCanvas.height = yCanvas.offsetHeight * window.devicePixelRatio;
yCtx.scale(window.devicePixelRatio, window.devicePixelRatio);
yCtx.clearRect(0, 0, yCanvas.offsetWidth, yCanvas.offsetHeight);
yCtx.fillStyle = '#999';
yCtx.font = '10px sans-serif';
const canvasHeight = yCanvas.offsetHeight;
const startWorldY = Math.floor((-offsetY) / scale / step) * step;
const endWorldY = (-offsetY + canvasHeight) / scale;
for (let worldY = startWorldY; worldY < endWorldY; worldY += step) {
const screenY = worldY * scale + offsetY;
yCtx.beginPath();
if (Math.round(worldY) % bigStep === 0) {
yCtx.strokeStyle = '#fff';
yCtx.save();
yCtx.translate(rulerSize - 2, screenY + 5);
yCtx.rotate(-Math.PI / 2);
yCtx.fillStyle = '#ccc';
yCtx.fillText(Math.round(worldY).toString(), 0, 0);
yCtx.restore();
yCtx.moveTo(rulerSize - 20, screenY);
yCtx.lineTo(rulerSize, screenY);
yCtx.stroke();
} else {
yCtx.strokeStyle = '#666';
yCtx.moveTo(rulerSize - 10, screenY);
yCtx.lineTo(rulerSize, screenY);
yCtx.stroke();
}
}
};
const renderLoop = (time) => {
if (time - lastRenderTime >= FRAME_INTERVAL) {
drawRulers();
lastRenderTime = time;
}
animationFrameId = requestAnimationFrame(renderLoop);
};
onMounted(() => {
animationFrameId = requestAnimationFrame(renderLoop);
});
</script>
<style scoped>
.hw-flow-ruler-container {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
.hw-flow-ruler-x {
height: 30px;
background: #1F2122;
border-bottom: 1px solid #444;
overflow: hidden;
}
.hw-flow-ruler-y {
width: 30px;
background: #1F2122;
border-right: 1px solid #444;
height: 100%;
overflow: hidden;
}
.hw-flow-ruler-main-content {
display: flex;
height: calc(100% - 30px);
}
.hw-flow-ruler-canvas {
width: 100%;
height: 100%;
display: block;
}
</style>

@ -0,0 +1,177 @@
<template>
<div>
<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>
<el-image style="width: 100%; height: 100%" :src="lineImg" fit="contain"/>
</template>
<div class="moduleText">折线</div>
</el-card>
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'multiLines')"
:style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<el-image style="width: 100%; height: 100%" :src="multiLinesImg" fit="contain"/>
</template>
<div class="moduleText">多折线</div>
</el-card>
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'curve')" :style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<el-image style="width: 100%; height: 100%" :src="curveImg" fit="contain"/>
</template>
<div class="moduleText">曲线</div>
</el-card>
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'multiCurves')"
:style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<el-image style="width: 100%; height: 100%" :src="multiCurvesImg" fit="contain"/>
</template>
<div class="moduleText">多曲线
</div>
</el-card>
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'lineBar')"
:style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<el-image style="width: 100%; height: 100%" :src="lineBarImg" fit="contain"/>
</template>
<div class="moduleText">双轴图
</div>
</el-card>
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'bar')" :style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<el-image style="width: 100%; height: 100%" :src="barImg" fit="contain"/>
</template>
<div class="moduleText">柱状图</div>
</el-card>
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'backgroundBar')"
:style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<el-image style="width: 100%; height: 100%" :src="backgroundBarImg" fit="contain"/>
</template>
<div class="moduleText">背景柱状图</div>
</el-card>
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'multiBars')"
:style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<el-image style="width: 100%; height: 100%" :src="multiBarsImg" fit="contain"/>
</template>
<div class="moduleText">多柱状图
</div>
</el-card>
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'pie')" :style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<el-image style="width: 100%; height: 100%" :src="pieImg" fit="contain"/>
</template>
<div class="moduleText">饼图</div>
</el-card>
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'annular')"
:style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<el-image style="width: 100%; height: 100%" :src="annularImg" fit="contain"/>
</template>
<div class="moduleText">中空饼图</div>
</el-card>
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'dashboard')"
:style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<el-image style="width: 100%; height: 100%" :src="dashboardImg" fit="contain"/>
</template>
<div class="moduleText">仪表盘</div>
</el-card>
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'radar')"
:style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<el-image style="width: 100%; height: 100%" :src="radarImg" fit="contain"/>
</template>
<div class="moduleText">雷达图</div>
</el-card>
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'nightingaleRoseDiagram')"
:style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<el-image style="width: 100%; height: 100%" :src="nightingaleRoseDiagramImg" fit="contain"/>
</template>
<div class="moduleText">南丁格尔玫瑰图</div>
</el-card>
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'carousel')"
:style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<el-image style="width: 100%; height: 100%" :src="carouselImg" fit="contain"/>
</template>
<div class="moduleText">轮播图</div>
</el-card>
</div>
</template>
<script setup>
import backgroundBarImg from '../../../assets/images/boardNodes/backgroundBarImg.png';
import pieImg from '../../../assets/images/boardNodes/pieImg.png';
import multiCurvesImg from '../../../assets/images/boardNodes/multiCurvesImg.png';
import lineBarImg from '../../../assets/images/boardNodes/lineBarImg.png';
import multiLinesImg from '../../../assets/images/boardNodes/multiLinesImg.png';
import multiBarsImg from '../../../assets/images/boardNodes/multiBarsImg.png';
import annularImg from '../../../assets/images/boardNodes/annularImg.png';
import radarImg from '../../../assets/images/boardNodes/radarImg.png';
import carouselImg from '../../../assets/images/boardNodes/carouselImg.png';
import nightingaleRoseDiagramImg from '../../../assets/images/boardNodes/nightingaleRoseDiagramImg.png';
import curveImg from '../../../assets/images/boardNodes/curveImg.png';
import dashboardImg from '../../../assets/images/boardNodes/dashboardImg.png';
import lineImg from '../../../assets/images/boardNodes/lineImg.png';
import barImg from '../../../assets/images/boardNodes/barImg.png';
defineProps({
onDragStart: Function
})
</script>
<style scoped lang="less">
.moduleCard {
width: 100px;
vertical-align: top;
:deep(.el-card__body) {
height: 40px;
padding: 0 !important;
}
:deep(.el-card__header) {
padding: 0 !important;
}
.moduleText {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
text-align: center;
font-size: 12px;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
</style>

@ -0,0 +1,73 @@
<template>
<div>
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'map')" :style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<div style="width: 100%;height: 100px;text-align: center">
<el-image style="width:80px;height: 80px;margin-top: 10px;" :src="mapImg" fit="contain"/>
</div>
</template>
<div class="moduleText">映射</div>
</el-card>
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'staticData')"
:style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<el-image style="width: 100%; height: 100%" :src="customizationRequestImg" fit="contain"/>
</template>
<div class="moduleText">静态数据</div>
</el-card>
<template v-for="i in customData">
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'customData',i)"
:style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<el-image style="width: 100%; height: 100%" :src="network" fit="contain"/>
</template>
<div class="moduleText">{{ i.name }}</div>
</el-card>
</template>
</div>
</template>
<script setup>
import customizationRequestImg from '@/assets/images/customData/customizationRequestImg.png';
import mapImg from '@/assets/images/customData/mapImg.png';
import network from '@/assets/images/customData/network.png';
defineProps({
onDragStart: Function
})
</script>
<style lang="less" scoped>
.moduleCard {
width: 100px;
vertical-align: top;
:deep(.el-card__body) {
height: 40px;
padding: 0 !important;
}
:deep(.el-card__header) {
padding: 0 !important;
}
.moduleText {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
text-align: center;
font-size: 12px;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
</style>

@ -0,0 +1,213 @@
<template>
<div>
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'text')" :style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<div style="width: 100%;height: 100%;text-align: center">
<el-image style="width:50px;height: 50px;text-align: center" :src="textImg" fit="contain"/>
</div>
</template>
<div class="moduleText">文字</div>
</el-card>
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'buttonNode')"
:style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<div style="width: 100%;height: 100%;text-align: center">
<el-image style="width:50px;height: 50px;text-align: center" :src="inputNodeImg" fit="contain"/>
</div>
</template>
<div class="moduleText">按钮</div>
</el-card>
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'inputNode')"
:style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<div style="width: 100%;height: 100%;text-align: center">
<el-image style="width:50px;height: 50px;text-align: center" :src="inputNodeImg2" fit="contain"/>
</div>
</template>
<div class="moduleText">输入框</div>
</el-card>
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'selectNode')"
:style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<div style="width: 100%;height: 100%;text-align: center">
<el-image style="width:50px;height: 50px;text-align: center" :src="inputNodeImg3" fit="contain"/>
</div>
</template>
<div class="moduleText">选择框</div>
</el-card>
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'time')" :style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<div style="width: 100%;height: 100%;text-align: center">
<el-image style="width:50px;height: 50px;text-align: center" :src="timeImg" fit="contain"/>
</div>
</template>
<div class="moduleText">时间</div>
</el-card>
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'img')" :style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<div style="width: 100%;height: 100%;text-align: center">
<el-image style="width:50px;height: 50px;text-align: center" :src="imgImg" fit="contain"/>
</div>
</template>
<div class="moduleText">图片</div>
</el-card>
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'icon')" :style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<div style="width: 100%;height: 100%;text-align: center">
<el-image style="width:50px;height: 50px;text-align: center" :src="iconImg" fit="contain"/>
</div>
</template>
<div class="moduleText">图标</div>
</el-card>
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'video')" :style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<div style="width: 100%;height: 100%;text-align: center">
<el-image style="width:50px;height: 50px;text-align: center" :src="videoImg" fit="contain"/>
</div>
</template>
<div class="moduleText">视频</div>
</el-card>
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'timeline')" :style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<div style="width: 100%;height: 100%;text-align: center">
<el-image style="width:50px;height: 50px;text-align: center" :src="timelineImg" fit="contain"/>
</div>
</template>
<div class="moduleText">时间线</div>
</el-card>
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'digitalFlop')"
:style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<div style="width: 100%;height: 100%;text-align: center">
<el-image style="width:50px;height: 50px;text-align: center" :src="digitalFlopImg" fit="contain"/>
</div>
</template>
<div class="moduleText">数字翻牌器</div>
</el-card>
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'table')"
:style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<div style="width: 100%;height: 100%;text-align: center">
<el-image style="width:50px;height: 50px;text-align: center" :src="scrollTableImg" fit="contain"/>
</div>
</template>
<div class="moduleText">普通表格</div>
</el-card>
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'scrollTable')"
:style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<div style="width: 100%;height: 100%;text-align: center">
<el-image style="width:50px;height: 50px;text-align: center" :src="scrollTableImg2" fit="contain"/>
</div>
</template>
<div class="moduleText">滚动表格</div>
</el-card>
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'background')"
:style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<div style="width: 100%;height: 100%;text-align: center">
<el-image style="width:50px;height: 50px;text-align: center" :src="backgroundImg" fit="contain"/>
</div>
</template>
<div class="moduleText">背景</div>
</el-card>
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'pagination')"
:style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<div style="width: 100%;height: 100%;text-align: center">
<el-image style="width:50px;height: 50px;text-align: center" :src="backgroundImg" fit="contain"/>
</div>
</template>
<div class="moduleText">分页</div>
</el-card>
</div>
</template>
<script setup>
import backgroundImg from '@/assets/images/formNodes/backgroundImg.png';
import digitalFlopImg from '@/assets/images/formNodes/digitalFlopImg.png';
import iconImg from '@/assets/images/formNodes/iconImg.png';
import imgImg from '@/assets/images/formNodes/imgImg.png';
import inputNodeImg from '@/assets/images/formNodes/inputNodeImg.png';
import inputNodeImg2 from '@/assets/images/formNodes/inputNodeImg2.png';
import inputNodeImg3 from '@/assets/images/formNodes/inputNodeImg3.png';
import scrollTableImg from '@/assets/images/formNodes/scrollTableImg.png';
import scrollTableImg2 from '@/assets/images/formNodes/scrollTableImg2.png';
import textImg from '@/assets/images/formNodes/textImg.png';
import timeImg from '@/assets/images/formNodes/timeImg.png';
import timelineImg from '@/assets/images/formNodes/timelineImg.png';
import videoImg from '@/assets/images/formNodes/videoImg.png';
defineProps({
onDragStart: Function
})
</script>
<style lang="less" scoped>
.moduleCard {
width: 100px;
vertical-align: top;
:deep(.el-card__body) {
height: 40px;
padding: 0 !important;
}
:deep(.el-card__header) {
padding: 0 !important;
}
.moduleText {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
text-align: center;
font-size: 12px;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
</style>

@ -0,0 +1,68 @@
<template>
<div>
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'menu')" :style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<div style="width: 100%;height: 50px;line-height:50px;text-align: center">
<el-icon>
<component :is="icons.Menu"/>
</el-icon>
</div>
</template>
<div class="moduleText">菜单</div>
</el-card>
<el-card class="moduleCard" shadow="never" :draggable="true"
@dragstart="onDragStart($event, 'tree')" :style="{display:'inline-block',margin:'0 4px 4px 0'}"
:body-style="{padding:'4px 0'}">
<template #header>
<div style="width: 100%;height: 50px;display: flex; justify-content: center; align-items: center;">
<svg t="1768964556223" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="4878" width="30" height="30">
<path
d="M554.666667 682.666667h85.333333v128h-213.333333v-128h85.333333v-128H256v128h85.333333v128H128v-128h85.333333v-170.666667h298.666667V384h-85.333333V256h213.333333v128h-85.333333v128h298.666666v170.666667h85.333334v128h-213.333334v-128h85.333334v-128h-256v128z m42.666666-384h-128v42.666666h128V298.666667zM298.666667 725.333333H170.666667v42.666667h128v-42.666667z m597.333333 0h-128v42.666667h128v-42.666667z m-298.666667 0h-128v42.666667h128v-42.666667z"
fill="#444444" p-id="4879"></path>
</svg>
</div>
</template>
<div class="moduleText">树状图</div>
</el-card>
</div>
</template>
<script setup>
defineProps({
icons: Object,
onDragStart: Function
})
</script>
<style lang="less" scoped>
.moduleCard {
width: 100px;
vertical-align: top;
:deep(.el-card__body) {
height: 40px;
padding: 0 !important;
}
:deep(.el-card__header) {
padding: 0 !important;
}
.moduleText {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
text-align: center;
font-size: 12px;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
</style>

@ -0,0 +1,9 @@
import VisualEditor from './components/external/VisualEditor.vue'
export { VisualEditor }
export default {
install(app) {
app.component('VisualEditor', VisualEditor)
}
}

@ -0,0 +1,515 @@
import {useVueFlow} from '@vue-flow/core';
import {ref,} from 'vue';
import {v4 as uuidv4} from 'uuid';
const getId = (type) => {
return `${type}_${uuidv4().replaceAll('-', '_')}`;
};
const optionList = (e) => {
const normalOption = {
title: '', boundaryGap: true,
}
const chartOption = {
gridTop: 30,
gridLeft: 5,
gridBottom: 10,
gridRight: 10,
xName: '',
xNameLocation: 'end',
xAxisLineShow: true,
xAxisLineColor: '#DCE2E8',
xAxisTickShow: true,
xAxisTickInside: false,
xAxisTickColor: '#DCE2E8',
xAxisLabelShow: true,
xAxisLabelInterval: true,
xAxisLabelInside: false,
xAxisLabelRotate: 0,
xAxisLabelFormatter: '',
xAxisLabelColor: '#fff',
xAxisLabelFontSize: 12,
xAxisLabelMargin: 3,
xAxisSplitLineShow: false,
xAxisSplitLineColor: '#DCE2E8',
xAxisSplitLineType: 'solid',
yName: '',
yNames: [],
yNameLocation: 'end',
yAxisLineShow: true,
yAxisLineColor: '#DCE2E8',
yAxisTickShow: true,
yAxisTickInside: false,
yAxisTickColor: '#DCE2E8',
yAxisLabelShow: true,
yAxisLabelInterval: true,
yAxisLabelInside: false,
yAxisLabelRotate: 0,
yAxisLabelFormatter: '',
yAxisLabelColor: '#fff',
yAxisLabelFontSize: 12,
yAxisLabelMargin: 3,
yAxisSplitLineShow: false,
yAxisSplitLineColor: '#DCE2E8',
yAxisSplitLineType: 'solid',
lineSymbolShow: false,
lineSymbolSize: 5,
lineSymbolType: 'circle',
seriesColor: '#9E87FF',
tooltip: true,
legend: true,
}
const options = {
...normalOption, ...chartOption,
}
return e.map(v => {
return {[v]: options[v]};
})
}
const getOption = (e) => {
let res = {}
if (e === 'line' || e === 'multiLines') {
return optionList(['title', 'boundaryGap', 'gridTop', 'gridLeft', 'gridBottom', 'gridRight', 'xName', 'xNameLocation', 'xAxisLineShow', 'xAxisLineColor', 'xAxisTickShow', 'xAxisTickInside', 'xAxisTickColor', 'xAxisLabelShow', 'xAxisLabelInterval', 'xAxisLabelInside', 'xAxisLabelRotate', 'xAxisLabelFormatter', 'xAxisLabelColor', 'xAxisLabelFontSize', 'xAxisLabelMargin', 'xAxisSplitLineShow', 'xAxisSplitLineColor', 'xAxisSplitLineType', 'yName', 'yNames', 'yNameLocation', 'yAxisLineShow', 'yAxisLineColor', 'yAxisTickShow', 'yAxisTickInside', 'yAxisTickColor', 'yAxisLabelShow', 'yAxisLabelInterval', 'yAxisLabelInside', 'yAxisLabelRotate', 'yAxisLabelFormatter', 'yAxisLabelColor', 'yAxisLabelFontSize', 'yAxisLabelMargin', 'yAxisSplitLineShow', 'yAxisSplitLineColor', 'yAxisSplitLineType', 'lineSymbolShow', 'lineSymbolSize', 'lineSymbolType', 'seriesColor', 'tooltip', 'legend'])
} else if (e === 'bar' || e === 'multiBars') {
return {
title: '',
yNames: [],
gridTop: 30,
gridLeft: 5,
gridBottom: 10,
gridRight: 10,
xName: '',
xNameLocation: 'end',
xAxisLineShow: true,
xAxisLineColor: '#DCE2E8',
xAxisTickShow: true,
xAxisTickInside: false,
xAxisTickColor: '#DCE2E8',
xAxisLabelShow: true,
xAxisLabelInterval: true,
xAxisLabelInside: false,
xAxisLabelRotate: 0,
xAxisLabelFormatter: '',
xAxisLabelColor: '#fff',
xAxisLabelFontSize: 12,
xAxisLabelMargin: 3,
xAxisSplitLineShow: false,
xAxisSplitLineColor: '#DCE2E8',
xAxisSplitLineType: 'solid',
yName: '',
yNameLocation: 'end',
yAxisLineShow: true,
yAxisLineColor: '#DCE2E8',
yAxisTickShow: true,
yAxisTickInside: false,
yAxisTickColor: '#DCE2E8',
yAxisLabelShow: true,
yAxisLabelInterval: true,
yAxisLabelInside: false,
yAxisLabelRotate: 0,
yAxisLabelFormatter: '',
yAxisLabelColor: '#fff',
yAxisLabelFontSize: 12,
yAxisLabelMargin: 3,
yAxisSplitLineShow: false,
yAxisSplitLineColor: '#DCE2E8',
yAxisSplitLineType: 'solid',
seriesColor: '#9E87FF',
tooltip: true,
legend: true,
isHorizontal: false,
};
} else if (e === 'backgroundBar') {
return {
title: '',
yNames: [],
gridTop: 30,
gridLeft: 5,
gridBottom: 10,
gridRight: 10,
xName: '',
xNameLocation: 'end',
xAxisLineShow: true,
xAxisLineColor: '#DCE2E8',
xAxisTickShow: true,
xAxisTickInside: false,
xAxisTickColor: '#DCE2E8',
xAxisLabelShow: true,
xAxisLabelInterval: true,
xAxisLabelInside: false,
xAxisLabelRotate: 0,
xAxisLabelFormatter: '',
xAxisLabelColor: '#fff',
xAxisLabelFontSize: 12,
xAxisLabelMargin: 3,
xAxisSplitLineShow: false,
xAxisSplitLineColor: '#DCE2E8',
xAxisSplitLineType: 'solid',
yName: '',
yNameLocation: 'end',
yAxisLineShow: true,
yAxisLineColor: '#DCE2E8',
yAxisTickShow: true,
yAxisTickInside: false,
yAxisTickColor: '#DCE2E8',
yAxisLabelShow: true,
yAxisLabelInterval: true,
yAxisLabelInside: false,
yAxisLabelRotate: 0,
yAxisLabelFormatter: '',
yAxisLabelColor: '#fff',
yAxisLabelFontSize: 12,
yAxisLabelMargin: 3,
yAxisSplitLineShow: false,
yAxisSplitLineColor: '#DCE2E8',
yAxisSplitLineType: 'solid',
tooltip: true,
legend: true,
seriesColor: '#9E87FF',
backgroundColor: 'rgba(180, 180, 180, 0.2)',
isHorizontal: false,
};
} else if (e === 'curve' || e === 'multiCurves') {
return {
title: '',
yNames: [],
gridTop: 30,
gridLeft: 5,
gridBottom: 10,
gridRight: 10,
boundaryGap: true,
xName: '',
xNameLocation: 'end',
xAxisLineShow: true,
xAxisLineColor: '#DCE2E8',
xAxisTickShow: true,
xAxisTickInside: false,
xAxisTickColor: '#DCE2E8',
xAxisLabelShow: true,
xAxisLabelInterval: true,
xAxisLabelInside: false,
xAxisLabelRotate: 0,
xAxisLabelFormatter: '',
xAxisLabelColor: '#fff',
xAxisLabelFontSize: 12,
xAxisLabelMargin: 3,
xAxisSplitLineShow: false,
xAxisSplitLineColor: '#DCE2E8',
xAxisSplitLineType: 'solid',
yName: '',
yNameLocation: 'end',
yAxisLineShow: true,
yAxisLineColor: '#DCE2E8',
yAxisTickShow: true,
yAxisTickInside: false,
yAxisTickColor: '#DCE2E8',
yAxisLabelShow: true,
yAxisLabelInterval: true,
yAxisLabelInside: false,
yAxisLabelRotate: 0,
yAxisLabelFormatter: '',
yAxisLabelColor: '#fff',
yAxisLabelFontSize: 12,
yAxisLabelMargin: 3,
yAxisSplitLineShow: false,
yAxisSplitLineColor: '#DCE2E8',
yAxisSplitLineType: 'solid',
lineSymbolShow: false,
lineSymbolSize: 5,
lineSymbolType: 'circle',
seriesColor: '#9E87FF',
tooltip: true,
legend: true
};
} else if (e === 'lineBar') {
return {
title: '',
yNames: [],
gridTop: 30,
gridLeft: 5,
gridBottom: 10,
gridRight: 10,
xName: '',
xNameLocation: 'end',
xAxisLineShow: true,
xAxisLineColor: '#DCE2E8',
xAxisTickShow: true,
xAxisTickInside: false,
xAxisTickColor: '#DCE2E8',
xAxisLabelShow: true,
xAxisLabelInterval: true,
xAxisLabelInside: false,
xAxisLabelRotate: 0,
xAxisLabelFormatter: '',
xAxisLabelColor: '#fff',
xAxisLabelFontSize: 12,
xAxisLabelMargin: 3,
xAxisSplitLineShow: false,
xAxisSplitLineColor: '#DCE2E8',
xAxisSplitLineType: 'solid',
yName: '',
yNameLocation: 'end',
yAxisLineShow: true,
yAxisLineColor: '#DCE2E8',
yAxisTickShow: true,
yAxisTickInside: false,
yAxisTickColor: '#DCE2E8',
yAxisLabelShow: true,
yAxisLabelInterval: true,
yAxisLabelInside: false,
yAxisLabelRotate: 0,
yAxisLabelFormatter: '',
yAxisLabelColor: '#fff',
yAxisLabelFontSize: 12,
yAxisLabelMargin: 3,
yAxisSplitLineShow: false,
yAxisSplitLineColor: '#DCE2E8',
yAxisSplitLineType: 'solid',
lineSymbolShow: false,
lineSymbolSize: 5,
lineSymbolType: 'circle',
tooltip: true,
legend: true,
seriesColor: '#9E87FF'
};
} else if (e === 'radar') {
return {
title: '',
seriesColor: '#9E87FF',
yNames: [],
tooltip: true,
legend: true,
radarCenter: [30, 50],
radarRadius: 40,
radarShape: 'polygon',
radarSplitNumber: 5,
splitLineColor: '#9E87FF'
};
} else if (e === 'pie' || e === 'nightingaleRoseDiagram') {
return {
title: '',
yNames: [],
tooltip: true,
legend: true,
label: true,
pieCenter: 50,
pieLabelFormatter: '',
legendColor: '#fff'
};
} else if (e === 'annular') {
return {
title: '',
yNames: [],
tooltip: true,
legend: true,
label: true,
pieCenter: 50,
pieRadius: [30, 70],
colorList: ['#D6F4FF', '#D6F4FF', '#D6F4FF', '#D6F4FF'],
};
} else if (e === 'dashboard') {
return {title: '', yNames: [], tooltip: true, legend: true, label: true};
} else if (e === 'customBoard') {
return {title: '', yNames: []};
} else if (e === 'data') {
return {timeout: 5};
} else if (e === 'staticData') {
return {defaultInputArea: '', field: 'input', defaultDataType: ''};
} else if (e === 'customData') {
return {timeout: 5};
} else if (e === 'map') {
return {dataMap: []};
} else if (e === 'inputNode') {
return {field: 'input', defaultInput: ''};
} else if (e === 'buttonNode') {
return {backgroundColor: '#409EFF', color: '#fff', text: '按钮', sendType: ['ok']};
} else if (e === 'selectNode') {
return {field: 'select', defaultInput: '', labelField: 'label', valueField: 'value'};
} else if (e === 'time') {
return {startTimeId: 'startTime', endTimeId: 'endTime', defaultTime: [], format: ''};
} else if (e === 'text') {
return {text: '文字', align: '', color: '#fff', whiteSpace: 'nowrap'};
} else if (e === 'img') {
return {imgSrc: ''};
} else if (e === 'icon') {
return {icon: 'Tools', iconSrc: ''};
} else if (e === 'video') {
return {videoSrc: ''};
} else if (e === 'timeline') {
return {
color: '#fff', timestampColor: '#fff', field: 'content', timestampField: 'timestamp', isTimestamp: true
};
} else if (e === 'scrollTable' || e === 'table') {
return {
tableOptions: [],
tableCellOptions: [],
tableClassOption: {},
isThShow: true,
thHeight: '32px',
tdHeight: '28px',
thColor: '#D6F4FF',
tdColor: ['#D6F4FF', '#D6F4FF', '#D6F4FF', '#D6F4FF'],
thBgColor: '#1E90FF',
tdBgColor: ['#0D2B3D', '#103B4C', '#14576B', '#187E99']
};
} else if (e === 'customTable') {
return {
tableOptions: [],
tableCellOptions: [],
cellNum: 2,
dataNum: 2,
tdHeight: '28px',
tdColor: ['#D6F4FF', '#D6F4FF', '#D6F4FF', '#D6F4FF'],
tdBgColor: ['#0D2B3D', '#103B4C', '#14576B', '#187E99']
};
} else if (e === 'carousel') {
return {swiperOptions: {}, imageFit: 'contain', carouselImages: []};
} else if (e === 'background') {
return {backgroundColor: '#fff', isBorder: true, borderColor: '#fff', backgroundImage: ''};
} else if (e === 'pagination') {
return {pageSizeField: 'pageSize', currentPageField: 'pageNum'};
} else if (e === 'digitalFlop') {
return {
field: '',
color: '#fff',
fontSize: 22,
number: 123,
numQuantity: 3,
backgroundColor: 'rgba(180, 180, 180, 0.2)',
isBorder: true,
borderColor: '#fff'
};
} else if (e === 'menu') {
return {menuField: '', activeField: '', field: '', color: '#fff', activeColor: '#ffd04b'};
} else if (e === 'tree') {
return {level: 2, treeOptions: []};
} else {
return {};
}
};
const getNodeSize = (e) => {
if (e === 'line' || e === 'multiLines' || e === 'bar' || e === 'multiBars' || e === 'curve' || e === 'multiCurves' || e === 'customBoard' || e === 'pie' || e === 'annular' || e === 'dashboard' || e === 'radar') {
return {width: 300, height: 150};
} else if (e === 'data' || e === 'customData') {
return {width: 150, height: 50};
} else if (e === 'inputNode') {
return {width: 100, height: 30};
} else if (e === 'buttonNode') {
return {width: 100, height: 30};
} else if (e === 'staticData') {
return {width: 100, height: 60};
} else if (e === 'text') {
return {width: 100, height: 30};
} else if (e === 'time') {
return {width: 200, height: 30};
} else if (e === 'img') {
return {width: 300, height: 300};
} else if (e === 'map') {
return {width: 100, height: 30};
} else if (e === 'menu') {
return {width: 200, height: 800};
} else if (e === 'scrollTable' || e === 'table') {
return {width: 500, height: 300};
} else if (e === 'tree') {
return {width: 800, height: 800};
} else {
return {width: 100, height: 100};
}
};
const nameEnum = {
line: '折线',
multiLines: '多折线',
curve: '曲线',
multiCurves: '多曲线',
lineBar: '双轴图',
bar: '柱状图',
backgroundBar: '背景柱状图',
multiBars: '多柱状图',
pie: '饼图',
annular: '中空饼图',
dashboard: '仪表盘',
radar: '雷达图',
nightingaleRoseDiagram: '南丁格尔玫瑰图',
carousel: '轮播图',
map: '映射',
staticData: '静态数据',
customData: '自定义数据',
text: '文字',
inputNode: '输入框',
time: '时间',
img: '图片',
icon: '图标',
video: '视频',
timeline: '时间线',
digitalFlop: '数字翻牌器',
scrollTable: '滚动表格',
background: '背景',
tree: '树状图'
};
const tool = () => {
const nodeType = ref('');
const customData = ref({script: ''});
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 = {...customData.value, ...(data || {})};
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;

@ -0,0 +1,38 @@
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
// @ 指向 src 目录
'@': path.resolve(__dirname, 'src')
}
},
build: {
lib: {
entry: path.resolve(__dirname, 'src/index.js'),
name: 'ScrinVisualEditor',
fileName: (format) => {
if (format === 'es') return 'scrin-visual-editor.es.js'
if (format === 'umd') return 'scrin-visual-editor.umd.js'
return `scrin-visual-editor.${format}.js`
},
cssFileName: 'style'
},
preserveModules: false,
assetsInlineLimit: 0,
output: {
preserveModulesRoot: 'src',
},
rollupOptions: {
external: ['vue'],
output: {
globals: {
vue: 'Vue'
}
}
}
}
})
Loading…
Cancel
Save