|
@@ -1,5 +1,487 @@
|
|
|
<template>
|
|
|
- <div class=""></div>
|
|
|
+ <div class="content-main">
|
|
|
+ <div class="tool-container">
|
|
|
+ <el-button type="primary" text @click="copy">复制</el-button>
|
|
|
+ <el-divider direction="vertical" />
|
|
|
+ <el-button type="primary" text @click="paste">粘贴</el-button>
|
|
|
+ <el-divider direction="vertical" />
|
|
|
+ <el-button type="primary" text @click="del">删除</el-button>
|
|
|
+ <el-divider direction="vertical" />
|
|
|
+ <el-button type="primary" text @click="save">保存</el-button>
|
|
|
+ <!-- <el-divider direction="vertical" />
|
|
|
+ <el-button type="primary" text @click="exportPng">导出PNG</el-button> -->
|
|
|
+ </div>
|
|
|
+ <div id="" class="content-container">
|
|
|
+ <div class="content">
|
|
|
+ <div ref="stencilContainer" class="stencil"></div>
|
|
|
+ <div id="graphContainer" ref="graphContainer" class="graph-content"></div>
|
|
|
+ <!-- <img src="@/assets/images/road.png" class="roadback" alt="" /> -->
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</template>
|
|
|
-<script setup lang="ts"></script>
|
|
|
-<style lang="scss" scoped></style>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { Graph, Path, Edge, StringExt, Node, Cell, Model, DataUri } from '@antv/x6';
|
|
|
+import { Transform } from '@antv/x6-plugin-transform';
|
|
|
+import { Selection } from '@antv/x6-plugin-selection';
|
|
|
+import { Snapline } from '@antv/x6-plugin-snapline';
|
|
|
+import { Keyboard } from '@antv/x6-plugin-keyboard';
|
|
|
+import { Clipboard } from '@antv/x6-plugin-clipboard';
|
|
|
+import { Stencil } from '@antv/x6-plugin-stencil';
|
|
|
+import { Export } from '@antv/x6-plugin-export';
|
|
|
+
|
|
|
+const stencilContainer = ref();
|
|
|
+const graphContainer = ref();
|
|
|
+
|
|
|
+let graph: any = null;
|
|
|
+
|
|
|
+const state = reactive({
|
|
|
+ data: {
|
|
|
+ nodes: [
|
|
|
+ {
|
|
|
+ id: 'ac51fb2f-2753-4852-8239-53672a29bb14',
|
|
|
+ position: {
|
|
|
+ x: 160,
|
|
|
+ y: 250
|
|
|
+ },
|
|
|
+ data: {
|
|
|
+ name: '烟感系统'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: '81004c2f-0413-4cc6-8622-127004b3befa',
|
|
|
+ position: {
|
|
|
+ x: 220,
|
|
|
+ y: 110
|
|
|
+ },
|
|
|
+ data: {
|
|
|
+ name: '烟感系统'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'f2db8c64-6ffe-4c9b-83a2-037c9154e599',
|
|
|
+ position: {
|
|
|
+ x: 50,
|
|
|
+ y: 120
|
|
|
+ },
|
|
|
+ data: {
|
|
|
+ name: '摄像头'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+});
|
|
|
+const imageShapes = [
|
|
|
+ {
|
|
|
+ label: '摄像头',
|
|
|
+ image: new URL('@/assets/images/camera.svg', import.meta.url).href
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '风机',
|
|
|
+ image: new URL('@/assets/images/fan.svg', import.meta.url).href
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '照明设施',
|
|
|
+ image: new URL('@/assets/images/lighting.svg', import.meta.url).href
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '烟感系统',
|
|
|
+ image: new URL('@/assets/images/smoke.svg', import.meta.url).href
|
|
|
+ }
|
|
|
+];
|
|
|
+const getNodeAttrs = (label) => {
|
|
|
+ const [{ image }] = imageShapes.filter((item) => item.label === label);
|
|
|
+ return {
|
|
|
+ image: {
|
|
|
+ 'xlink:href': image
|
|
|
+ }
|
|
|
+ };
|
|
|
+};
|
|
|
+const init = () => {
|
|
|
+ graph = new Graph({
|
|
|
+ container: graphContainer.value,
|
|
|
+ grid: true,
|
|
|
+ mousewheel: {
|
|
|
+ enabled: true,
|
|
|
+ zoomAtMousePosition: true,
|
|
|
+ modifiers: 'ctrl',
|
|
|
+ minScale: 0.5,
|
|
|
+ maxScale: 3
|
|
|
+ },
|
|
|
+ highlighting: {
|
|
|
+ magnetAdsorbed: {
|
|
|
+ name: 'stroke',
|
|
|
+ args: {
|
|
|
+ attrs: {
|
|
|
+ fill: '#fff',
|
|
|
+ stroke: '#31d0c6',
|
|
|
+ strokeWidth: 4
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ // graph.centerContent();
|
|
|
+
|
|
|
+ // #region 使用插件
|
|
|
+ graph
|
|
|
+ .use(
|
|
|
+ new Transform({
|
|
|
+ resizing: true,
|
|
|
+ rotating: true
|
|
|
+ })
|
|
|
+ )
|
|
|
+ .use(
|
|
|
+ new Selection({
|
|
|
+ rubberband: true,
|
|
|
+ showNodeSelectionBox: true
|
|
|
+ })
|
|
|
+ )
|
|
|
+ .use(new Snapline())
|
|
|
+ .use(new Keyboard())
|
|
|
+ .use(new Clipboard())
|
|
|
+ .use(new Export());
|
|
|
+ Graph.registerNode(
|
|
|
+ 'custom-image',
|
|
|
+ {
|
|
|
+ inherit: 'rect',
|
|
|
+ width: 52,
|
|
|
+ height: 52,
|
|
|
+ markup: [
|
|
|
+ {
|
|
|
+ tagName: 'rect',
|
|
|
+ selector: 'body'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ tagName: 'image'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ tagName: 'text',
|
|
|
+ selector: 'label'
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ attrs: {
|
|
|
+ body: {
|
|
|
+ stroke: '#5F95FF',
|
|
|
+ fill: 'transparent',
|
|
|
+ strokeWidth: 0
|
|
|
+ },
|
|
|
+ image: {
|
|
|
+ width: 45,
|
|
|
+ height: 45,
|
|
|
+ refX: 5,
|
|
|
+ refY: 5
|
|
|
+ },
|
|
|
+ label: {
|
|
|
+ refX: 3,
|
|
|
+ refY: 2,
|
|
|
+ textAnchor: 'left',
|
|
|
+ textVerticalAnchor: 'top',
|
|
|
+ fontSize: 12,
|
|
|
+ fill: 'transparent'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ true
|
|
|
+ );
|
|
|
+ const stencil = new Stencil({
|
|
|
+ title: '道路元器',
|
|
|
+ target: graph,
|
|
|
+ stencilGraphWidth: 200,
|
|
|
+ stencilGraphHeight: 180,
|
|
|
+ collapsable: true,
|
|
|
+ groups: [
|
|
|
+ {
|
|
|
+ title: '基础元器',
|
|
|
+ name: 'processLibrary',
|
|
|
+ graphHeight: 250,
|
|
|
+ layoutOptions: {
|
|
|
+ rowHeight: 70
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ layoutOptions: {
|
|
|
+ columns: 2,
|
|
|
+ columnWidth: 80,
|
|
|
+ rowHeight: 55
|
|
|
+ }
|
|
|
+ });
|
|
|
+ stencilContainer.value.appendChild(stencil.container);
|
|
|
+ graph.drawBackground({
|
|
|
+ color: '#ccc',
|
|
|
+ position: 'center',
|
|
|
+ size: {
|
|
|
+ width: '100%'
|
|
|
+ // height: 100
|
|
|
+ },
|
|
|
+ image: new URL('@/assets/images/road.png', import.meta.url).href // 设置背景图片
|
|
|
+ });
|
|
|
+ // #region 快捷键与事件
|
|
|
+ graph.bindKey(['meta+c', 'ctrl+c'], () => {
|
|
|
+ copy();
|
|
|
+ });
|
|
|
+ graph.bindKey(['meta+x', 'ctrl+x'], () => {
|
|
|
+ const cells = graph.getSelectedCells();
|
|
|
+ if (cells.length) {
|
|
|
+ graph.cut(cells);
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ });
|
|
|
+ graph.bindKey(['meta+v', 'ctrl+v'], () => {
|
|
|
+ paste();
|
|
|
+ });
|
|
|
+ // select all
|
|
|
+ graph.bindKey(['meta+a', 'ctrl+a'], () => {
|
|
|
+ const nodes = graph.getNodes();
|
|
|
+ if (nodes) {
|
|
|
+ graph.select(nodes);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // delete
|
|
|
+ graph.bindKey('backspace', () => {
|
|
|
+ del();
|
|
|
+ });
|
|
|
+
|
|
|
+ // zoom
|
|
|
+ graph.bindKey(['ctrl+1', 'meta+1'], () => {
|
|
|
+ const zoom = graph.zoom();
|
|
|
+ if (zoom < 1.5) {
|
|
|
+ graph.zoom(0.1);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ graph.bindKey(['ctrl+2', 'meta+2'], () => {
|
|
|
+ const zoom = graph.zoom();
|
|
|
+ if (zoom > 0.5) {
|
|
|
+ graph.zoom(-0.1);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ //节点被取消选中时触发。
|
|
|
+ graph.on('node:unselected', (args: { cell: Cell; node: Node; options: Model.SetOptions }) => {
|
|
|
+ args.node.removeTools();
|
|
|
+ });
|
|
|
+
|
|
|
+ //边选中事件
|
|
|
+ graph.on('edge:selected', (args: { cell: Cell; edge: Edge; options: Model.SetOptions }) => {
|
|
|
+ args.edge.attr('line/strokeWidth', 3);
|
|
|
+ });
|
|
|
+
|
|
|
+ //边被取消选中时触发。
|
|
|
+ graph.on('edge:unselected', (args: { cell: Cell; edge: Edge; options: Model.SetOptions }) => {
|
|
|
+ args.edge.attr('line/strokeWidth', 1);
|
|
|
+ });
|
|
|
+
|
|
|
+ const nodes = imageShapes.map((item) => {
|
|
|
+ const node = {
|
|
|
+ shape: 'custom-image',
|
|
|
+ label: item.label,
|
|
|
+ attrs: getNodeAttrs(item.label)
|
|
|
+ };
|
|
|
+ const newNode = graph.addNode(node);
|
|
|
+ return newNode;
|
|
|
+ });
|
|
|
+ stencil.load(nodes, 'processLibrary');
|
|
|
+};
|
|
|
+
|
|
|
+//保存
|
|
|
+function save() {
|
|
|
+ console.log('save');
|
|
|
+ const graphData = graph.toJSON();
|
|
|
+ console.log(graphData);
|
|
|
+}
|
|
|
+//复制
|
|
|
+function copy() {
|
|
|
+ const cells = graph.getSelectedCells();
|
|
|
+ if (cells.length) {
|
|
|
+ graph.copy(cells);
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+}
|
|
|
+//粘贴
|
|
|
+function paste() {
|
|
|
+ if (!graph.isClipboardEmpty()) {
|
|
|
+ const cells = graph.paste({ offset: 32 });
|
|
|
+ graph.cleanSelection();
|
|
|
+ graph.select(cells);
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+}
|
|
|
+//删除
|
|
|
+function del() {
|
|
|
+ const cells = graph.getSelectedCells();
|
|
|
+ if (cells.length) {
|
|
|
+ graph.removeCells(cells);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+//导出PNG
|
|
|
+function exportPng() {
|
|
|
+ graph.toPNG(
|
|
|
+ (dataUri: string) => {
|
|
|
+ // 下载
|
|
|
+ DataUri.downloadDataUri(dataUri, 'chart.png');
|
|
|
+ },
|
|
|
+ {
|
|
|
+ padding: {
|
|
|
+ top: 20,
|
|
|
+ right: 20,
|
|
|
+ bottom: 20,
|
|
|
+ left: 20
|
|
|
+ }
|
|
|
+ }
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+//加载初始节点
|
|
|
+function getData() {
|
|
|
+ let cells = [] as any;
|
|
|
+ const location = state.data;
|
|
|
+ location.nodes.map((node) => {
|
|
|
+ cells.push(
|
|
|
+ graph.addNode({
|
|
|
+ id: node.id,
|
|
|
+ x: node.position.x,
|
|
|
+ y: node.position.y,
|
|
|
+ shape: 'custom-image',
|
|
|
+ attrs: getNodeAttrs(node.data.name),
|
|
|
+ label: node.data.name,
|
|
|
+ data: node.data
|
|
|
+ })
|
|
|
+ );
|
|
|
+ });
|
|
|
+ graph.resetCells(cells);
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ init();
|
|
|
+ getData();
|
|
|
+});
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ graph.dispose();
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.content-main {
|
|
|
+ display: flex;
|
|
|
+ width: 100%;
|
|
|
+ flex-direction: column;
|
|
|
+ height: calc(100vh - 85px - 40px);
|
|
|
+ background-color: #ffffff;
|
|
|
+ position: relative;
|
|
|
+
|
|
|
+ .tool-container {
|
|
|
+ padding: 8px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ color: rgba(0, 0, 0, 0.45);
|
|
|
+ }
|
|
|
+}
|
|
|
+.content-container {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ .content {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ position: relative;
|
|
|
+ min-width: 400px;
|
|
|
+ min-height: 600px;
|
|
|
+ display: flex;
|
|
|
+ border: 1px solid #dfe3e8;
|
|
|
+ flex-direction: row;
|
|
|
+ flex: 1;
|
|
|
+ .stencil {
|
|
|
+ width: 200px;
|
|
|
+ height: 100%;
|
|
|
+ border-right: 1px solid #dfe3e8;
|
|
|
+ position: relative;
|
|
|
+
|
|
|
+ :deep(.x6-widget-stencil) {
|
|
|
+ background-color: #fff;
|
|
|
+ }
|
|
|
+ :deep(.x6-widget-stencil-title) {
|
|
|
+ background-color: #fff;
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+ :deep(.x6-widget-stencil-content) {
|
|
|
+ top: 0;
|
|
|
+ }
|
|
|
+ :deep(.x6-widget-stencil-group-title) {
|
|
|
+ background-color: #fff !important;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .graph-content {
|
|
|
+ width: calc(100% - 180px);
|
|
|
+ height: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .editor-sidebar {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ border-left: 1px solid #e6f7ff;
|
|
|
+ background: #fafafa;
|
|
|
+ z-index: 9;
|
|
|
+
|
|
|
+ .el-card {
|
|
|
+ border: none;
|
|
|
+ }
|
|
|
+ .edit-panel {
|
|
|
+ flex: 1 1;
|
|
|
+ background-color: #fff;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.x6-widget-minimap-viewport) {
|
|
|
+ border: 1px solid #8f8f8f;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.x6-widget-minimap-viewport-zoom) {
|
|
|
+ border: 1px solid #8f8f8f;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.x6-widget-transform) {
|
|
|
+ margin: -1px 0 0 -1px;
|
|
|
+ padding: 0px;
|
|
|
+ border: 1px solid #239edd;
|
|
|
+}
|
|
|
+:deep(.x6-widget-transform > div) {
|
|
|
+ border: 1px solid #239edd;
|
|
|
+}
|
|
|
+:deep(.x6-widget-transform > div:hover) {
|
|
|
+ background-color: #3dafe4;
|
|
|
+}
|
|
|
+:deep(.x6-widget-transform-active-handle) {
|
|
|
+ background-color: #3dafe4;
|
|
|
+}
|
|
|
+:deep(.x6-widget-transform-resize) {
|
|
|
+ border-radius: 0;
|
|
|
+}
|
|
|
+:deep(.x6-widget-selection-inner) {
|
|
|
+ border: 1px solid #239edd;
|
|
|
+}
|
|
|
+:deep(.x6-widget-selection-box) {
|
|
|
+ opacity: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.topic-image {
|
|
|
+ visibility: hidden;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+.x6-node:hover .topic-image {
|
|
|
+ visibility: visible;
|
|
|
+}
|
|
|
+.x6-node-selected rect {
|
|
|
+ stroke-width: 2px;
|
|
|
+}
|
|
|
+.roadback {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 0;
|
|
|
+ left: 200px;
|
|
|
+}
|
|
|
+</style>
|