├── .github └── workflows │ └── docs.yaml ├── .gitignore ├── .vscode └── extensions.json ├── README.md ├── index.html ├── package.json ├── pnpm-lock.yaml ├── public ├── images │ ├── beautify.svg │ ├── delay.svg │ └── grid.svg └── vite.svg ├── src ├── App.vue ├── components │ ├── BeautifyFlow.vue │ ├── FlowHeader.vue │ ├── LayoutFlow.vue │ ├── MindmapFlow.vue │ ├── beautifyElement │ │ ├── index.ts │ │ ├── nodes │ │ │ ├── BeautifyLine.ts │ │ │ └── BeautifyNode.ts │ │ ├── style.css │ │ └── tools │ │ │ ├── Palette.vue │ │ │ └── dagre.ts │ ├── mindElement │ │ ├── index.ts │ │ ├── nodes │ │ │ ├── CenterNode.ts │ │ │ ├── CenterNode.vue │ │ │ ├── MindmapEdge.ts │ │ │ ├── SubNode.ts │ │ │ └── SubNode.vue │ │ ├── style.css │ │ └── tools │ │ │ ├── layout.ts │ │ │ ├── menu.ts │ │ │ └── menu.vue │ └── util.ts ├── main.ts ├── style.css └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.github/workflows/docs.yaml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: [push] 4 | 5 | jobs: 6 | deploy: 7 | runs-on: ubuntu-latest 8 | if: github.ref == 'refs/heads/master' 9 | steps: 10 | - uses: actions/checkout@v3 11 | 12 | - name: Use node v16 13 | uses: actions/setup-node@v1 14 | with: 15 | node-version: '16.16.0' 16 | 17 | - name: load dependency 18 | run: | 19 | npm install 20 | 21 | - name: generate static file 22 | run: | 23 | npm run build 24 | 25 | - name: deploy to github pages 26 | uses: peaceiris/actions-gh-pages@v3 27 | with: 28 | force_orphan: true 29 | github_token: ${{ secrets.GITHUB_TOKEN }} 30 | publish_dir: ./dist -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 背景 4 | 5 | 随着项目中流程图被运营同学画的越来越复杂,出现了一个流程图上存在几百个节点的情况,流程图带来的**业务逻辑可视化效果越来越不明显**。虽然我们提供了分组、注释、记号等修饰性功能去支持标识流程中重要的业务关键点,但是效果仍然不明显。这个时候想再去通过人为调整去让混乱的流程图变得清晰,**比我们代码重构还难**。所以为了解决这个问题,我们增加了自动布局和一键美化功能,通过实践发现这两种方案效果都不错,有自己独特的应用场景。 6 | 7 | # 自动布局 8 | 9 | 自动布局大家最常见到的效果应该是脑图。画图的人不需要手动调整节点的位置,由系统自动计算出节点合适的位置。 10 | 11 | ![autolayout.gif](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8d5d9d05ebd54eb5b1e47aa86d2c2434~tplv-k3u1fbpfcp-watermark.image?) 12 | 13 | ## 自动布局的优缺点 14 | 15 | **在实际项目中,并不是所有的流程图都可以用自动布局,需要基于具体的产品来结合自动布局的优缺点来权衡。** 16 | 17 | 自动布局优点: 18 | 1. 操作便捷,省去了传统绘制流程图的位置调整的操作成本。 19 | 1. 流程图风格统一,整个流程图美观度下限更高。 20 | 21 | 自动布局的不足: 22 | 1. 首先能用的业务场景有限,一般用于树型结构(如脑图)或者布局有规律的场景。 23 | 1. 画布空间利用率不高,系统引导的节点位置在很多情况下不是最优的,但是因为自动布局不允许调整节点位置,所以在节点较多的情况下,整个画布空白的地方比较多。 24 | 25 | ## 实现自动布局 26 | 27 | 对于布局目前社区有很多方案,在很多数据可视化库中都有使用。我们在流程图这边最常用一般都是结构化布局**hierarchy**。在结构化布局里面,又有基于不同的情况,使用不同的布局方案,例如**tree、dagre、elk**等。关于图布局的详细介绍,大家可以看这篇文章:, 这里就不详细介绍了。 28 | 29 | 在我们需要用到**自动布局的业务场景**中,比较适合的是**树布局**。我们就直接使用了 。用它的原因主要是API使用起来比较方便(API上比会更友好一点),还能比较好的处理我们各节点大小不一致等情况,也能很好的和我们项目中的流程图框架[LogicFlow](https://github.com/didi/LogicFlow)结合起来。使用步骤如下: 30 | 31 | **步骤1:将LogicFlow的图数据转换为树结构** 32 | 33 | 34 | ```js 35 | export const graphToTree = (graphData) => { 36 | let tree = null; 37 | const nodesMap = new Map(); 38 | graphData.nodes.forEach(node => { 39 | const treeNode = { 40 | ...node, 41 | children: [], 42 | }; 43 | nodesMap.set(node.id, treeNode); 44 | if (node.type === ROOT_NODE) { 45 | tree = treeNode; 46 | } 47 | }); 48 | graphData.edges.forEach(edge => { 49 | const node = nodesMap.get(edge.sourceNodeId); 50 | node.children.push(nodesMap.get(edge.targetNodeId)); 51 | }); 52 | return tree; 53 | } 54 | const graphData = lf.getGraphData() 55 | const tree = graphToTree(graphData 56 | ``` 57 | 58 | **步骤二:调用Hierarchy对树进行布局,重新计算出所有节点的坐标** 59 | 60 | 61 | ```js 62 | import Hierarchy from '@antv/hierarchy'; 63 | 64 | const rootNode = Hierarchy.compactBox(tree, { 65 | direction: 'LR', // 从左到右布局 66 | getHeight(d) { // 可以细粒度处理节点高度 67 | if (d.type === ROOT_NODE) { 68 | return NODE_SIZE * 4; 69 | } 70 | return NODE_SIZE; 71 | }, 72 | getWidth() { // 可以细粒度处理节点宽度 73 | return 200 + PEM * 1.6; 74 | }, 75 | ... 76 | }); 77 | // 保证中心点位置不变,避免抖动 78 | const moveX = tree.x - rootNode.x; 79 | const moveY = tree.y - rootNode.y; 80 | const newTree = dfsTree(rootNode, currentNode => { 81 | return { 82 | id: currentNode.id, 83 | text: currentNode.data.text.value, 84 | properties: currentNode.data.properties, 85 | type: currentNode.data.type, 86 | x: currentNode.x + moveX, 87 | y: currentNode.y + moveY, 88 | } 89 | }); 90 | return newTre 91 | ``` 92 | **步骤三:将得到的新的树数据转回LogicFlow需要的图数据** 93 | 94 | ```js 95 | export const treeToGraph = (rootNode) => { 96 | const nodes = []; 97 | const edges = []; 98 | function getNode(current, parent = null) { 99 | const node = { 100 | ...current 101 | }; 102 | nodes.push(node); 103 | if (current.children) { 104 | current.children.forEach(subNode => { 105 | getNode(subNode, node); 106 | }); 107 | } 108 | if (parent) { 109 | const edge = { 110 | sourceNodeId: parent.id, 111 | targetNodeId: node.id, 112 | type: 'mindmap-edge', 113 | }; 114 | edges.push(edge); 115 | } 116 | } 117 | getNode(rootNode); 118 | return { 119 | nodes, 120 | edges, 121 | }; 122 | } 123 | ``` 124 | 125 | 最后,把图数据交给LogicFlow重新渲染就行。 126 | 127 | # 一键美化 128 | 129 | 一键美化在产品上是一个非常理想的功能,不论流程图多混乱,只需要我们点击一下一键美化就自动变成整齐美观的流程图。 130 | 131 | ![QQ20220920-165532-HD.gif](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8f29adb03ca441319b9d34342f3ea755~tplv-k3u1fbpfcp-watermark.image?) 132 | 133 | ## 实现一键美化 134 | 135 | 和上面的自动布局不同的是,在我们业务场景中,流程图会存在环这种结构。这个时候直接用树布局是不合适的,所以我们使用dagre布局。在网上找了一圈后,发现提供的dagre布局最好用(G6里面的dagre布局也是用的这个)。一键美化的实现思路和自动布局类似,都是把LogicFlow中的图数据传给布局库,然后再把得到的新的图数据重新使用LogicFlow渲染。这里为了方便,我把一键美化给封装成了一个LogicFlow插件,然后在里面做了一些符合LogicFlow特殊的处理。 136 | 137 | ```js 138 | import { DagreLayout, DagreLayoutOptions } from '@antv/layout'; 139 | export default class Dagre { 140 | static pluginName = 'dagre'; 141 | render(lf) { 142 | this.lf = lf; 143 | } 144 | layout(option = {}) { 145 | const { nodes, edges, gridSize } = this.lf.graphModel 146 | const layoutInstance = new DagreLayout(option); 147 | const layoutData = layoutInstance.layout({ 148 | nodes: nodes.map((node) => ({ 149 | id: node.id, 150 | size: { 151 | width: node.width, 152 | height: node.height, 153 | }, 154 | model: node, 155 | })), 156 | edges: edges.map((edge) => ({ 157 | source: edge.sourceNodeId, 158 | target: edge.targetNodeId, 159 | model: edge, 160 | })), 161 | }); 162 | const newGraphData = { 163 | nodes: [], 164 | edges: [], 165 | }; 166 | layoutData.nodes.forEach(node => { 167 | const { model } = node; 168 | const data = model.getData(); 169 | data.x = node.x; 170 | data.y = node.y; 171 | newGraphData.nodes.push(data); 172 | }); 173 | layoutData.edges.forEach(edge => { 174 | const { model } = edge; 175 | const data = model.getData(); 176 | data.pointsList = this.calcPointsList(model, newGraphData.nodes); 177 | newGraphData.edges.push(data); 178 | }); 179 | this.lf.render(newGraphData); 180 | } 181 | } 182 | ``` 183 | 184 | ## 一键美化的不足 185 | 186 | 在我们做出第一版一键美化后,发现最大的问题就是**一键美化后整个流程图布局全变了。** 例如当我们在一个比较规整的流程图上新增了一个节点,如果这个节点刚好把它所属的某条路径变成最长路径,就会触发布局算法中**最长路径作为主要路径的思路**,导致很多节点位置改变。但是因为画流程图的人基本上都会**选择将业务上有关联或者类似的节点放到同一块区域**,我们的美化会破坏这个逻辑,导致使用**体验十分不好**。 187 | 188 | ## 选区美化 189 | 190 | 由于一键美化这种纯系统布局的不够人性化,我们增加了一种**半系统半人工**的布局方式,也就是选区美化。选区美化就是整个流程图的整体布局仍然由画图的人来控制,我们提供一个工具,让画流程图的人选中部分区域内的节点,对这部分节点布局进行美化。这样虽然没有一键美化那样便捷,但是在实际体验发现更加**实用**。 191 | 192 | # 总结 193 | 194 | 不论任何对流程图的布局的美化效果,在研发的角度来说都是**对流程上的点、线坐标进行调整,** 但是最大的问题可能是也不知道**基于什么规则去调整**。如果产品已经给出来清晰的规则,事情往往比较简单,我们只需要按照这个规则实现自己的布局算法即可。可大多数情况下还是需要我们研发自己去调研,从已开源的项目里面找到**合适的布局算法,** 然后结合我们当前项目的流程图框架来实现 。 195 | 196 | 目前上有部分流程图框架已经自带了一些布局算法,但是在实际项目中,还有比较多的细节需要处理,例如让布局后的连线排布更加合理、某些节点位置保证相对不变等。我把在我们项目中实际用到的两种方式提取出布局部分的逻辑放到了github上,赶兴趣的同学可以拉下来看看。 197 | 198 | github地址: 199 | 200 | demo在线地址:https://hsole.github.io/layoutFlow 201 | 202 | 主要依赖开源项目: 203 | 204 | 205 | 206 | 207 | 208 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 自动布局和一键美化 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "layout-flow", 3 | "private": true, 4 | "version": "1.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vue-tsc --noEmit && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@antv/hierarchy": "^0.6.8", 13 | "@antv/layout": "^0.3.1", 14 | "@logicflow/core": "^1.1.27", 15 | "@logicflow/extension": "^1.1.28", 16 | "dagre": "^0.8.5", 17 | "element-plus": "^2.2.17", 18 | "vue": "^3.2.37" 19 | }, 20 | "devDependencies": { 21 | "@vitejs/plugin-vue": "^3.1.0", 22 | "typescript": "^4.6.4", 23 | "vite": "^3.1.0", 24 | "vue-tsc": "^0.40.4" 25 | } 26 | } -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: 5.4 2 | 3 | specifiers: 4 | '@antv/hierarchy': ^0.6.8 5 | '@antv/layout': ^0.3.1 6 | '@logicflow/core': ^1.1.27 7 | '@logicflow/extension': ^1.1.28 8 | '@vitejs/plugin-vue': ^3.1.0 9 | dagre: ^0.8.5 10 | element-plus: ^2.2.17 11 | typescript: ^4.6.4 12 | vite: ^3.1.0 13 | vue: ^3.2.37 14 | vue-tsc: ^0.40.4 15 | 16 | dependencies: 17 | '@antv/hierarchy': 0.6.8 18 | '@antv/layout': 0.3.1_dagre@0.8.5 19 | '@logicflow/core': 1.1.27 20 | '@logicflow/extension': 1.1.28 21 | dagre: 0.8.5 22 | element-plus: 2.2.17_vue@3.2.39 23 | vue: 3.2.39 24 | 25 | devDependencies: 26 | '@vitejs/plugin-vue': 3.1.0_vite@3.1.0+vue@3.2.39 27 | typescript: 4.8.3 28 | vite: 3.1.0 29 | vue-tsc: 0.40.13_typescript@4.8.3 30 | 31 | packages: 32 | 33 | /@antv/g-webgpu-core/0.5.6: 34 | resolution: {integrity: sha512-DPiH3GkAUiT0Q+LAKeImpI+IOQ/gP2w6HstYKivpFIpBPIvZ/9equM3icVrn1iDfDkZANVXQ1PppcO3xBv1ZTw==} 35 | dependencies: 36 | eventemitter3: 4.0.7 37 | gl-matrix: 3.4.3 38 | inversify: 5.1.1 39 | inversify-inject-decorators: 3.1.0 40 | probe.gl: 3.5.2 41 | reflect-metadata: 0.1.13 42 | dev: false 43 | 44 | /@antv/g-webgpu-engine/0.5.6: 45 | resolution: {integrity: sha512-D311qYUefdEFwLayutIHqucrAY3cAGH3BdnXS37nq+0nsglrHcNP0Ab1YTinn9RihLoY3yXFTLzrYkJHJbZXDg==} 46 | dependencies: 47 | '@antv/g-webgpu-core': 0.5.6 48 | '@webgpu/glslang': 0.0.15 49 | '@webgpu/types': 0.0.31 50 | gl-matrix: 3.4.3 51 | hammerjs: 2.0.8 52 | inversify: 5.1.1 53 | inversify-inject-decorators: 3.1.0 54 | probe.gl: 3.5.2 55 | reflect-metadata: 0.1.13 56 | regl: 1.7.0 57 | dev: false 58 | 59 | /@antv/g-webgpu/0.5.5: 60 | resolution: {integrity: sha512-TxtBniINFq1jFGEPo46xjJfrbJbUqkFd5wmsRs3tcg/7J7xoldOP1kEadpI3AJG9knMYdE92VpILw1VPd6DgzQ==} 61 | dependencies: 62 | '@antv/g-webgpu-core': 0.5.6 63 | '@antv/g-webgpu-engine': 0.5.6 64 | '@webgpu/types': 0.0.31 65 | gl-matrix: 3.4.3 66 | gl-vec2: 1.3.0 67 | hammerjs: 2.0.8 68 | inversify: 5.1.1 69 | inversify-inject-decorators: 3.1.0 70 | polyline-miter-util: 1.0.1 71 | polyline-normals: 2.0.2 72 | probe.gl: 3.5.2 73 | reflect-metadata: 0.1.13 74 | dev: false 75 | 76 | /@antv/graphlib/1.2.0: 77 | resolution: {integrity: sha512-hhJOMThec51nU4Fe5p/viLlNIL71uDEgYFzKPajWjr2715SFG1HAgiP6AVylIeqBcAZ04u3Lw7usjl/TuI5RuQ==} 78 | dev: false 79 | 80 | /@antv/hierarchy/0.6.8: 81 | resolution: {integrity: sha512-wVzUl+pxny5gyGJ2mkWx8IiEypX6bnMHgr/NILgbxY6shoy0Vf4FhZpI3CY8Ez7bQT6js8fMkB2NymPW7d7i8A==} 82 | dependencies: 83 | '@antv/util': 2.0.17 84 | dev: false 85 | 86 | /@antv/layout/0.3.1_dagre@0.8.5: 87 | resolution: {integrity: sha512-V7/Ys8IcctmGL/b1pYhA1ubSTuymQQcOkXOO3zpzQ/7PfvgW/v5QqcLEMy+roCz+Rr2Y5LmmBZXtgd7DxzpRAw==} 88 | dependencies: 89 | '@antv/g-webgpu': 0.5.5 90 | '@antv/graphlib': 1.2.0 91 | d3-force: 2.1.1 92 | d3-quadtree: 2.0.0 93 | dagre-compound: 0.0.11_dagre@0.8.5 94 | ml-matrix: 6.10.2 95 | transitivePeerDependencies: 96 | - dagre 97 | dev: false 98 | 99 | /@antv/util/2.0.17: 100 | resolution: {integrity: sha512-o6I9hi5CIUvLGDhth0RxNSFDRwXeywmt6ExR4+RmVAzIi48ps6HUy+svxOCayvrPBN37uE6TAc2KDofRo0nK9Q==} 101 | dependencies: 102 | csstype: 3.1.1 103 | tslib: 2.4.0 104 | dev: false 105 | 106 | /@babel/parser/7.19.1: 107 | resolution: {integrity: sha512-h7RCSorm1DdTVGJf3P2Mhj3kdnkmF/EiysUkzS2TdgAYqyjFdMQJbVuXOBej2SBJaXan/lIVtT6KkGbyyq753A==} 108 | engines: {node: '>=6.0.0'} 109 | hasBin: true 110 | 111 | /@babel/runtime/7.19.0: 112 | resolution: {integrity: sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==} 113 | engines: {node: '>=6.9.0'} 114 | dependencies: 115 | regenerator-runtime: 0.13.9 116 | dev: false 117 | 118 | /@ctrl/tinycolor/3.4.1: 119 | resolution: {integrity: sha512-ej5oVy6lykXsvieQtqZxCOaLT+xD4+QNarq78cIYISHmZXshCvROLudpQN3lfL8G0NL7plMSSK+zlyvCaIJ4Iw==} 120 | engines: {node: '>=10'} 121 | dev: false 122 | 123 | /@element-plus/icons-vue/2.0.9_vue@3.2.39: 124 | resolution: {integrity: sha512-okdrwiVeKBmW41Hkl0eMrXDjzJwhQMuKiBOu17rOszqM+LS/yBYpNQNV5Jvoh06Wc+89fMmb/uhzf8NZuDuUaQ==} 125 | peerDependencies: 126 | vue: ^3.2.0 127 | dependencies: 128 | vue: 3.2.39 129 | dev: false 130 | 131 | /@esbuild/linux-loong64/0.15.7: 132 | resolution: {integrity: sha512-IKznSJOsVUuyt7cDzzSZyqBEcZe+7WlBqTVXiF1OXP/4Nm387ToaXZ0fyLwI1iBlI/bzpxVq411QE2/Bt2XWWw==} 133 | engines: {node: '>=12'} 134 | cpu: [loong64] 135 | os: [linux] 136 | requiresBuild: true 137 | dev: true 138 | optional: true 139 | 140 | /@floating-ui/core/1.0.1: 141 | resolution: {integrity: sha512-bO37brCPfteXQfFY0DyNDGB3+IMe4j150KFQcgJ5aBP295p9nBGeHEs/p0czrRbtlHq4Px/yoPXO/+dOCcF4uA==} 142 | dev: false 143 | 144 | /@floating-ui/dom/1.0.1: 145 | resolution: {integrity: sha512-wBDiLUKWU8QNPNOTAFHiIAkBv1KlHauG2AhqjSeh2H+wR8PX+AArXfz8NkRexH5PgMJMmSOS70YS89AbWYh5dA==} 146 | dependencies: 147 | '@floating-ui/core': 1.0.1 148 | dev: false 149 | 150 | /@logicflow/core/1.1.27: 151 | resolution: {integrity: sha512-1OoioLTjOCfrBV2eJ9nNr+NT01XML0eq4BiMbRc6v7eBH085GZEynpLwipyLb0Zqp2rEJPvM/9R78E/LUyHb3g==} 152 | dependencies: 153 | '@types/mousetrap': 1.6.9 154 | mousetrap: 1.6.5 155 | preact: 10.11.0 156 | dev: false 157 | 158 | /@logicflow/core/1.1.28: 159 | resolution: {integrity: sha512-X85e0yU1jvFjbMXcfaFaRmM/ATHCx5JvWXUn65wLALZnqVmYe/OQ6FEMEuBv+sXjMwaluac0L/GWVpce8KGZ8g==} 160 | dependencies: 161 | '@types/mousetrap': 1.6.9 162 | mousetrap: 1.6.5 163 | preact: 10.11.0 164 | dev: false 165 | 166 | /@logicflow/extension/1.1.28: 167 | resolution: {integrity: sha512-duq6dfAgb8I/zv+DOWSj0W39Q2cXAOcgN8KTWPi+VnvX21gk0qtnrWkXeIE85Xpj6pRuFOooV+5wZqzoIDy1wg==} 168 | dependencies: 169 | '@logicflow/core': 1.1.28 170 | ids: 1.0.0 171 | preact: 10.11.0 172 | dev: false 173 | 174 | /@probe.gl/env/3.5.2: 175 | resolution: {integrity: sha512-JlNvJ2p6+ObWX7es6n3TycGPTv5CfVrCS8vblI1eHhrFCcZ6RxIo727ffRVwldpp0YTzdgjx3/4fB/1dnVYElw==} 176 | dependencies: 177 | '@babel/runtime': 7.19.0 178 | dev: false 179 | 180 | /@probe.gl/log/3.5.2: 181 | resolution: {integrity: sha512-5yo8Dg8LrSltuPBdGlLh/WOvt4LdU7DDHu75GMeiS0fKM+J4IACRpGV8SOrktCj1MWZ6JVHcNQkJnoyZ6G7p/w==} 182 | dependencies: 183 | '@babel/runtime': 7.19.0 184 | '@probe.gl/env': 3.5.2 185 | dev: false 186 | 187 | /@probe.gl/stats/3.5.2: 188 | resolution: {integrity: sha512-YKaYXiHF//fgy1OkX38JD70Lc8qxg2Viw8Q2CTNMwGPDJe12wda7kEmMKPJNw2oYLyFUfTzv00KJMA5h18z02w==} 189 | dependencies: 190 | '@babel/runtime': 7.19.0 191 | dev: false 192 | 193 | /@sxzz/popperjs-es/2.11.7: 194 | resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==} 195 | dev: false 196 | 197 | /@types/lodash-es/4.17.6: 198 | resolution: {integrity: sha512-R+zTeVUKDdfoRxpAryaQNRKk3105Rrgx2CFRClIgRGaqDTdjsm8h6IYA8ir584W3ePzkZfst5xIgDwYrlh9HLg==} 199 | dependencies: 200 | '@types/lodash': 4.14.185 201 | dev: false 202 | 203 | /@types/lodash/4.14.185: 204 | resolution: {integrity: sha512-evMDG1bC4rgQg4ku9tKpuMh5iBNEwNa3tf9zRHdP1qlv+1WUg44xat4IxCE14gIpZRGUUWAx2VhItCZc25NfMA==} 205 | dev: false 206 | 207 | /@types/mousetrap/1.6.9: 208 | resolution: {integrity: sha512-HUAiN65VsRXyFCTicolwb5+I7FM6f72zjMWr+ajGk+YTvzBgXqa2A5U7d+rtsouAkunJ5U4Sb5lNJjo9w+nmXg==} 209 | dev: false 210 | 211 | /@types/web-bluetooth/0.0.15: 212 | resolution: {integrity: sha512-w7hEHXnPMEZ+4nGKl/KDRVpxkwYxYExuHOYXyzIzCDzEZ9ZCGMAewulr9IqJu2LR4N37fcnb1XVeuZ09qgOxhA==} 213 | dev: false 214 | 215 | /@vitejs/plugin-vue/3.1.0_vite@3.1.0+vue@3.2.39: 216 | resolution: {integrity: sha512-fmxtHPjSOEIRg6vHYDaem+97iwCUg/uSIaTzp98lhELt2ISOQuDo2hbkBdXod0g15IhfPMQmAxh4heUks2zvDA==} 217 | engines: {node: ^14.18.0 || >=16.0.0} 218 | peerDependencies: 219 | vite: ^3.0.0 220 | vue: ^3.2.25 221 | dependencies: 222 | vite: 3.1.0 223 | vue: 3.2.39 224 | dev: true 225 | 226 | /@volar/code-gen/0.40.13: 227 | resolution: {integrity: sha512-4gShBWuMce868OVvgyA1cU5WxHbjfEme18Tw6uVMfweZCF5fB2KECG0iPrA9D54vHk3FeHarODNwgIaaFfUBlA==} 228 | dependencies: 229 | '@volar/source-map': 0.40.13 230 | dev: true 231 | 232 | /@volar/source-map/0.40.13: 233 | resolution: {integrity: sha512-dbdkAB2Nxb0wLjAY5O64o3ywVWlAGONnBIoKAkXSf6qkGZM+nJxcizsoiI66K+RHQG0XqlyvjDizfnTxr+6PWg==} 234 | dependencies: 235 | '@vue/reactivity': 3.2.38 236 | dev: true 237 | 238 | /@volar/typescript-faster/0.40.13: 239 | resolution: {integrity: sha512-uy+TlcFkKoNlKEnxA4x5acxdxLyVDIXGSc8cYDNXpPKjBKXrQaetzCzlO3kVBqu1VLMxKNGJMTKn35mo+ILQmw==} 240 | dependencies: 241 | semver: 7.3.7 242 | dev: true 243 | 244 | /@volar/vue-language-core/0.40.13: 245 | resolution: {integrity: sha512-QkCb8msi2KUitTdM6Y4kAb7/ZlEvuLcbBFOC2PLBlFuoZwyxvSP7c/dBGmKGtJlEvMX0LdCyrg5V2aBYxD38/Q==} 246 | dependencies: 247 | '@volar/code-gen': 0.40.13 248 | '@volar/source-map': 0.40.13 249 | '@vue/compiler-core': 3.2.39 250 | '@vue/compiler-dom': 3.2.39 251 | '@vue/compiler-sfc': 3.2.39 252 | '@vue/reactivity': 3.2.39 253 | '@vue/shared': 3.2.39 254 | dev: true 255 | 256 | /@volar/vue-typescript/0.40.13: 257 | resolution: {integrity: sha512-o7bNztwjs8JmbQjVkrnbZUOfm7q4B8ZYssETISN1tRaBdun6cfNqgpkvDYd+VUBh1O4CdksvN+5BUNnwAz4oCQ==} 258 | dependencies: 259 | '@volar/code-gen': 0.40.13 260 | '@volar/typescript-faster': 0.40.13 261 | '@volar/vue-language-core': 0.40.13 262 | dev: true 263 | 264 | /@vue/compiler-core/3.2.39: 265 | resolution: {integrity: sha512-mf/36OWXqWn0wsC40nwRRGheR/qoID+lZXbIuLnr4/AngM0ov8Xvv8GHunC0rKRIkh60bTqydlqTeBo49rlbqw==} 266 | dependencies: 267 | '@babel/parser': 7.19.1 268 | '@vue/shared': 3.2.39 269 | estree-walker: 2.0.2 270 | source-map: 0.6.1 271 | 272 | /@vue/compiler-dom/3.2.39: 273 | resolution: {integrity: sha512-HMFI25Be1C8vLEEv1hgEO1dWwG9QQ8LTTPmCkblVJY/O3OvWx6r1+zsox5mKPMGvqYEZa6l8j+xgOfUspgo7hw==} 274 | dependencies: 275 | '@vue/compiler-core': 3.2.39 276 | '@vue/shared': 3.2.39 277 | 278 | /@vue/compiler-sfc/3.2.39: 279 | resolution: {integrity: sha512-fqAQgFs1/BxTUZkd0Vakn3teKUt//J3c420BgnYgEOoVdTwYpBTSXCMJ88GOBCylmUBbtquGPli9tVs7LzsWIA==} 280 | dependencies: 281 | '@babel/parser': 7.19.1 282 | '@vue/compiler-core': 3.2.39 283 | '@vue/compiler-dom': 3.2.39 284 | '@vue/compiler-ssr': 3.2.39 285 | '@vue/reactivity-transform': 3.2.39 286 | '@vue/shared': 3.2.39 287 | estree-walker: 2.0.2 288 | magic-string: 0.25.9 289 | postcss: 8.4.16 290 | source-map: 0.6.1 291 | 292 | /@vue/compiler-ssr/3.2.39: 293 | resolution: {integrity: sha512-EoGCJ6lincKOZGW+0Ky4WOKsSmqL7hp1ZYgen8M7u/mlvvEQUaO9tKKOy7K43M9U2aA3tPv0TuYYQFrEbK2eFQ==} 294 | dependencies: 295 | '@vue/compiler-dom': 3.2.39 296 | '@vue/shared': 3.2.39 297 | 298 | /@vue/reactivity-transform/3.2.39: 299 | resolution: {integrity: sha512-HGuWu864zStiWs9wBC6JYOP1E00UjMdDWIG5W+FpUx28hV3uz9ODOKVNm/vdOy/Pvzg8+OcANxAVC85WFBbl3A==} 300 | dependencies: 301 | '@babel/parser': 7.19.1 302 | '@vue/compiler-core': 3.2.39 303 | '@vue/shared': 3.2.39 304 | estree-walker: 2.0.2 305 | magic-string: 0.25.9 306 | 307 | /@vue/reactivity/3.2.38: 308 | resolution: {integrity: sha512-6L4myYcH9HG2M25co7/BSo0skKFHpAN8PhkNPM4xRVkyGl1K5M3Jx4rp5bsYhvYze2K4+l+pioN4e6ZwFLUVtw==} 309 | dependencies: 310 | '@vue/shared': 3.2.38 311 | dev: true 312 | 313 | /@vue/reactivity/3.2.39: 314 | resolution: {integrity: sha512-vlaYX2a3qMhIZfrw3Mtfd+BuU+TZmvDrPMa+6lpfzS9k/LnGxkSuf0fhkP0rMGfiOHPtyKoU9OJJJFGm92beVQ==} 315 | dependencies: 316 | '@vue/shared': 3.2.39 317 | 318 | /@vue/runtime-core/3.2.39: 319 | resolution: {integrity: sha512-xKH5XP57JW5JW+8ZG1khBbuLakINTgPuINKL01hStWLTTGFOrM49UfCFXBcFvWmSbci3gmJyLl2EAzCaZWsx8g==} 320 | dependencies: 321 | '@vue/reactivity': 3.2.39 322 | '@vue/shared': 3.2.39 323 | dev: false 324 | 325 | /@vue/runtime-dom/3.2.39: 326 | resolution: {integrity: sha512-4G9AEJP+sLhsqf5wXcyKVWQKUhI+iWfy0hWQgea+CpaTD7BR0KdQzvoQdZhwCY6B3oleSyNLkLAQwm0ya/wNoA==} 327 | dependencies: 328 | '@vue/runtime-core': 3.2.39 329 | '@vue/shared': 3.2.39 330 | csstype: 2.6.21 331 | dev: false 332 | 333 | /@vue/server-renderer/3.2.39_vue@3.2.39: 334 | resolution: {integrity: sha512-1yn9u2YBQWIgytFMjz4f/t0j43awKytTGVptfd3FtBk76t1pd8mxbek0G/DrnjJhd2V7mSTb5qgnxMYt8Z5iSQ==} 335 | peerDependencies: 336 | vue: 3.2.39 337 | dependencies: 338 | '@vue/compiler-ssr': 3.2.39 339 | '@vue/shared': 3.2.39 340 | vue: 3.2.39 341 | dev: false 342 | 343 | /@vue/shared/3.2.38: 344 | resolution: {integrity: sha512-dTyhTIRmGXBjxJE+skC8tTWCGLCVc4wQgRRLt8+O9p5ewBAjoBwtCAkLPrtToSr1xltoe3st21Pv953aOZ7alg==} 345 | dev: true 346 | 347 | /@vue/shared/3.2.39: 348 | resolution: {integrity: sha512-D3dl2ZB9qE6mTuWPk9RlhDeP1dgNRUKC3NJxji74A4yL8M2MwlhLKUC/49WHjrNzSPug58fWx/yFbaTzGAQSBw==} 349 | 350 | /@vueuse/core/9.2.0_vue@3.2.39: 351 | resolution: {integrity: sha512-/MZ6qpz6uSyaXrtoeBWQzAKRG3N7CvfVWvQxiM3ei3Xe5ydOjjtVbo7lGl9p8dECV93j7W8s63A8H0kFLpLyxg==} 352 | dependencies: 353 | '@types/web-bluetooth': 0.0.15 354 | '@vueuse/metadata': 9.2.0 355 | '@vueuse/shared': 9.2.0_vue@3.2.39 356 | vue-demi: 0.13.11_vue@3.2.39 357 | transitivePeerDependencies: 358 | - '@vue/composition-api' 359 | - vue 360 | dev: false 361 | 362 | /@vueuse/metadata/9.2.0: 363 | resolution: {integrity: sha512-exN4KE6iquxDCdt72BgEhb3tlOpECtD61AUdXnUqBTIUCl70x1Ar/QXo3bYcvxmdMS2/peQyfeTzBjRTpvL5xw==} 364 | dev: false 365 | 366 | /@vueuse/shared/9.2.0_vue@3.2.39: 367 | resolution: {integrity: sha512-NnRp/noSWuXW0dKhZK5D0YLrDi0nmZ18UeEgwXQq7Ul5TTP93lcNnKjrHtd68j2xFB/l59yPGFlCryL692bnrA==} 368 | dependencies: 369 | vue-demi: 0.13.11_vue@3.2.39 370 | transitivePeerDependencies: 371 | - '@vue/composition-api' 372 | - vue 373 | dev: false 374 | 375 | /@webgpu/glslang/0.0.15: 376 | resolution: {integrity: sha512-niT+Prh3Aff8Uf1MVBVUsaNjFj9rJAKDXuoHIKiQbB+6IUP/3J3JIhBNyZ7lDhytvXxw6ppgnwKZdDJ08UMj4Q==} 377 | dev: false 378 | 379 | /@webgpu/types/0.0.31: 380 | resolution: {integrity: sha512-cvvCMSZBT4VsRNtt0lI6XQqvOIIWw6+NRUtnPUMDVDgsI4pCZColz3qzF5QcP9wIYOHEc3jssIBse8UWONKhlQ==} 381 | dev: false 382 | 383 | /async-validator/4.2.5: 384 | resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} 385 | dev: false 386 | 387 | /csstype/2.6.21: 388 | resolution: {integrity: sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==} 389 | dev: false 390 | 391 | /csstype/3.1.1: 392 | resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==} 393 | dev: false 394 | 395 | /d3-dispatch/2.0.0: 396 | resolution: {integrity: sha512-S/m2VsXI7gAti2pBoLClFFTMOO1HTtT0j99AuXLoGFKO6deHDdnv6ZGTxSTTUTgO1zVcv82fCOtDjYK4EECmWA==} 397 | dev: false 398 | 399 | /d3-force/2.1.1: 400 | resolution: {integrity: sha512-nAuHEzBqMvpFVMf9OX75d00OxvOXdxY+xECIXjW6Gv8BRrXu6gAWbv/9XKrvfJ5i5DCokDW7RYE50LRoK092ew==} 401 | dependencies: 402 | d3-dispatch: 2.0.0 403 | d3-quadtree: 2.0.0 404 | d3-timer: 2.0.0 405 | dev: false 406 | 407 | /d3-quadtree/2.0.0: 408 | resolution: {integrity: sha512-b0Ed2t1UUalJpc3qXzKi+cPGxeXRr4KU9YSlocN74aTzp6R/Ud43t79yLLqxHRWZfsvWXmbDWPpoENK1K539xw==} 409 | dev: false 410 | 411 | /d3-timer/2.0.0: 412 | resolution: {integrity: sha512-TO4VLh0/420Y/9dO3+f9abDEFYeCUr2WZRlxJvbp4HPTQcSylXNiL6yZa9FIUvV1yRiFufl1bszTCLDqv9PWNA==} 413 | dev: false 414 | 415 | /dagre-compound/0.0.11_dagre@0.8.5: 416 | resolution: {integrity: sha512-UrSgRP9LtOZCYb9e5doolZXpc7xayyszgyOs7uakTK4n4KsLegLVTRRtq01GpQd/iZjYw5fWMapx9ed+c80MAQ==} 417 | engines: {node: '>=6.0.0'} 418 | peerDependencies: 419 | dagre: ^0.8.5 420 | dependencies: 421 | dagre: 0.8.5 422 | dev: false 423 | 424 | /dagre/0.8.5: 425 | resolution: {integrity: sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==} 426 | dependencies: 427 | graphlib: 2.1.8 428 | lodash: 4.17.21 429 | dev: false 430 | 431 | /dayjs/1.11.5: 432 | resolution: {integrity: sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==} 433 | dev: false 434 | 435 | /element-plus/2.2.17_vue@3.2.39: 436 | resolution: {integrity: sha512-MGwMIE/q+FFD3kgS23x8HIe5043tmD1cTRwjhIX9o6fim1avFnUkrsfYRvybbz4CkyqSb185EheZS5AUPpXh2g==} 437 | peerDependencies: 438 | vue: ^3.2.0 439 | dependencies: 440 | '@ctrl/tinycolor': 3.4.1 441 | '@element-plus/icons-vue': 2.0.9_vue@3.2.39 442 | '@floating-ui/dom': 1.0.1 443 | '@popperjs/core': /@sxzz/popperjs-es/2.11.7 444 | '@types/lodash': 4.14.185 445 | '@types/lodash-es': 4.17.6 446 | '@vueuse/core': 9.2.0_vue@3.2.39 447 | async-validator: 4.2.5 448 | dayjs: 1.11.5 449 | escape-html: 1.0.3 450 | lodash: 4.17.21 451 | lodash-es: 4.17.21 452 | lodash-unified: 1.0.2_3ib2ivapxullxkx3xftsimdk7u 453 | memoize-one: 6.0.0 454 | normalize-wheel-es: 1.2.0 455 | vue: 3.2.39 456 | transitivePeerDependencies: 457 | - '@vue/composition-api' 458 | dev: false 459 | 460 | /esbuild-android-64/0.15.7: 461 | resolution: {integrity: sha512-p7rCvdsldhxQr3YHxptf1Jcd86dlhvc3EQmQJaZzzuAxefO9PvcI0GLOa5nCWem1AJ8iMRu9w0r5TG8pHmbi9w==} 462 | engines: {node: '>=12'} 463 | cpu: [x64] 464 | os: [android] 465 | requiresBuild: true 466 | dev: true 467 | optional: true 468 | 469 | /esbuild-android-arm64/0.15.7: 470 | resolution: {integrity: sha512-L775l9ynJT7rVqRM5vo+9w5g2ysbOCfsdLV4CWanTZ1k/9Jb3IYlQ06VCI1edhcosTYJRECQFJa3eAvkx72eyQ==} 471 | engines: {node: '>=12'} 472 | cpu: [arm64] 473 | os: [android] 474 | requiresBuild: true 475 | dev: true 476 | optional: true 477 | 478 | /esbuild-darwin-64/0.15.7: 479 | resolution: {integrity: sha512-KGPt3r1c9ww009t2xLB6Vk0YyNOXh7hbjZ3EecHoVDxgtbUlYstMPDaReimKe6eOEfyY4hBEEeTvKwPsiH5WZg==} 480 | engines: {node: '>=12'} 481 | cpu: [x64] 482 | os: [darwin] 483 | requiresBuild: true 484 | dev: true 485 | optional: true 486 | 487 | /esbuild-darwin-arm64/0.15.7: 488 | resolution: {integrity: sha512-kBIHvtVqbSGajN88lYMnR3aIleH3ABZLLFLxwL2stiuIGAjGlQW741NxVTpUHQXUmPzxi6POqc9npkXa8AcSZQ==} 489 | engines: {node: '>=12'} 490 | cpu: [arm64] 491 | os: [darwin] 492 | requiresBuild: true 493 | dev: true 494 | optional: true 495 | 496 | /esbuild-freebsd-64/0.15.7: 497 | resolution: {integrity: sha512-hESZB91qDLV5MEwNxzMxPfbjAhOmtfsr9Wnuci7pY6TtEh4UDuevmGmkUIjX/b+e/k4tcNBMf7SRQ2mdNuK/HQ==} 498 | engines: {node: '>=12'} 499 | cpu: [x64] 500 | os: [freebsd] 501 | requiresBuild: true 502 | dev: true 503 | optional: true 504 | 505 | /esbuild-freebsd-arm64/0.15.7: 506 | resolution: {integrity: sha512-dLFR0ChH5t+b3J8w0fVKGvtwSLWCv7GYT2Y2jFGulF1L5HftQLzVGN+6pi1SivuiVSmTh28FwUhi9PwQicXI6Q==} 507 | engines: {node: '>=12'} 508 | cpu: [arm64] 509 | os: [freebsd] 510 | requiresBuild: true 511 | dev: true 512 | optional: true 513 | 514 | /esbuild-linux-32/0.15.7: 515 | resolution: {integrity: sha512-v3gT/LsONGUZcjbt2swrMjwxo32NJzk+7sAgtxhGx1+ZmOFaTRXBAi1PPfgpeo/J//Un2jIKm/I+qqeo4caJvg==} 516 | engines: {node: '>=12'} 517 | cpu: [ia32] 518 | os: [linux] 519 | requiresBuild: true 520 | dev: true 521 | optional: true 522 | 523 | /esbuild-linux-64/0.15.7: 524 | resolution: {integrity: sha512-LxXEfLAKwOVmm1yecpMmWERBshl+Kv5YJ/1KnyAr6HRHFW8cxOEsEfisD3sVl/RvHyW//lhYUVSuy9jGEfIRAQ==} 525 | engines: {node: '>=12'} 526 | cpu: [x64] 527 | os: [linux] 528 | requiresBuild: true 529 | dev: true 530 | optional: true 531 | 532 | /esbuild-linux-arm/0.15.7: 533 | resolution: {integrity: sha512-JKgAHtMR5f75wJTeuNQbyznZZa+pjiUHV7sRZp42UNdyXC6TiUYMW/8z8yIBAr2Fpad8hM1royZKQisqPABPvQ==} 534 | engines: {node: '>=12'} 535 | cpu: [arm] 536 | os: [linux] 537 | requiresBuild: true 538 | dev: true 539 | optional: true 540 | 541 | /esbuild-linux-arm64/0.15.7: 542 | resolution: {integrity: sha512-P3cfhudpzWDkglutWgXcT2S7Ft7o2e3YDMrP1n0z2dlbUZghUkKCyaWw0zhp4KxEEzt/E7lmrtRu/pGWnwb9vw==} 543 | engines: {node: '>=12'} 544 | cpu: [arm64] 545 | os: [linux] 546 | requiresBuild: true 547 | dev: true 548 | optional: true 549 | 550 | /esbuild-linux-mips64le/0.15.7: 551 | resolution: {integrity: sha512-T7XKuxl0VpeFLCJXub6U+iybiqh0kM/bWOTb4qcPyDDwNVhLUiPcGdG2/0S7F93czUZOKP57YiLV8YQewgLHKw==} 552 | engines: {node: '>=12'} 553 | cpu: [mips64el] 554 | os: [linux] 555 | requiresBuild: true 556 | dev: true 557 | optional: true 558 | 559 | /esbuild-linux-ppc64le/0.15.7: 560 | resolution: {integrity: sha512-6mGuC19WpFN7NYbecMIJjeQgvDb5aMuvyk0PDYBJrqAEMkTwg3Z98kEKuCm6THHRnrgsdr7bp4SruSAxEM4eJw==} 561 | engines: {node: '>=12'} 562 | cpu: [ppc64] 563 | os: [linux] 564 | requiresBuild: true 565 | dev: true 566 | optional: true 567 | 568 | /esbuild-linux-riscv64/0.15.7: 569 | resolution: {integrity: sha512-uUJsezbswAYo/X7OU/P+PuL/EI9WzxsEQXDekfwpQ23uGiooxqoLFAPmXPcRAt941vjlY9jtITEEikWMBr+F/g==} 570 | engines: {node: '>=12'} 571 | cpu: [riscv64] 572 | os: [linux] 573 | requiresBuild: true 574 | dev: true 575 | optional: true 576 | 577 | /esbuild-linux-s390x/0.15.7: 578 | resolution: {integrity: sha512-+tO+xOyTNMc34rXlSxK7aCwJgvQyffqEM5MMdNDEeMU3ss0S6wKvbBOQfgd5jRPblfwJ6b+bKiz0g5nABpY0QQ==} 579 | engines: {node: '>=12'} 580 | cpu: [s390x] 581 | os: [linux] 582 | requiresBuild: true 583 | dev: true 584 | optional: true 585 | 586 | /esbuild-netbsd-64/0.15.7: 587 | resolution: {integrity: sha512-yVc4Wz+Pu3cP5hzm5kIygNPrjar/v5WCSoRmIjCPWfBVJkZNb5brEGKUlf+0Y759D48BCWa0WHrWXaNy0DULTQ==} 588 | engines: {node: '>=12'} 589 | cpu: [x64] 590 | os: [netbsd] 591 | requiresBuild: true 592 | dev: true 593 | optional: true 594 | 595 | /esbuild-openbsd-64/0.15.7: 596 | resolution: {integrity: sha512-GsimbwC4FSR4lN3wf8XmTQ+r8/0YSQo21rWDL0XFFhLHKlzEA4SsT1Tl8bPYu00IU6UWSJ+b3fG/8SB69rcuEQ==} 597 | engines: {node: '>=12'} 598 | cpu: [x64] 599 | os: [openbsd] 600 | requiresBuild: true 601 | dev: true 602 | optional: true 603 | 604 | /esbuild-sunos-64/0.15.7: 605 | resolution: {integrity: sha512-8CDI1aL/ts0mDGbWzjEOGKXnU7p3rDzggHSBtVryQzkSOsjCHRVe0iFYUuhczlxU1R3LN/E7HgUO4NXzGGP/Ag==} 606 | engines: {node: '>=12'} 607 | cpu: [x64] 608 | os: [sunos] 609 | requiresBuild: true 610 | dev: true 611 | optional: true 612 | 613 | /esbuild-windows-32/0.15.7: 614 | resolution: {integrity: sha512-cOnKXUEPS8EGCzRSFa1x6NQjGhGsFlVgjhqGEbLTPsA7x4RRYiy2RKoArNUU4iR2vHmzqS5Gr84MEumO/wxYKA==} 615 | engines: {node: '>=12'} 616 | cpu: [ia32] 617 | os: [win32] 618 | requiresBuild: true 619 | dev: true 620 | optional: true 621 | 622 | /esbuild-windows-64/0.15.7: 623 | resolution: {integrity: sha512-7MI08Ec2sTIDv+zH6StNBKO+2hGUYIT42GmFyW6MBBWWtJhTcQLinKS6ldIN1d52MXIbiJ6nXyCJ+LpL4jBm3Q==} 624 | engines: {node: '>=12'} 625 | cpu: [x64] 626 | os: [win32] 627 | requiresBuild: true 628 | dev: true 629 | optional: true 630 | 631 | /esbuild-windows-arm64/0.15.7: 632 | resolution: {integrity: sha512-R06nmqBlWjKHddhRJYlqDd3Fabx9LFdKcjoOy08YLimwmsswlFBJV4rXzZCxz/b7ZJXvrZgj8DDv1ewE9+StMw==} 633 | engines: {node: '>=12'} 634 | cpu: [arm64] 635 | os: [win32] 636 | requiresBuild: true 637 | dev: true 638 | optional: true 639 | 640 | /esbuild/0.15.7: 641 | resolution: {integrity: sha512-7V8tzllIbAQV1M4QoE52ImKu8hT/NLGlGXkiDsbEU5PS6K8Mn09ZnYoS+dcmHxOS9CRsV4IRAMdT3I67IyUNXw==} 642 | engines: {node: '>=12'} 643 | hasBin: true 644 | requiresBuild: true 645 | optionalDependencies: 646 | '@esbuild/linux-loong64': 0.15.7 647 | esbuild-android-64: 0.15.7 648 | esbuild-android-arm64: 0.15.7 649 | esbuild-darwin-64: 0.15.7 650 | esbuild-darwin-arm64: 0.15.7 651 | esbuild-freebsd-64: 0.15.7 652 | esbuild-freebsd-arm64: 0.15.7 653 | esbuild-linux-32: 0.15.7 654 | esbuild-linux-64: 0.15.7 655 | esbuild-linux-arm: 0.15.7 656 | esbuild-linux-arm64: 0.15.7 657 | esbuild-linux-mips64le: 0.15.7 658 | esbuild-linux-ppc64le: 0.15.7 659 | esbuild-linux-riscv64: 0.15.7 660 | esbuild-linux-s390x: 0.15.7 661 | esbuild-netbsd-64: 0.15.7 662 | esbuild-openbsd-64: 0.15.7 663 | esbuild-sunos-64: 0.15.7 664 | esbuild-windows-32: 0.15.7 665 | esbuild-windows-64: 0.15.7 666 | esbuild-windows-arm64: 0.15.7 667 | dev: true 668 | 669 | /escape-html/1.0.3: 670 | resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} 671 | dev: false 672 | 673 | /estree-walker/2.0.2: 674 | resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} 675 | 676 | /eventemitter3/4.0.7: 677 | resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} 678 | dev: false 679 | 680 | /fsevents/2.3.2: 681 | resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} 682 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 683 | os: [darwin] 684 | requiresBuild: true 685 | dev: true 686 | optional: true 687 | 688 | /function-bind/1.1.1: 689 | resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} 690 | dev: true 691 | 692 | /gl-matrix/3.4.3: 693 | resolution: {integrity: sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==} 694 | dev: false 695 | 696 | /gl-vec2/1.3.0: 697 | resolution: {integrity: sha512-YiqaAuNsheWmUV0Sa8k94kBB0D6RWjwZztyO+trEYS8KzJ6OQB/4686gdrf59wld4hHFIvaxynO3nRxpk1Ij/A==} 698 | dev: false 699 | 700 | /graphlib/2.1.8: 701 | resolution: {integrity: sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==} 702 | dependencies: 703 | lodash: 4.17.21 704 | dev: false 705 | 706 | /hammerjs/2.0.8: 707 | resolution: {integrity: sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==} 708 | engines: {node: '>=0.8.0'} 709 | dev: false 710 | 711 | /has/1.0.3: 712 | resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} 713 | engines: {node: '>= 0.4.0'} 714 | dependencies: 715 | function-bind: 1.1.1 716 | dev: true 717 | 718 | /ids/1.0.0: 719 | resolution: {integrity: sha512-Zvtq1xUto4LttpstyOlFum8lKx+i1OmRfg+6A9drFS9iSZsDPMHG4Sof/qwNR4kCU7jBeWFPrY2ocHxiz7cCRw==} 720 | dev: false 721 | 722 | /inversify-inject-decorators/3.1.0: 723 | resolution: {integrity: sha512-/seBlVp5bXrLQS3DpKEmlgeZL6C7Tf/QITd+IMQrbBBGuCbxb7k3hRAWu9XSreNpFzLgSboz3sClLSEmGwHphw==} 724 | dev: false 725 | 726 | /inversify/5.1.1: 727 | resolution: {integrity: sha512-j8grHGDzv1v+8T1sAQ+3boTCntFPfvxLCkNcxB1J8qA0lUN+fAlSyYd+RXKvaPRL4AGyPxViutBEJHNXOyUdFQ==} 728 | dev: false 729 | 730 | /is-any-array/2.0.0: 731 | resolution: {integrity: sha512-WdPV58rT3aOWXvvyuBydnCq4S2BM1Yz8shKxlEpk/6x+GX202XRvXOycEFtNgnHVLoc46hpexPFx8Pz1/sMS0w==} 732 | dev: false 733 | 734 | /is-core-module/2.10.0: 735 | resolution: {integrity: sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==} 736 | dependencies: 737 | has: 1.0.3 738 | dev: true 739 | 740 | /lodash-es/4.17.21: 741 | resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} 742 | dev: false 743 | 744 | /lodash-unified/1.0.2_3ib2ivapxullxkx3xftsimdk7u: 745 | resolution: {integrity: sha512-OGbEy+1P+UT26CYi4opY4gebD8cWRDxAT6MAObIVQMiqYdxZr1g3QHWCToVsm31x2NkLS4K3+MC2qInaRMa39g==} 746 | peerDependencies: 747 | '@types/lodash-es': '*' 748 | lodash: '*' 749 | lodash-es: '*' 750 | dependencies: 751 | '@types/lodash-es': 4.17.6 752 | lodash: 4.17.21 753 | lodash-es: 4.17.21 754 | dev: false 755 | 756 | /lodash/4.17.21: 757 | resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} 758 | dev: false 759 | 760 | /lru-cache/6.0.0: 761 | resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} 762 | engines: {node: '>=10'} 763 | dependencies: 764 | yallist: 4.0.0 765 | dev: true 766 | 767 | /magic-string/0.25.9: 768 | resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} 769 | dependencies: 770 | sourcemap-codec: 1.4.8 771 | 772 | /memoize-one/6.0.0: 773 | resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} 774 | dev: false 775 | 776 | /ml-array-max/1.2.4: 777 | resolution: {integrity: sha512-BlEeg80jI0tW6WaPyGxf5Sa4sqvcyY6lbSn5Vcv44lp1I2GR6AWojfUvLnGTNsIXrZ8uqWmo8VcG1WpkI2ONMQ==} 778 | dependencies: 779 | is-any-array: 2.0.0 780 | dev: false 781 | 782 | /ml-array-min/1.2.3: 783 | resolution: {integrity: sha512-VcZ5f3VZ1iihtrGvgfh/q0XlMobG6GQ8FsNyQXD3T+IlstDv85g8kfV0xUG1QPRO/t21aukaJowDzMTc7j5V6Q==} 784 | dependencies: 785 | is-any-array: 2.0.0 786 | dev: false 787 | 788 | /ml-array-rescale/1.3.7: 789 | resolution: {integrity: sha512-48NGChTouvEo9KBctDfHC3udWnQKNKEWN0ziELvY3KG25GR5cA8K8wNVzracsqSW1QEkAXjTNx+ycgAv06/1mQ==} 790 | dependencies: 791 | is-any-array: 2.0.0 792 | ml-array-max: 1.2.4 793 | ml-array-min: 1.2.3 794 | dev: false 795 | 796 | /ml-matrix/6.10.2: 797 | resolution: {integrity: sha512-+yxwzMazC76DAsgEvDrdCfRMH7+lO7UdnuWllavnPeWkPD2oIBA6rLzvst7BFYUxzgRz/h6V6lLzYIYtTvINbw==} 798 | dependencies: 799 | is-any-array: 2.0.0 800 | ml-array-rescale: 1.3.7 801 | dev: false 802 | 803 | /mousetrap/1.6.5: 804 | resolution: {integrity: sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==} 805 | dev: false 806 | 807 | /nanoid/3.3.4: 808 | resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} 809 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 810 | hasBin: true 811 | 812 | /normalize-wheel-es/1.2.0: 813 | resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==} 814 | dev: false 815 | 816 | /path-parse/1.0.7: 817 | resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} 818 | dev: true 819 | 820 | /picocolors/1.0.0: 821 | resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} 822 | 823 | /polyline-miter-util/1.0.1: 824 | resolution: {integrity: sha512-/3u91zz6mBerBZo6qnOJOTjv7EfPhKtsV028jMyj86YpzLRNmCCFfrX7IO9tCEQ2W4x45yc+vKOezjf7u2Nd6Q==} 825 | dependencies: 826 | gl-vec2: 1.3.0 827 | dev: false 828 | 829 | /polyline-normals/2.0.2: 830 | resolution: {integrity: sha512-dpHrAi61ymhsB4N0XlNb3YpkKJeTFnXBXDWpeH8Ucstq0TUZrCN3YK4Jlgk8ofMWN25lhGC4wnxLMv+TUK8rig==} 831 | dependencies: 832 | polyline-miter-util: 1.0.1 833 | dev: false 834 | 835 | /postcss/8.4.16: 836 | resolution: {integrity: sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==} 837 | engines: {node: ^10 || ^12 || >=14} 838 | dependencies: 839 | nanoid: 3.3.4 840 | picocolors: 1.0.0 841 | source-map-js: 1.0.2 842 | 843 | /preact/10.11.0: 844 | resolution: {integrity: sha512-Fk6+vB2kb6mSJfDgODq0YDhMfl0HNtK5+Uc9QqECO4nlyPAQwCI+BKyWO//idA7ikV7o+0Fm6LQmNuQi1wXI1w==} 845 | dev: false 846 | 847 | /probe.gl/3.5.2: 848 | resolution: {integrity: sha512-8lFQVmi7pMQZkqfj8+VjX4GU9HTkyxgRm5/h/xxA/4/IvZPv3qtP996L+awPwZsrPRKEw99t12SvqEHqSls/sA==} 849 | dependencies: 850 | '@babel/runtime': 7.19.0 851 | '@probe.gl/env': 3.5.2 852 | '@probe.gl/log': 3.5.2 853 | '@probe.gl/stats': 3.5.2 854 | dev: false 855 | 856 | /reflect-metadata/0.1.13: 857 | resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==} 858 | dev: false 859 | 860 | /regenerator-runtime/0.13.9: 861 | resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==} 862 | dev: false 863 | 864 | /regl/1.7.0: 865 | resolution: {integrity: sha512-bEAtp/qrtKucxXSJkD4ebopFZYP0q1+3Vb2WECWv/T8yQEgKxDxJ7ztO285tAMaYZVR6mM1GgI6CCn8FROtL1w==} 866 | dev: false 867 | 868 | /resolve/1.22.1: 869 | resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} 870 | hasBin: true 871 | dependencies: 872 | is-core-module: 2.10.0 873 | path-parse: 1.0.7 874 | supports-preserve-symlinks-flag: 1.0.0 875 | dev: true 876 | 877 | /rollup/2.78.1: 878 | resolution: {integrity: sha512-VeeCgtGi4P+o9hIg+xz4qQpRl6R401LWEXBmxYKOV4zlF82lyhgh2hTZnheFUbANE8l2A41F458iwj2vEYaXJg==} 879 | engines: {node: '>=10.0.0'} 880 | hasBin: true 881 | optionalDependencies: 882 | fsevents: 2.3.2 883 | dev: true 884 | 885 | /semver/7.3.7: 886 | resolution: {integrity: sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==} 887 | engines: {node: '>=10'} 888 | hasBin: true 889 | dependencies: 890 | lru-cache: 6.0.0 891 | dev: true 892 | 893 | /source-map-js/1.0.2: 894 | resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} 895 | engines: {node: '>=0.10.0'} 896 | 897 | /source-map/0.6.1: 898 | resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 899 | engines: {node: '>=0.10.0'} 900 | 901 | /sourcemap-codec/1.4.8: 902 | resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} 903 | 904 | /supports-preserve-symlinks-flag/1.0.0: 905 | resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 906 | engines: {node: '>= 0.4'} 907 | dev: true 908 | 909 | /tslib/2.4.0: 910 | resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} 911 | dev: false 912 | 913 | /typescript/4.8.3: 914 | resolution: {integrity: sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==} 915 | engines: {node: '>=4.2.0'} 916 | hasBin: true 917 | dev: true 918 | 919 | /vite/3.1.0: 920 | resolution: {integrity: sha512-YBg3dUicDpDWFCGttmvMbVyS9ydjntwEjwXRj2KBFwSB8SxmGcudo1yb8FW5+M/G86aS8x828ujnzUVdsLjs9g==} 921 | engines: {node: ^14.18.0 || >=16.0.0} 922 | hasBin: true 923 | peerDependencies: 924 | less: '*' 925 | sass: '*' 926 | stylus: '*' 927 | terser: ^5.4.0 928 | peerDependenciesMeta: 929 | less: 930 | optional: true 931 | sass: 932 | optional: true 933 | stylus: 934 | optional: true 935 | terser: 936 | optional: true 937 | dependencies: 938 | esbuild: 0.15.7 939 | postcss: 8.4.16 940 | resolve: 1.22.1 941 | rollup: 2.78.1 942 | optionalDependencies: 943 | fsevents: 2.3.2 944 | dev: true 945 | 946 | /vue-demi/0.13.11_vue@3.2.39: 947 | resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==} 948 | engines: {node: '>=12'} 949 | hasBin: true 950 | requiresBuild: true 951 | peerDependencies: 952 | '@vue/composition-api': ^1.0.0-rc.1 953 | vue: ^3.0.0-0 || ^2.6.0 954 | peerDependenciesMeta: 955 | '@vue/composition-api': 956 | optional: true 957 | dependencies: 958 | vue: 3.2.39 959 | dev: false 960 | 961 | /vue-tsc/0.40.13_typescript@4.8.3: 962 | resolution: {integrity: sha512-xzuN3g5PnKfJcNrLv4+mAjteMd5wLm5fRhW0034OfNJZY4WhB07vhngea/XeGn7wNYt16r7syonzvW/54dcNiA==} 963 | hasBin: true 964 | peerDependencies: 965 | typescript: '*' 966 | dependencies: 967 | '@volar/vue-language-core': 0.40.13 968 | '@volar/vue-typescript': 0.40.13 969 | typescript: 4.8.3 970 | dev: true 971 | 972 | /vue/3.2.39: 973 | resolution: {integrity: sha512-tRkguhRTw9NmIPXhzk21YFBqXHT2t+6C6wPOgQ50fcFVWnPdetmRqbmySRHznrYjX2E47u0cGlKGcxKZJ38R/g==} 974 | dependencies: 975 | '@vue/compiler-dom': 3.2.39 976 | '@vue/compiler-sfc': 3.2.39 977 | '@vue/runtime-dom': 3.2.39 978 | '@vue/server-renderer': 3.2.39_vue@3.2.39 979 | '@vue/shared': 3.2.39 980 | dev: false 981 | 982 | /yallist/4.0.0: 983 | resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} 984 | dev: true 985 | -------------------------------------------------------------------------------- /public/images/beautify.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/delay.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/images/grid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 背景网格 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 9 | -------------------------------------------------------------------------------- /src/components/BeautifyFlow.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 61 | 62 | 155 | -------------------------------------------------------------------------------- /src/components/FlowHeader.vue: -------------------------------------------------------------------------------- 1 | 14 | 37 | -------------------------------------------------------------------------------- /src/components/LayoutFlow.vue: -------------------------------------------------------------------------------- 1 | 13 | 22 | -------------------------------------------------------------------------------- /src/components/MindmapFlow.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 75 | 76 | 107 | -------------------------------------------------------------------------------- /src/components/beautifyElement/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { createApp } from 'vue'; 3 | import './style.css' 4 | import BeautifyLine from './nodes/BeautifyLine' 5 | import BeautifyNode from './nodes/BeautifyNode' 6 | import Palette from './tools/Palette.vue'; 7 | 8 | class BeautifyFlowExtension { 9 | static pluginName = 'BeautifyFlowExtension' 10 | app: any; 11 | constructor ({ lf, LogicFlow }) { 12 | lf.register(BeautifyLine) 13 | lf.register(BeautifyNode) 14 | this.app = createApp(Palette, { 15 | lf 16 | }) 17 | lf.setDefaultEdgeType(BeautifyLine.type) 18 | 19 | } 20 | render(lf, domOverlay) { 21 | const node = document.createElement('div') 22 | node.className = 'node-red-palette' 23 | domOverlay.appendChild(node) 24 | this.app.mount(node) 25 | } 26 | } 27 | 28 | export default BeautifyFlowExtension -------------------------------------------------------------------------------- /src/components/beautifyElement/nodes/BeautifyLine.ts: -------------------------------------------------------------------------------- 1 | // const { PolylineEdge, PolylineEdgeModel, h} = window; 2 | import { PolylineEdge, PolylineEdgeModel, h } from "@logicflow/core"; 3 | import { OutlineTheme } from "@logicflow/core/types/constant/DefaultTheme"; 4 | import { pointFilter } from '../../util'; 5 | 6 | const DISTANCE = 12; 7 | const ICON_HEIGHT = 16; 8 | const ICON_WIDTH = 16; 9 | const WORD_HEIGHT = 16; 10 | const NODE_HEIGHT = 30; 11 | class BeautifyLine extends PolylineEdge { 12 | getText() { 13 | const { pointsList, text, id } = this.props.model; 14 | if (!pointsList || pointsList.length === 0) return null; 15 | const lastPoint = pointsList[pointsList.length - 1]; 16 | const lastPrePoint = pointsList[pointsList.length - 2]; 17 | const positionData: { 18 | x?: number, 19 | y?: number, 20 | } = {} 21 | let width = Math.abs(lastPoint.x - lastPrePoint.x); 22 | let height = Math.abs(lastPoint.y - lastPrePoint.y); 23 | let direction = '' 24 | if (lastPoint.x < lastPrePoint.x) { // 箭头向左 25 | direction = 'row' 26 | positionData.x = lastPoint.x + DISTANCE; 27 | positionData.y = lastPoint.y - ICON_HEIGHT / 2; 28 | } else if (lastPoint.y < lastPrePoint.y) { // 箭头向上 29 | direction = 'column' 30 | positionData.x = lastPoint.x - ICON_WIDTH / 2; 31 | positionData.y = lastPoint.y + DISTANCE + ICON_HEIGHT / 2; 32 | } else if (lastPoint.y > lastPrePoint.y) { // 箭头向下 33 | direction = 'column-reverse' 34 | positionData.x = lastPoint.x - ICON_WIDTH / 2; 35 | positionData.y = lastPoint.y - DISTANCE - ICON_HEIGHT / 2 - WORD_HEIGHT; 36 | } else { // 箭头向右 37 | direction = 'row-reverse' 38 | positionData.x = lastPoint.x - DISTANCE - width; 39 | positionData.y = lastPoint.y - ICON_HEIGHT / 2; 40 | } 41 | 42 | const { model } = this.props; 43 | return h("foreignObject", { ...positionData, id: 'line_' + id, style: `z-index: 20; width: ${width ? width : height}px`}, [ 44 | h("div", { 45 | style: `display:flex;width: 100%;flex-direction: ${direction};` 46 | }, [ 47 | h("div", { 48 | className: "add-wrapper", 49 | }), 50 | ]) 51 | ]) 52 | } 53 | } 54 | 55 | class BeautifyLineModel extends PolylineEdgeModel { 56 | initEdgeData(data) { 57 | super.initEdgeData(data) 58 | this.offset=30 59 | } 60 | setAttributes () { 61 | if (this.properties.executeStatus === 'executed') { 62 | this.setZIndex(999); 63 | } else { 64 | this.setZIndex(0) 65 | } 66 | } 67 | getOutlineStyle(): OutlineTheme { 68 | const style = super.getOutlineStyle() 69 | style.stroke = 'none' 70 | return style 71 | } 72 | getEdgeStyle() { 73 | const style = super.getEdgeStyle(); 74 | style.stroke = '#BAC1D0'; 75 | style.strokeDasharray = '3 2'; 76 | style.strokeWidth = 1; 77 | if (this.isHovered || this.isSelected) { 78 | style.stroke = '#33DD89' 79 | } 80 | return style; 81 | } 82 | getEdgeAnimationStyle() { 83 | const style = super.getEdgeAnimationStyle(); 84 | style.animationDuration = "20s"; 85 | const { executeStatus } = this.properties; 86 | if (executeStatus) { 87 | style.strokeDasharray = "8 3"; 88 | style.stroke = executeStatus === 'executed' ? 'rgb(79 235 151 / 80%)' : 'red' 89 | if (executeStatus === 'execute-failed') { 90 | style.strokeDasharray = null; 91 | } 92 | } else { 93 | style.strokeDasharray = "3 2"; 94 | style.stroke = '#33DD89' 95 | } 96 | return style; 97 | } 98 | setHovered(flag) { 99 | super.setHovered(flag); 100 | this.setZIndex(flag ? 999 : 0); 101 | } 102 | setSelected(flag) { 103 | super.setSelected(flag); 104 | this.setZIndex(flag ? 999 : 0); 105 | } 106 | setZIndex (index) { 107 | if (this.isHovered || this.isSelected || this.properties.executeStatus) { 108 | super.setZIndex(999); 109 | this.openEdgeAnimation(); 110 | } else { 111 | this.closeEdgeAnimation(); 112 | super.setZIndex(index); 113 | } 114 | } 115 | initPoints() { 116 | if (this.pointsList && this.pointsList.length > 0) { 117 | this.points = this.pointsList.map(point => `${point.x},${point.y}`).join(' '); 118 | return 119 | } 120 | const { startPoint, endPoint } = this 121 | const { x: x1, y: y1 } = startPoint 122 | const { x: x2, y: y2 } = endPoint 123 | const betterDistance = this.offset * 2 124 | // 1. 起点在终点左边 125 | if (x1 - x2 < -betterDistance) { 126 | this.pointsList = pointFilter([ 127 | { 128 | x: x1, 129 | y: y1 130 | }, 131 | { 132 | x: x1 + this.offset, 133 | y: y1 134 | }, 135 | { 136 | x: x1 + this.offset, 137 | y: y2 138 | }, 139 | { 140 | x: x2, 141 | y: y2 142 | } 143 | ]) 144 | this.points = this.pointsList.map(point => `${point.x},${point.y}`).join(' '); 145 | } else if (x1 - x2 > betterDistance) { // 起点在右边,终点在左边 146 | this.pointsList = pointFilter([ 147 | { 148 | x: x1, 149 | y: y1 150 | }, 151 | { 152 | x: x1 + this.offset, 153 | y: y1 154 | }, 155 | { 156 | x: x1 + this.offset, 157 | y: y2 + NODE_HEIGHT 158 | }, 159 | { 160 | x: x2 - NODE_HEIGHT / 2, 161 | y: y2 + NODE_HEIGHT 162 | }, 163 | { 164 | x: x2 - NODE_HEIGHT / 2, 165 | y: y2 166 | }, 167 | { 168 | x: x2, 169 | y: y2 170 | } 171 | ]) 172 | this.points = this.pointsList.map(point => `${point.x},${point.y}`).join(' '); 173 | } else { 174 | super.initPoints() 175 | } 176 | } 177 | } 178 | 179 | export default { 180 | type: 'beautify-line', 181 | model: BeautifyLineModel, 182 | view: BeautifyLine 183 | } 184 | -------------------------------------------------------------------------------- /src/components/beautifyElement/nodes/BeautifyNode.ts: -------------------------------------------------------------------------------- 1 | import { RectNode, RectNodeModel, h } from "@logicflow/core" 2 | import { getTextLengthByCanvas } from "../../util"; 3 | 4 | 5 | class BeautifyNodeModel extends RectNodeModel { 6 | /** 7 | * 初始化 8 | */ 9 | initNodeData(data) { 10 | super.initNodeData(data) 11 | this.width = 120; 12 | this.height = 40; 13 | this.radius = 5; 14 | this.text.x = this.x + 10; 15 | this.text.y = this.y; 16 | this.iconPosition = ''; 17 | this.defaultFill = '#a6bbcf'; 18 | } 19 | getData () { 20 | const data = super.getData() 21 | data.properties.ui = 'node-red' 22 | return data 23 | } 24 | /** 25 | * 动态设置初始化数据 26 | */ 27 | setAttributes() { 28 | if (this.text.value) { 29 | let width = 30 + getTextLengthByCanvas(this.text.value, 12).width; 30 | width = Math.ceil(width / 20) * 20; 31 | if (width < 100) { 32 | width = 100; 33 | } 34 | this.width = width; 35 | } 36 | } 37 | updateText(val) { 38 | super.updateText(val) 39 | this.setAttributes(); 40 | } 41 | /** 42 | * 重写节点样式 43 | */ 44 | getNodeStyle() { 45 | const style = super.getNodeStyle(); 46 | const dataStyle = this.properties.style || {}; 47 | if (this.isSelected) { 48 | style.strokeWidth = Number(dataStyle.borderWidth) || 2; 49 | style.stroke = dataStyle.borderColor || '#ff7f0e'; 50 | } else { 51 | style.strokeWidth = Number(dataStyle.borderWidth) || 1; 52 | style.stroke = dataStyle.borderColor || '#999'; 53 | } 54 | style.fill = dataStyle.backgroundColor || this.defaultFill; 55 | return style; 56 | } 57 | /** 58 | * 重写定义锚点 59 | */ 60 | getDefaultAnchor() { 61 | const { x, y, id, width, height } = this; 62 | const anchors = [ 63 | { 64 | x: x + width / 2, 65 | y: y, 66 | id: `${id}_right`, 67 | type: "right" 68 | }, 69 | { 70 | x: x - width / 2, 71 | y: y, 72 | id: `${id}_left`, 73 | type: "left" 74 | } 75 | ]; 76 | return anchors; 77 | } 78 | /** 79 | * 80 | */ 81 | getOutlineStyle() { 82 | const style = super.getOutlineStyle(); 83 | style.stroke = 'transparent'; 84 | style.hover.stroke = 'transparent'; 85 | return style; 86 | } 87 | } 88 | class BeautifyNode extends RectNode { 89 | getAnchorShape(anchorData) { 90 | const { x, y, type } = anchorData; 91 | return h("rect", { 92 | x: x - 5, 93 | y: y - 5, 94 | width: 10, 95 | height: 10, 96 | className: 'custom-anchor' 97 | }); 98 | } 99 | getIcon () { 100 | const { 101 | width, 102 | height, 103 | } = this.props.model; 104 | return h('image', { 105 | width: 30, 106 | height: 40, 107 | x: - width / 2, 108 | y: - height / 2, 109 | href: './images/delay.svg' 110 | }) 111 | } 112 | getShape() { 113 | const { 114 | text, 115 | x, 116 | y, 117 | width, 118 | height, 119 | radius 120 | } = this.props.model; 121 | const style = this.props.model.getNodeStyle() 122 | return h( 123 | 'g', 124 | { 125 | className: 'lf-red-node' 126 | }, 127 | [ 128 | h('rect', { 129 | ...style, 130 | x: x - width / 2, 131 | y: y - height / 2, 132 | width, 133 | height, 134 | rx: radius, 135 | ry: radius 136 | }), 137 | h('g', { 138 | style: 'pointer-events: none;', 139 | transform: `translate(${x}, ${y})` 140 | }, [ 141 | h('rect', { 142 | x: - width / 2, 143 | y: - height / 2, 144 | width: 30, 145 | height: 40, 146 | fill: '#000', 147 | fillOpacity: 0.05, 148 | stroke: 'none', 149 | }), 150 | this.getIcon(), 151 | h('path', { 152 | d: `M ${30 - width / 2} ${1 -height / 2 } l 0 38`, 153 | stroke: '#000', 154 | strokeOpacity: 0.1, 155 | strokeWidth: 1 156 | }) 157 | ]) 158 | ] 159 | ) 160 | } 161 | } 162 | 163 | 164 | export default { 165 | type: 'beautify-node', 166 | model: BeautifyNodeModel, 167 | view: BeautifyNode 168 | } 169 | -------------------------------------------------------------------------------- /src/components/beautifyElement/style.css: -------------------------------------------------------------------------------- 1 | .custom-anchor { 2 | stroke: #999; 3 | stroke-width: 1; 4 | fill: #d9d9d9; 5 | cursor: crosshair; 6 | rx: 3; 7 | ry: 3; 8 | } 9 | .custom-anchor:hover { 10 | fill: #ff7f0e; 11 | stroke: #ff7f0e; 12 | } 13 | .node-red-palette { 14 | width: 150px; 15 | background: #FFF; 16 | padding: 10px 0; 17 | opacity: 0.9; 18 | box-shadow: 0 0 10px rgba(0,0,0,0.2); 19 | } 20 | .beautify-chart .lf-grid > svg { 21 | background-image: url('../images/grid.svg'); 22 | background-size:50px 50px; 23 | background-repeat: repeat; 24 | } 25 | .beautify-chart .beautify-flow { 26 | background-image: url('../images/beautify.svg'); 27 | width: 20px; 28 | height: 20px; 29 | } 30 | -------------------------------------------------------------------------------- /src/components/beautifyElement/tools/Palette.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 75 | -------------------------------------------------------------------------------- /src/components/beautifyElement/tools/dagre.ts: -------------------------------------------------------------------------------- 1 | import { DagreLayout, DagreLayoutOptions } from '@antv/layout'; 2 | 3 | export default class Dagre { 4 | static pluginName = 'dagre'; 5 | lf: any; 6 | option: DagreLayoutOptions; 7 | render(lf) { 8 | this.lf = lf; 9 | } 10 | getBytesLength(word: string): number { 11 | if (!word) { 12 | return 0; 13 | } 14 | let totalLength = 0; 15 | for (let i = 0; i < word.length; i++) { 16 | const c = word.charCodeAt(i); 17 | if ((word.match(/[A-Z]/))) { 18 | totalLength += 1.5; 19 | } else if ((c >= 0x0001 && c <= 0x007e) || (c >= 0xff60 && c <= 0xff9f)) { 20 | totalLength += 1; 21 | } else { 22 | totalLength += 2; 23 | } 24 | } 25 | return totalLength; 26 | } 27 | /** 28 | * option: { 29 | * rankdir: "TB", // layout 方向, 可选 TB, BT, LR, RL 30 | * align: undefined, // 节点对齐方式,可选 UL, UR, DL, DR 31 | * nodeSize: undefined, // 节点大小 32 | * nodesepFunc: undefined, // 节点水平间距(px) 33 | * ranksepFunc: undefined, // 每一层节点之间间距 34 | * nodesep: 40, // 节点水平间距(px) 注意:如果有grid,需要保证nodesep为grid的偶数倍 35 | * ranksep: 40, // 每一层节点之间间距 注意:如果有grid,需要保证ranksep为grid的偶数倍 36 | * controlPoints: false, // 是否保留布局连线的控制点 37 | * radial: false, // 是否基于 dagre 进行辐射布局 38 | * focusNode: null, // radial 为 true 时生效,关注的节点 39 | * }; 40 | */ 41 | layout(option = {}) { 42 | const { nodes, edges, gridSize } = this.lf.graphModel; 43 | // 为了保证生成的节点在girdSize上,需要处理一下。 44 | let nodesep = 40; 45 | let ranksep = 40; 46 | if (gridSize > 20) { 47 | nodesep = gridSize * 2; 48 | ranksep = gridSize * 2; 49 | } 50 | this.option = { 51 | type: 'dagre', 52 | rankdir: 'LR', 53 | // align: 'UL', 54 | // align: 'UR', 55 | align: 'DR', 56 | nodesep, 57 | ranksep, 58 | begin: [120, 120], 59 | ...option, 60 | }; 61 | const layoutInstance = new DagreLayout(this.option); 62 | const layoutData = layoutInstance.layout({ 63 | nodes: nodes.map((node) => ({ 64 | id: node.id, 65 | size: { 66 | width: node.width, 67 | height: node.height, 68 | }, 69 | model: node, 70 | })), 71 | edges: edges.map((edge) => ({ 72 | source: edge.sourceNodeId, 73 | target: edge.targetNodeId, 74 | model: edge, 75 | })), 76 | }); 77 | const newGraphData = { 78 | nodes: [], 79 | edges: [], 80 | }; 81 | layoutData.nodes.forEach(node => { 82 | // @ts-ignore: pass node data 83 | const { model } = node; 84 | const data = model.getData(); 85 | // @ts-ignore: pass node data 86 | data.x = node.x; 87 | // @ts-ignore: pass node data 88 | data.y = node.y; 89 | newGraphData.nodes.push(data); 90 | }); 91 | layoutData.edges.forEach(edge => { 92 | // @ts-ignore: pass edge data 93 | const { model } = edge; 94 | const data = model.getData(); 95 | data.pointsList = this.calcPointsList(model, newGraphData.nodes); 96 | if (data.pointsList) { 97 | const first = data.pointsList[0]; 98 | const last = data.pointsList[data.pointsList.length - 1]; 99 | data.startPoint = { x: first.x, y: first.y }; 100 | data.endPoint = { x: last.x, y: last.y }; 101 | if (data.text && data.text.value) { 102 | data.text = { 103 | x: last.x - this.getBytesLength(data.text.value) * 6 - 10, 104 | y: last.y, 105 | value: data.text.value, 106 | }; 107 | } 108 | } else { 109 | data.startPoint = undefined; 110 | data.endPoint = undefined; 111 | if (data.text && data.text.value) { 112 | data.text = data.text.value; 113 | } 114 | } 115 | newGraphData.edges.push(data); 116 | }); 117 | this.lf.render(newGraphData); 118 | } 119 | pointFilter(points) { 120 | const allPoints = points; 121 | let i = 1; 122 | while (i < allPoints.length - 1) { 123 | const pre = allPoints[i - 1]; 124 | const current = allPoints[i]; 125 | const next = allPoints[i + 1]; 126 | if ((pre.x === current.x && current.x === next.x) 127 | || (pre.y === current.y && current.y === next.y)) { 128 | allPoints.splice(i, 1); 129 | } else { 130 | i++; 131 | } 132 | } 133 | return allPoints; 134 | } 135 | calcPointsList(model, nodes) { 136 | // 在节点确认从左向右后,通过计算来保证节点连线清晰。 137 | // TODO: 避障 138 | const pointsList = []; 139 | if (this.option.rankdir === 'LR' && model.modelType === 'polyline-edge') { 140 | const sourceNodeModel = this.lf.getNodeModelById(model.sourceNodeId); 141 | const targetNodeModel = this.lf.getNodeModelById(model.targetNodeId); 142 | const newSourceNodeData = nodes.find(node => node.id === model.sourceNodeId); 143 | const newTargetNodeData = nodes.find(node => node.id === model.targetNodeId); 144 | if (newSourceNodeData.x < newTargetNodeData.x) { 145 | pointsList.push({ 146 | x: newSourceNodeData.x + sourceNodeModel.width / 2, 147 | y: newSourceNodeData.y, 148 | }); 149 | pointsList.push({ 150 | x: newSourceNodeData.x + sourceNodeModel.width / 2 + (model.offset || 50), 151 | y: newSourceNodeData.y, 152 | }); 153 | pointsList.push({ 154 | x: newSourceNodeData.x + sourceNodeModel.width / 2 + (model.offset || 50), 155 | y: newTargetNodeData.y, 156 | }); 157 | pointsList.push({ 158 | x: newTargetNodeData.x - targetNodeModel.width / 2, 159 | y: newTargetNodeData.y, 160 | }); 161 | return this.pointFilter(pointsList); 162 | } 163 | // 向回连线 164 | if (newSourceNodeData.x > newTargetNodeData.x) { 165 | if (newSourceNodeData.y >= newTargetNodeData.y) { 166 | pointsList.push({ 167 | x: newSourceNodeData.x, 168 | y: newSourceNodeData.y + sourceNodeModel.height / 2, 169 | }); 170 | pointsList.push({ 171 | x: newSourceNodeData.x, 172 | y: newSourceNodeData.y + sourceNodeModel.height / 2 + (model.offset || 50), 173 | }); 174 | pointsList.push({ 175 | x: newTargetNodeData.x, 176 | y: newSourceNodeData.y + sourceNodeModel.height / 2 + (model.offset || 50), 177 | }); 178 | pointsList.push({ 179 | x: newTargetNodeData.x, 180 | y: newTargetNodeData.y + targetNodeModel.height / 2, 181 | }); 182 | } else { 183 | pointsList.push({ 184 | x: newSourceNodeData.x, 185 | y: newSourceNodeData.y - sourceNodeModel.height / 2, 186 | }); 187 | pointsList.push({ 188 | x: newSourceNodeData.x, 189 | y: newSourceNodeData.y - sourceNodeModel.height / 2 - (model.offset || 50), 190 | }); 191 | pointsList.push({ 192 | x: newTargetNodeData.x, 193 | y: newSourceNodeData.y - sourceNodeModel.height / 2 - (model.offset || 50), 194 | }); 195 | pointsList.push({ 196 | x: newTargetNodeData.x, 197 | y: newTargetNodeData.y - targetNodeModel.height / 2, 198 | }); 199 | } 200 | return this.pointFilter(pointsList); 201 | } 202 | } 203 | return undefined; 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/components/mindElement/index.ts: -------------------------------------------------------------------------------- 1 | import CenterNode from './nodes/CenterNode'; 2 | import SubNode from './nodes/SubNode'; 3 | import MindmapEdge from './nodes/MindmapEdge'; 4 | import { ContextPad } from './tools/menu' 5 | import './style.css' 6 | 7 | class LayoutFlowExtension { 8 | static pluginName = 'LayoutFlowExtension' 9 | constructor ({ lf }) { 10 | lf.register(CenterNode) 11 | lf.register(SubNode) 12 | lf.register(MindmapEdge) 13 | lf.extension.contextPad = new ContextPad({ lf }) 14 | } 15 | render(lf, domOverlay) { 16 | lf.extension.contextPad.render(lf, domOverlay) 17 | } 18 | } 19 | 20 | export default LayoutFlowExtension -------------------------------------------------------------------------------- /src/components/mindElement/nodes/CenterNode.ts: -------------------------------------------------------------------------------- 1 | import { HtmlNode, HtmlNodeModel } from "@logicflow/core"; 2 | import { createApp, ref, h } from 'vue'; 3 | import VueBaseNode from './CenterNode.vue'; 4 | import { getTextLengthByCanvas } from '../../util'; 5 | 6 | class CenterNode extends HtmlNode { 7 | isMounted: boolean; 8 | app: any; 9 | r: any; 10 | constructor (props) { 11 | super(props) 12 | this.isMounted = false 13 | this.r = h(this.getVueComponent(), this.getVueProps(props)) 14 | this.app = createApp({ 15 | render: () => this.r 16 | }) 17 | } 18 | getVueComponent () { 19 | return VueBaseNode 20 | } 21 | getVueProps (props) { 22 | return { 23 | properties: props.model.getProperties(), 24 | isSelected: props.model.isSelected, 25 | isEditing: props.model.state === 2, 26 | text: props.model.text.value, 27 | } 28 | } 29 | shouldUpdate() { 30 | const data = { 31 | ...this.props.model.properties, 32 | isSelected: this.props.model.isSelected, 33 | text: this.props.model.text.value 34 | } 35 | if (this.preProperties && this.preProperties === JSON.stringify(data)) return; 36 | this.preProperties = JSON.stringify(data); 37 | return true; 38 | } 39 | setHtml(rootEl) { 40 | if (!this.isMounted) { 41 | this.isMounted = true 42 | const node = document.createElement('div') 43 | node.className = 'mind-element_base-node' 44 | node.addEventListener('dblclick', () => { 45 | this.props.model.setElementState(2) 46 | this.r.component.props.isEditing = true 47 | }) 48 | rootEl.appendChild(node) 49 | this.app.mount(node) 50 | } else { 51 | const values = this.getVueProps(this.props) 52 | Object.keys(values).forEach((key) => { 53 | this.r.component.props[key] = values[key] 54 | }) 55 | } 56 | } 57 | getText () { 58 | return null 59 | } 60 | } 61 | 62 | class CenterNodeModel extends HtmlNodeModel { 63 | initNodeData (data) { 64 | super.initNodeData(data) 65 | this.isEditing = false 66 | this.fontSize = 24 67 | this.text.editable = false 68 | } 69 | setAttributes() { 70 | if (!this.text.value) { 71 | this.width = 100; 72 | this.height = 80; 73 | } else { 74 | const { width, height } = getTextLengthByCanvas(this.text.value, this.fontSize); 75 | this.width = width + 90 76 | this.height = height + 28 + 8 77 | } 78 | } 79 | 80 | getOutlineStyle() { 81 | const style = super.getOutlineStyle(); 82 | style.stroke = 'none'; 83 | style.hover.stroke = 'none'; 84 | return style; 85 | } 86 | getAnchorStyle(anchorInfo) { 87 | const style = super.getAnchorStyle(anchorInfo) 88 | style.stroke = 'none' 89 | style.fill = 'none' 90 | return style 91 | } 92 | // getDefaultAnchor() { 93 | // return [] 94 | // } 95 | updateText(value) { 96 | super.updateText(value) 97 | this.setAttributes() 98 | } 99 | } 100 | 101 | export default { 102 | type: 'center-node', 103 | model: CenterNodeModel, 104 | view: CenterNode 105 | } 106 | -------------------------------------------------------------------------------- /src/components/mindElement/nodes/CenterNode.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 27 | 29 | -------------------------------------------------------------------------------- /src/components/mindElement/nodes/MindmapEdge.ts: -------------------------------------------------------------------------------- 1 | import { BezierEdge, BezierEdgeModel } from "@logicflow/core" 2 | 3 | class MindmapEdge extends BezierEdge { 4 | getArrow () { 5 | return null 6 | } 7 | } 8 | 9 | class MindmapEdgeModel extends BezierEdgeModel { 10 | getEdgeStyle () { 11 | const style = super.getEdgeStyle() 12 | style.stroke = 'rgb(67, 169, 255)' 13 | style.strokeWidth = 2 14 | return style 15 | } 16 | } 17 | 18 | export default { 19 | type: "mindmap-edge", 20 | model: MindmapEdgeModel, 21 | view: MindmapEdge 22 | } 23 | -------------------------------------------------------------------------------- /src/components/mindElement/nodes/SubNode.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 子节点为线条样式 3 | */ 4 | import CenterNode from './CenterNode' 5 | import SubNodeComponent from './SubNode.vue' 6 | import { getTextLengthByCanvas } from '../../util'; 7 | 8 | class SubNode extends CenterNode.view { 9 | getVueComponent () { 10 | return SubNodeComponent 11 | } 12 | setHtml(rootEl) { 13 | if (!this.isMounted) { 14 | this.isMounted = true 15 | const node = document.createElement('div') 16 | node.className = 'mind-element_base-node mind-element_sub-node' 17 | node.addEventListener('dblclick', () => { 18 | this.props.model.setElementState(2) 19 | this.r.component.props.isEditing = true 20 | }) 21 | rootEl.appendChild(node) 22 | this.app.mount(node) 23 | } else { 24 | const values = this.getVueProps(this.props) 25 | Object.keys(values).forEach((key) => { 26 | this.r.component.props[key] = values[key] 27 | }) 28 | } 29 | } 30 | } 31 | 32 | class SubNodeModel extends CenterNode.model { 33 | initNodeData (data) { 34 | super.initNodeData(data) 35 | this.fontSize = 16 36 | } 37 | setAttributes() { 38 | if (!this.text.value) { 39 | this.width = 100; 40 | this.height = 40; 41 | } else { 42 | const { width, height } = getTextLengthByCanvas(this.text.value, this.fontSize); 43 | this.width = width + 60 44 | this.height = height + 10 + 8 45 | } 46 | } 47 | } 48 | 49 | export default { 50 | type: "sub-node", 51 | model: SubNodeModel, 52 | view: SubNode 53 | } 54 | -------------------------------------------------------------------------------- /src/components/mindElement/nodes/SubNode.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 27 | 32 | -------------------------------------------------------------------------------- /src/components/mindElement/style.css: -------------------------------------------------------------------------------- 1 | .mind-element_base-node { 2 | text-align: left; 3 | font-family: 微软雅黑; 4 | background-color: rgb(255, 255, 255); 5 | color: rgb(42, 46, 54); 6 | font-size: 24px; 7 | border-radius: 7px; 8 | border: 2px solid transparent; 9 | box-shadow: rgb(0 0 0 / 10%) 0px 2px 4px 0px; 10 | padding: 14px 38px; 11 | font-style: normal; 12 | font-weight: normal; 13 | width: calc(100% - 2px); 14 | height: calc(100% - 4px); 15 | margin: 2px; 16 | box-sizing: border-box; 17 | text-align: center; 18 | line-height: 1.5; 19 | } 20 | .lf-node-selected .mind-element_base-node { 21 | border: 2px solid #1890ff; 22 | } 23 | .menu-wrapper { 24 | position: absolute; 25 | } 26 | .mind-element_sub-node { 27 | font-size: 16px; 28 | border-radius: 7px; 29 | padding: 5px 10px; 30 | background-color: rgb(204, 229, 255); 31 | } 32 | -------------------------------------------------------------------------------- /src/components/mindElement/tools/layout.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 将树这种数据格式转换为图 3 | */ 4 | import Hierarchy from '@antv/hierarchy'; 5 | const FAKER_NODE = 'faker-node'; 6 | const ROOT_NODE = 'center-node'; 7 | const FIRST_ROOT_X = 10; 8 | const FIRST_ROOT_Y = 10; 9 | 10 | export const treeToGraph = (rootNode) => { 11 | const nodes = []; 12 | const edges = []; 13 | function getNode(current, parent = null) { 14 | const node = { 15 | ...current 16 | }; 17 | nodes.push(node); 18 | if (current.children) { 19 | current.children.forEach(subNode => { 20 | getNode(subNode, node); 21 | }); 22 | } 23 | if (parent) { 24 | const edge = { 25 | sourceNodeId: parent.id, 26 | targetNodeId: node.id, 27 | type: 'mindmap-edge', 28 | }; 29 | edges.push(edge); 30 | } 31 | } 32 | getNode(rootNode); 33 | return { 34 | nodes, 35 | edges, 36 | }; 37 | } 38 | /** 39 | * 由于树这种数据格式本身是没有坐标的 40 | * 需要使用一些算法来将树转换为有坐标的树 41 | */ 42 | export const layoutTree = (tree) => { 43 | if (!tree || !tree.children || tree.children.length === 0) return tree; 44 | const NODE_SIZE = 40; 45 | const PEM = 20; 46 | tree.isRoot = true; 47 | const rootNode = Hierarchy.compactBox(tree, { 48 | direction: 'LR', 49 | getId(d) { 50 | return d.id; 51 | }, 52 | getHeight(d) { 53 | if (d.type === ROOT_NODE) { 54 | return NODE_SIZE * 4; 55 | } 56 | return NODE_SIZE; 57 | }, 58 | getWidth() { 59 | return 200 + PEM * 1.6; 60 | }, 61 | getHGap() { 62 | return PEM; 63 | }, 64 | getVGap() { 65 | return PEM; 66 | }, 67 | getSubTreeSep(d) { 68 | if (!d.children || !d.children.length) { 69 | return 0; 70 | } 71 | return PEM; 72 | }, 73 | }); 74 | const x = tree.x || FIRST_ROOT_X; 75 | const y = tree.y || FIRST_ROOT_Y; 76 | const x1 = rootNode.x; 77 | const y1 = rootNode.y; 78 | const moveX = x - x1; 79 | const moveY = y - y1; 80 | const newTree = dfsTree(rootNode, currentNode => { 81 | return { 82 | id: currentNode.id, 83 | text: currentNode.data.text.value, 84 | properties: currentNode.data.properties, 85 | type: currentNode.data.type, 86 | x: currentNode.x + moveX, 87 | y: currentNode.y + moveY, 88 | } 89 | }); 90 | return newTree; 91 | } 92 | /** 93 | * 遍历树的每一项,已传入的回调方法重新构建一个新的树 94 | */ 95 | export const dfsTree = (tree, callback) => { 96 | const newTree = callback(tree); 97 | if (tree.children && tree.children.length > 0) { 98 | newTree.children = tree.children.map(treeNode => dfsTree(treeNode, callback)); 99 | } 100 | return newTree; 101 | } 102 | 103 | export const graphToTree = (graphData) => { 104 | let tree = null; 105 | const nodesMap = new Map(); 106 | graphData.nodes.forEach(node => { 107 | const treeNode = { 108 | ...node, 109 | children: [], 110 | }; 111 | nodesMap.set(node.id, treeNode); 112 | if (node.type === ROOT_NODE) { 113 | tree = treeNode; 114 | } 115 | }); 116 | graphData.edges.forEach(edge => { 117 | const node = nodesMap.get(edge.sourceNodeId); 118 | node.children.push(nodesMap.get(edge.targetNodeId)); 119 | }); 120 | return tree; 121 | } 122 | 123 | export const layoutGraphData = (graphData) => { 124 | const tree = graphToTree(graphData); 125 | const newTree = layoutTree(tree); 126 | return treeToGraph(newTree); 127 | } 128 | -------------------------------------------------------------------------------- /src/components/mindElement/tools/menu.ts: -------------------------------------------------------------------------------- 1 | import type { LogicFlow } from '@logicflow/core' 2 | import { createApp, ref, h } from 'vue' 3 | import { layoutGraphData } from './layout' 4 | import VueMenu from './menu.vue' 5 | const COMMON_TYPE_KEY = "menu-common" 6 | const NEXT_X_DISTANCE = 200 7 | const NEXT_Y_DISTANCE = 100 8 | 9 | class ContextPad { 10 | static pluginName = 'contextPad' 11 | lf: LogicFlow 12 | menuTypeMap: Record 13 | container: HTMLElement 14 | isShow: any 15 | private _activeData: any 16 | private menuComponent: any 17 | app: any 18 | menuWrapper: any 19 | constructor({ lf }) { 20 | this.menuTypeMap = new Map() 21 | this.lf = lf 22 | this.menuTypeMap.set(COMMON_TYPE_KEY, []) 23 | this.menuComponent = h(VueMenu, { 24 | onAddSubNode: () => { 25 | this.addSubNode() 26 | }, 27 | }) 28 | this.app = createApp({ 29 | render: () => this.menuComponent 30 | }) 31 | this.lf.keyboard.on('tab', () => { 32 | const { nodes } = this.lf.getSelectElements() 33 | if (nodes.length > 0) { 34 | this._activeData = nodes[0] 35 | } 36 | if (this._activeData) { 37 | this.addSubNode() 38 | } 39 | }) 40 | this.lf.keyboard.on('enter', () => { 41 | const { nodes } = this.lf.getSelectElements() 42 | if (nodes.length > 0) { 43 | const { edges } = this.lf.graphModel 44 | const edge = edges.find(edge => edge.targetNodeId === nodes[0].id) 45 | if (edge) { 46 | this._activeData = this.lf.getNodeDataById(edge.sourceNodeId) 47 | } 48 | } 49 | if (this._activeData) { 50 | this.addSubNode() 51 | } 52 | }) 53 | } 54 | render(lf, container) { 55 | this.container = container 56 | lf.on("node:contextmenu", ({ data }) => { 57 | this._activeData = data 58 | this.showMenu() 59 | }) 60 | } 61 | setContextMenuByType = (type, menus) => { 62 | this.menuTypeMap.set(type, menus) 63 | } 64 | 65 | /** 66 | * 获取新菜单位置 67 | */ 68 | getContextMenuPosition() { 69 | const data = this._activeData 70 | const Model = this.lf.graphModel.getElement(data.id) 71 | if (!Model) { 72 | console.warn(`找不到元素${data.id}`) 73 | return 74 | } 75 | let x 76 | let y 77 | if (Model.BaseType === "edge") { 78 | x = Number.MIN_SAFE_INTEGER 79 | y = Number.MAX_SAFE_INTEGER 80 | const edgeData = Model.getData() 81 | x = Math.max(edgeData.startPoint.x, x) 82 | y = Math.min(edgeData.startPoint.y, y) 83 | x = Math.max(edgeData.endPoint.x, x) 84 | y = Math.min(edgeData.endPoint.y, y) 85 | if (edgeData.pointsList) { 86 | edgeData.pointsList.forEach((point) => { 87 | x = Math.max(point.x, x) 88 | y = Math.min(point.y, y) 89 | }) 90 | } 91 | } 92 | if (Model.BaseType === "node") { 93 | x = data.x + Model.width / 2 94 | y = data.y - Model.height / 2 95 | } 96 | return this.lf.graphModel.transformModel.CanvasPointToHtmlPoint([x, y]) 97 | } 98 | showMenu() { 99 | const { isSilentMode } = this.lf.options 100 | // 静默模式不显示菜单 101 | if (isSilentMode) { 102 | return 103 | } 104 | if (!this.menuWrapper) { 105 | this.isShow = true 106 | const node = document.createElement('div') 107 | node.className = 'menu-wrapper' 108 | this.container.appendChild(node) 109 | this.app.mount(node) 110 | this.menuWrapper = node 111 | } 112 | const [x, y] = this.getContextMenuPosition() 113 | this.menuWrapper.style.display = 'block' 114 | this.menuWrapper.style.left = `${x}px` 115 | this.menuWrapper.style.top = `${y}px` 116 | this.lf.on( 117 | "node:delete,blank:click,edge:delete,node:drag,graph:transform", 118 | this.listenDelete 119 | ) 120 | } 121 | /** 122 | * 隐藏菜单 123 | */ 124 | hideContextMenu() { 125 | this.lf.off( 126 | "node:delete,blank:click,edge:delete,node:drag,graph:transform", 127 | this.listenDelete 128 | ) 129 | if (this.menuWrapper) { 130 | this.menuWrapper.style.display = 'none' 131 | } 132 | } 133 | addSubNode() { 134 | this.hideContextMenu() 135 | const node = this.lf.addNode({ 136 | type: 'sub-node', 137 | x: this._activeData.x + NEXT_X_DISTANCE, 138 | y: this._activeData.y, 139 | text: '子主题', 140 | }) 141 | this.lf.addEdge({ 142 | type: 'bezier', 143 | sourceNodeId: this._activeData.id, 144 | targetNodeId: node.id, 145 | }) 146 | const graphData = this.lf.getGraphData() 147 | const { nodes, edges } = layoutGraphData(graphData) 148 | const nodeIdMap = nodes.reduce((acc, cur) => { 149 | acc[cur.id] = cur 150 | return acc 151 | }, {}) 152 | // 处理edge弧线,保持其美观 153 | edges.map((edge) => { 154 | const { 155 | sourceNodeId, 156 | targetNodeId 157 | } = edge 158 | // const sModel = this.lf.getNodeModelById(sourceNodeId) 159 | const tModel = this.lf.getNodeModelById(targetNodeId) 160 | const startPoint = { 161 | x: nodeIdMap[sourceNodeId].x, 162 | y: nodeIdMap[sourceNodeId].y 163 | } 164 | const sJustPoint = { 165 | x: nodeIdMap[targetNodeId].x - tModel.width / 2 - 100, 166 | y: nodeIdMap[targetNodeId].y 167 | } 168 | const tJustPoint = { 169 | x: nodeIdMap[targetNodeId].x - tModel.width / 2 - 100, 170 | y: nodeIdMap[targetNodeId].y 171 | } 172 | const endPoint = { 173 | x: nodeIdMap[targetNodeId].x - tModel.width / 2, 174 | y: nodeIdMap[targetNodeId].y 175 | } 176 | edge.startPoint = startPoint 177 | edge.endPoint = endPoint 178 | edge.pointsList = [ 179 | startPoint, 180 | sJustPoint, 181 | tJustPoint, 182 | endPoint 183 | ] 184 | }) 185 | this.lf.render({ 186 | nodes, 187 | edges, 188 | }) 189 | this.lf.selectElementById(node.id) 190 | // this.lf.getNodeModelById(node.id).setElementState(2) 191 | } 192 | listenDelete = () => { 193 | this.hideContextMenu() 194 | } 195 | } 196 | 197 | export { ContextPad } 198 | -------------------------------------------------------------------------------- /src/components/mindElement/tools/menu.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 41 | -------------------------------------------------------------------------------- /src/components/util.ts: -------------------------------------------------------------------------------- 1 | export const getNodeSizeByText = (text, fontSize) => { 2 | const rows = String(text).split(/[\r\n]/g); 3 | let longestBytes = 0; 4 | rows && rows.forEach(item => { 5 | const rowByteLength = getBytesLength(item); 6 | longestBytes = rowByteLength > longestBytes ? rowByteLength : longestBytes; 7 | }); 8 | // 背景框宽度,最长一行字节数/2 * fontsize + 2 9 | // 背景框宽度, 行数 * fontsize + 2 10 | return { 11 | width: Math.ceil(longestBytes / 2) * fontSize + fontSize / 4, 12 | height: rows.length * (fontSize + 2) + fontSize / 4, 13 | }; 14 | } 15 | 16 | export const getBytesLength = (word) => { 17 | if (!word) { 18 | return 0; 19 | } 20 | let totalLength = 0; 21 | for (let i = 0; i < word.length; i++) { 22 | const c = word.charCodeAt(i); 23 | console.log(c) 24 | if ((word.match(/[A-Z]/))) { 25 | totalLength += 1.5; 26 | } else if (word === 49) { 27 | totalLength += 0.5 28 | console.log('word', word) 29 | } else if ((c >= 0x0001 && c <= 0x007e) || (c >= 0xff60 && c <= 0xff9f)) { 30 | totalLength += 1.2; 31 | } else { 32 | totalLength += 2; 33 | } 34 | } 35 | return totalLength; 36 | }; 37 | 38 | function getCssStyle(prop) { 39 | return window.getComputedStyle(document.body, null).getPropertyValue(prop); 40 | } 41 | 42 | function getCanvasFont(fs) { 43 | const fontSize = fs + 'px' || getCssStyle('font-size'); 44 | const fontFamily = getCssStyle('font-family'); 45 | return `${fontSize} ${fontFamily}`; 46 | } 47 | 48 | export const getTextLengthByCanvas = (text, fontSize) => { 49 | const canvas = document.createElement('canvas'); 50 | const context = canvas.getContext('2d'); 51 | context.font = getCanvasFont(fontSize); 52 | const rows = String(text).split(/[\r\n]/g); 53 | let maxWidth = 0; 54 | rows && rows.forEach(item => { 55 | context.clearRect(0, 0, canvas.width, canvas.height); 56 | const { width } = context.measureText(item) 57 | maxWidth = maxWidth > width ? maxWidth : width; 58 | }) 59 | return { 60 | width: maxWidth, 61 | height: rows.length * (fontSize) * 1.5, 62 | }; 63 | } 64 | 65 | export const pointFilter = (points) => { 66 | const allPoints = points; 67 | let i = 1; 68 | while (i < allPoints.length - 1) { 69 | const pre = allPoints[i - 1]; 70 | const current = allPoints[i]; 71 | const next = allPoints[i + 1]; 72 | if ((pre.x === current.x && current.x === next.x) 73 | || (pre.y === current.y && current.y === next.y)) { 74 | allPoints.splice(i, 1); 75 | } else { 76 | i++; 77 | } 78 | } 79 | return allPoints; 80 | }; 81 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import ElementPlus from 'element-plus' 3 | import 'element-plus/dist/index.css' 4 | import './style.css' 5 | import App from './App.vue' 6 | 7 | const app = createApp(App) 8 | app.use(ElementPlus) 9 | app.mount('#app') 10 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-synthesis: none; 6 | text-rendering: optimizeLegibility; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | -webkit-text-size-adjust: 100%; 10 | } 11 | 12 | a { 13 | font-weight: 500; 14 | color: #646cff; 15 | text-decoration: inherit; 16 | } 17 | a:hover { 18 | color: #535bf2; 19 | } 20 | 21 | html, body, .app-container { 22 | padding: 0; 23 | margin: 0; 24 | width: 100%; 25 | height: 100%; 26 | } 27 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue' 5 | const component: DefineComponent<{}, {}, any> 6 | export default component 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "allowJs": false, 5 | "allowSyntheticDefaultImports": true, 6 | "downlevelIteration": true, 7 | "noImplicitAny": false, 8 | "module": "es2020", 9 | "moduleResolution": "node", 10 | "skipLibCheck": true, 11 | "target": "es2015", 12 | "maxNodeModuleJsDepth": 5, 13 | }, 14 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 15 | "references": [{ "path": "./tsconfig.node.json" }] 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | base: '', 7 | plugins: [vue()] 8 | }) 9 | --------------------------------------------------------------------------------