|
@@ -0,0 +1,560 @@
|
|
|
+<template>
|
|
|
+ <div class="system-graph">
|
|
|
+ <div id="container" ref="container"></div>
|
|
|
+ <!-- <div class="control-panel">
|
|
|
+ <button @click="handleMatch">匹配模式</button>
|
|
|
+ </div> -->
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { onMounted, ref, onUnmounted } from 'vue';
|
|
|
+import { Graph } from '@antv/g6';
|
|
|
+import { GADDI } from '@antv/algorithm';
|
|
|
+import { color } from 'echarts';
|
|
|
+
|
|
|
+const container = ref(null);
|
|
|
+let graph = null;
|
|
|
+
|
|
|
+// 系统集成数据
|
|
|
+const systemData = {
|
|
|
+ nodes: [
|
|
|
+ {
|
|
|
+ id: 'ERP',
|
|
|
+ label: 'ERP',
|
|
|
+ style: {
|
|
|
+ x: 167.86420885505296,
|
|
|
+ y: 94.28884847734246,
|
|
|
+ size: 35,
|
|
|
+ },
|
|
|
+ data: {
|
|
|
+ cluster: 'nodeType-1'
|
|
|
+ },
|
|
|
+ labelCfg: {
|
|
|
+ style: {
|
|
|
+ fill: '#fff',
|
|
|
+ fontSize: 14,
|
|
|
+ }
|
|
|
+ },
|
|
|
+ cluster: 'nodeType-1',
|
|
|
+ description: '企业资源计划系统'
|
|
|
+ },
|
|
|
+ // ERP子模块
|
|
|
+ {
|
|
|
+ id: 'ERP-Finance',
|
|
|
+ label: 'Finance',
|
|
|
+ style: {
|
|
|
+ x: 127.86420885505296,
|
|
|
+ y: 134.28884847734246,
|
|
|
+ size: 27,
|
|
|
+ labelFontSize: 8,
|
|
|
+ },
|
|
|
+ data: {
|
|
|
+ cluster: 'nodeType-1'
|
|
|
+ },
|
|
|
+ cluster: 'nodeType-1',
|
|
|
+ parentId: 'ERP'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'ERP-HR',
|
|
|
+ label: 'HR',
|
|
|
+ style: {
|
|
|
+ x: 147.86420885505296,
|
|
|
+ y: 154.28884847734246,
|
|
|
+ size: 27,
|
|
|
+ labelFontSize: 8,
|
|
|
+ },
|
|
|
+ data: {
|
|
|
+ cluster: 'nodeType-1'
|
|
|
+ },
|
|
|
+ cluster: 'nodeType-1',
|
|
|
+ parentId: 'ERP'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'ERP-Purchase',
|
|
|
+ label: 'Purchase',
|
|
|
+ style: {
|
|
|
+ x: 187.86420885505296,
|
|
|
+ y: 154.28884847734246,
|
|
|
+ size: 27,
|
|
|
+ },
|
|
|
+ data: {
|
|
|
+ cluster: 'nodeType-1'
|
|
|
+ },
|
|
|
+ cluster: 'nodeType-1',
|
|
|
+ parentId: 'ERP'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'ERP-Sales',
|
|
|
+ label: 'Sales',
|
|
|
+ style: {
|
|
|
+ x: 207.86420885505296,
|
|
|
+ y: 134.28884847734246,
|
|
|
+ size: 27,
|
|
|
+ },
|
|
|
+ data: {
|
|
|
+ cluster: 'nodeType-1'
|
|
|
+ },
|
|
|
+ cluster: 'nodeType-1',
|
|
|
+ parentId: 'ERP'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'ERP-Inventory',
|
|
|
+ label: 'Inventory',
|
|
|
+ style: {
|
|
|
+ x: 167.86420885505296,
|
|
|
+ y: 174.28884847734246,
|
|
|
+ size: 27,
|
|
|
+ labelFontSize: 5,
|
|
|
+ },
|
|
|
+ data: {
|
|
|
+ cluster: 'nodeType-1'
|
|
|
+ },
|
|
|
+ cluster: 'nodeType-1',
|
|
|
+ parentId: 'ERP'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'MES',
|
|
|
+ label: 'MES',
|
|
|
+ style: {
|
|
|
+ x: 113.58792632732174,
|
|
|
+ y: 25.785315472468127
|
|
|
+ },
|
|
|
+ data: {
|
|
|
+ cluster: 'nodeType-1'
|
|
|
+ },
|
|
|
+ cluster: 'nodeType-1',
|
|
|
+ description: '制造执行系统'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'PLM',
|
|
|
+ label: 'PLM',
|
|
|
+ style: {
|
|
|
+ x: 179.59682070334452,
|
|
|
+ y: 38.87850516662148
|
|
|
+ },
|
|
|
+ data: {
|
|
|
+ cluster: 'nodeType-2'
|
|
|
+ },
|
|
|
+ cluster: 'nodeType-2',
|
|
|
+ description: '产品生命周期管理'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'QMS',
|
|
|
+ label: 'QMS',
|
|
|
+ style: {
|
|
|
+ x: 204.20226244579672,
|
|
|
+ y: -5.33508012158744
|
|
|
+ },
|
|
|
+ data: {
|
|
|
+ cluster: 'nodeType-1'
|
|
|
+ },
|
|
|
+ cluster: 'nodeType-1',
|
|
|
+ description: '质量管理系统'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'PMS',
|
|
|
+ label: 'PMS',
|
|
|
+ style: {
|
|
|
+ x: 308.74171746938134,
|
|
|
+ y: 10.554714934145961
|
|
|
+ },
|
|
|
+ data: {
|
|
|
+ cluster: 'nodeType-1'
|
|
|
+ },
|
|
|
+ cluster: 'nodeType-1',
|
|
|
+ description: '项目管理系统'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'WMS',
|
|
|
+ label: 'WMS',
|
|
|
+ style: {
|
|
|
+ x: 341.99836557519745,
|
|
|
+ y: 48.30310308747067
|
|
|
+ },
|
|
|
+ data: {
|
|
|
+ cluster: 'nodeType-2'
|
|
|
+ },
|
|
|
+ cluster: 'nodeType-2',
|
|
|
+ description: '仓库管理系统'
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ edges: [
|
|
|
+ // ERP子模块连接
|
|
|
+ {
|
|
|
+ source: 'ERP',
|
|
|
+ target: 'ERP-Finance',
|
|
|
+ data: { cluster: 'submodule' },
|
|
|
+ cluster: 'submodule',
|
|
|
+ id: 'edge-erp-finance'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ source: 'ERP',
|
|
|
+ target: 'ERP-HR',
|
|
|
+ data: { cluster: 'submodule' },
|
|
|
+ cluster: 'submodule',
|
|
|
+ id: 'edge-erp-hr'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ source: 'ERP',
|
|
|
+ target: 'ERP-Purchase',
|
|
|
+ data: { cluster: 'submodule' },
|
|
|
+ cluster: 'submodule',
|
|
|
+ id: 'edge-erp-purchase'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ source: 'ERP',
|
|
|
+ target: 'ERP-Sales',
|
|
|
+ data: { cluster: 'submodule' },
|
|
|
+ cluster: 'submodule',
|
|
|
+ id: 'edge-erp-sales'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ source: 'ERP',
|
|
|
+ target: 'ERP-Inventory',
|
|
|
+ data: { cluster: 'submodule' },
|
|
|
+ cluster: 'submodule',
|
|
|
+ id: 'edge-erp-inventory'
|
|
|
+ },
|
|
|
+ // 系统间连接
|
|
|
+ {
|
|
|
+ source: 'ERP',
|
|
|
+ target: 'MES',
|
|
|
+ data: { cluster: 'edgeType-0' },
|
|
|
+ cluster: 'edgeType-0',
|
|
|
+ id: 'edge-erp-mes'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ source: 'ERP',
|
|
|
+ target: 'PLM',
|
|
|
+ data: { cluster: 'edgeType-1' },
|
|
|
+ cluster: 'edgeType-1',
|
|
|
+ id: 'edge-erp-plm'
|
|
|
+ },
|
|
|
+
|
|
|
+ {
|
|
|
+ source: 'MES',
|
|
|
+ target: 'QMS',
|
|
|
+ data: { cluster: 'edgeType-2' },
|
|
|
+ cluster: 'edgeType-2',
|
|
|
+ id: 'edge-mes-qms'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ source: 'PLM',
|
|
|
+ target: 'PMS',
|
|
|
+ data: { cluster: 'edgeType-0' },
|
|
|
+ cluster: 'edgeType-0',
|
|
|
+ id: 'edge-plm-pms'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ source: 'QMS',
|
|
|
+ target: 'WMS',
|
|
|
+ data: { cluster: 'edgeType-1' },
|
|
|
+ cluster: 'edgeType-1',
|
|
|
+ id: 'edge-qms-wms'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ source: 'PMS',
|
|
|
+ target: 'WMS',
|
|
|
+ data: { cluster: 'edgeType-2' },
|
|
|
+ cluster: 'edgeType-2',
|
|
|
+ id: 'edge-pms-wms'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ source: 'PLM',
|
|
|
+ target: 'QMS',
|
|
|
+ data: { cluster: 'edgeType-21' },
|
|
|
+ cluster: 'edgeType-21',
|
|
|
+ id: 'edge-pms-wms1'
|
|
|
+ }
|
|
|
+ ]
|
|
|
+};
|
|
|
+
|
|
|
+const graphData = ref(systemData);
|
|
|
+
|
|
|
+// 处理窗口大小变化
|
|
|
+const handleResize = () => {
|
|
|
+ if (!graph || graph.get('destroyed')) return;
|
|
|
+ const width = 900;
|
|
|
+ const height = 800 || 500;
|
|
|
+ graph.changeSize(width, height);
|
|
|
+};
|
|
|
+
|
|
|
+const getGraphOptions = () => {
|
|
|
+ const width = 900;
|
|
|
+ const height = 800 || 600;
|
|
|
+
|
|
|
+ return {
|
|
|
+ width,
|
|
|
+ height,
|
|
|
+ autoFit: 'view',
|
|
|
+ modes: {
|
|
|
+ default: ['drag-canvas', 'zoom-canvas', 'drag-node', 'click-select'],
|
|
|
+ },
|
|
|
+ defaultNode: {
|
|
|
+ draggable: true,
|
|
|
+ labelCfg: {
|
|
|
+ style: {
|
|
|
+ fontSize: 16,
|
|
|
+ fill: '#000',
|
|
|
+ }
|
|
|
+ },
|
|
|
+ },
|
|
|
+ node: {
|
|
|
+ style: (node) => {
|
|
|
+ const isSubmodule = node.cluster === 'submodule';
|
|
|
+ return {
|
|
|
+ labelPlacement: isSubmodule ? 'bottom' : 'center',
|
|
|
+ labelText: (d) => d.label,
|
|
|
+ stroke: isSubmodule ? '#1890ff' : '#5F95FF',
|
|
|
+ lineWidth: isSubmodule ? 1 : 2,
|
|
|
+ fill: '#fff',
|
|
|
+ shadowColor: isSubmodule ? '#e8e8e8' : '#d9d9d9',
|
|
|
+ shadowBlur: isSubmodule ? 5 : 10,
|
|
|
+ radius: isSubmodule ? 15 : 30,
|
|
|
+ fontSize: isSubmodule ? 12 : 16,
|
|
|
+ fontWeight: isSubmodule ? 400 : 500,
|
|
|
+ opacity: isSubmodule ? 0.8 : 1,
|
|
|
+ };
|
|
|
+ },
|
|
|
+ palette: {
|
|
|
+ type: 'group',
|
|
|
+ field: 'cluster',
|
|
|
+ color: {
|
|
|
+ 'nodeType-0': '#1890ff',
|
|
|
+ 'nodeType-1': '#52c41a',
|
|
|
+ 'nodeType-2': '#722ed1',
|
|
|
+ 'submodule': '#40a9ff'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ state: {
|
|
|
+ hover: {
|
|
|
+ stroke: '#40a9ff',
|
|
|
+ lineWidth: (node) => node.cluster === 'submodule' ? 2 : 3,
|
|
|
+ shadowColor: '#1890ff',
|
|
|
+ shadowBlur: (node) => node.cluster === 'submodule' ? 8 : 15,
|
|
|
+ opacity: 1,
|
|
|
+ },
|
|
|
+ selected: {
|
|
|
+ stroke: '#1890ff',
|
|
|
+ lineWidth: (node) => node.cluster === 'submodule' ? 2 : 3,
|
|
|
+ shadowColor: '#1890ff',
|
|
|
+ shadowBlur: (node) => node.cluster === 'submodule' ? 8 : 15,
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ edge: {
|
|
|
+ style: (edge) => {
|
|
|
+ const isSubmodule = edge.cluster === 'submodule';
|
|
|
+ return {
|
|
|
+ stroke: isSubmodule ? '#40a9ff' : '#91d5ff',
|
|
|
+ lineWidth: isSubmodule ? 1 : 2,
|
|
|
+ endArrow: isSubmodule ? false : {
|
|
|
+ path: 'M 0,0 L 12,6 L 12,-6 Z',
|
|
|
+ fill: '#91d5ff',
|
|
|
+ },
|
|
|
+ radius: isSubmodule ? 10 : 20,
|
|
|
+ opacity: isSubmodule ? 0.5 : 0.8,
|
|
|
+ lineDash: isSubmodule ? [2, 2] : null,
|
|
|
+ };
|
|
|
+ },
|
|
|
+ palette: {
|
|
|
+ type: 'group',
|
|
|
+ field: 'cluster',
|
|
|
+ color: {
|
|
|
+ 'edgeType-0': '#1890ff',
|
|
|
+ 'edgeType-1': '#52c41a',
|
|
|
+ 'edgeType-2': '#722ed1',
|
|
|
+ 'submodule': '#40a9ff'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ state: {
|
|
|
+ hover: {
|
|
|
+ stroke: '#1890ff',
|
|
|
+ lineWidth: (edge) => edge.cluster === 'submodule' ? 1.5 : 3,
|
|
|
+ opacity: 1,
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ plugins: [
|
|
|
+ {
|
|
|
+ type: 'legend',
|
|
|
+ nodeField: 'cluster',
|
|
|
+ position: 'bottom',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'hull-0',
|
|
|
+ type: 'hull',
|
|
|
+ members: [],
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'hull-1',
|
|
|
+ type: 'hull',
|
|
|
+ members: [],
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ };
|
|
|
+};
|
|
|
+
|
|
|
+const handleMatch = () => {
|
|
|
+ if (!graphData.value || !graph) return;
|
|
|
+
|
|
|
+ const matches = GADDI(graphData.value, pattern, true, undefined, undefined, 'cluster', 'cluster');
|
|
|
+ matches.forEach((match, i) => {
|
|
|
+ graph.updatePlugin({
|
|
|
+ key: `hull-${i}`,
|
|
|
+ members: match.nodes.map((node) => node.id),
|
|
|
+ });
|
|
|
+ });
|
|
|
+ graph.render();
|
|
|
+};
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ try {
|
|
|
+ // 初始化图形
|
|
|
+ graph = new Graph({
|
|
|
+ ...getGraphOptions(),
|
|
|
+ container: container.value,
|
|
|
+ data: graphData.value,
|
|
|
+ autoFit: 'view',
|
|
|
+ behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],
|
|
|
+ layout: {
|
|
|
+ type: 'gForce',
|
|
|
+ preventOverlap: true,
|
|
|
+ nodeStrength: -100,
|
|
|
+ edgeStrength: 0.1,
|
|
|
+ nodeSize: 60,
|
|
|
+ nodeSpacing: 100,
|
|
|
+ linkDistance: 200,
|
|
|
+ gravity: 1,
|
|
|
+ maxIteration: 1000,
|
|
|
+ minMovement: 0.1,
|
|
|
+ damping: 0.9,
|
|
|
+ maxSpeed: 1000,
|
|
|
+ center: [400, 300]
|
|
|
+ },
|
|
|
+ node: {
|
|
|
+ style: {
|
|
|
+ labelPlacement: 'center',
|
|
|
+ labelText: (d) => d.label,
|
|
|
+ stroke: '#61DDAA',
|
|
|
+ lineWidth: 1,
|
|
|
+ labelFontSize: 6,
|
|
|
+ labelFill: '#fff',
|
|
|
+ },
|
|
|
+ labelCfg: {
|
|
|
+ style: {
|
|
|
+ fontSize: 16,
|
|
|
+ fill: '#000',
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ palette: {
|
|
|
+ type: 'group',
|
|
|
+ field: 'cluster',
|
|
|
+ color: ['#61DDAA', '#65789B'],
|
|
|
+ },
|
|
|
+ },
|
|
|
+ edge: {
|
|
|
+ style: {
|
|
|
+ endArrow: true,
|
|
|
+ },
|
|
|
+ palette: {
|
|
|
+ type: 'group',
|
|
|
+ field: 'cluster',
|
|
|
+ color: ['#61DDAA', '#65789B'],
|
|
|
+ },
|
|
|
+ },
|
|
|
+ plugins: [
|
|
|
+ {
|
|
|
+ type: 'legend',
|
|
|
+ nodeField: 'cluster',
|
|
|
+ position: 'bottom',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'hull-0',
|
|
|
+ type: 'hull',
|
|
|
+ members: [],
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'hull-1',
|
|
|
+ type: 'hull',
|
|
|
+ members: [],
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ });
|
|
|
+
|
|
|
+ graph.render();
|
|
|
+
|
|
|
+ // 添加事件监听
|
|
|
+ window.addEventListener('resize', handleResize);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Failed to fetch graph data:', error);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加节点点击事件
|
|
|
+ graph.on('node:click', (evt) => {
|
|
|
+ const { item } = evt;
|
|
|
+ const model = item.getModel();
|
|
|
+ console.log('Clicked node:', model);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 监听窗口大小变化
|
|
|
+ window.addEventListener('resize', handleResize);
|
|
|
+});
|
|
|
+
|
|
|
+// 组件卸载时清理
|
|
|
+onUnmounted(() => {
|
|
|
+ window.removeEventListener('resize', handleResize);
|
|
|
+ if (graph) {
|
|
|
+ graph.destroy();
|
|
|
+ }
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.system-graph {
|
|
|
+ width: 100%;
|
|
|
+ height: 100vh;
|
|
|
+ background: #fff;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+#container {
|
|
|
+ width: 900px;
|
|
|
+ height: 100vh;
|
|
|
+ border: 1px solid #eee;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.control-panel {
|
|
|
+ width: 100%;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.control-panel button {
|
|
|
+ padding: 8px 16px;
|
|
|
+ background: #1890ff;
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+ border-radius: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 14px;
|
|
|
+ transition: all 0.3s;
|
|
|
+}
|
|
|
+
|
|
|
+.control-panel button:hover {
|
|
|
+ background: #40a9ff;
|
|
|
+}
|
|
|
+
|
|
|
+.control-panel button:active {
|
|
|
+ background: #096dd9;
|
|
|
+}
|
|
|
+</style>
|