45 changed files with 1070 additions and 285 deletions
			
			
		| @ -1,78 +0,0 @@ | |||
| # iMove | |||
| 
 | |||
| <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section --> | |||
| [](#contributors-) | |||
| <!-- ALL-CONTRIBUTORS-BADGE:END --> | |||
| 
 | |||
| <div align="center"> | |||
|   <img src="https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=3874775950,1064987171&fm=26&gp=0.jpg" width="200px"> | |||
| </div> | |||
| 
 | |||
| <p align="center"> | |||
|   iMove 是一个逻辑可复用的,面向函数的,流程可视化的 JavaScript 工具库。 | |||
| </p> | |||
| 
 | |||
| [English](./README.en-US.md) | 简体中文 | |||
| 
 | |||
| ## 特性 | |||
| 
 | |||
| - [x] **流程可视化**: 上手简单,绘图方便,逻辑表达更直观,易于理解 | |||
| - [x] **逻辑复用**: iMove 节点支持复用,单节点支持参数配置 | |||
| - [x] **灵活可扩展**: 仅需写一个函数,节点可扩展,支持插件集成 | |||
| - [ ] **多语言编译**: 无语言编译出码限制(例: 支持 JavaScript, Java 编译出码) | |||
| 
 | |||
| ## 快速开始 | |||
| 
 | |||
| ### 步骤 1. 准备 | |||
| 
 | |||
| 下载仓库,安装并启动 | |||
| 
 | |||
| ```bash | |||
| $ git clone https://github.com/imgcook/imove.git | |||
| $ cd ./imove/example | |||
| $ npm install | |||
| $ npm start | |||
| ``` | |||
| 
 | |||
| ### 步骤 2. 绘制流程图 | |||
| 
 | |||
| 从左侧拖动节点至中央画布,绘制流程图 | |||
| 
 | |||
|  | |||
| 
 | |||
| ### 步骤 3. 配置节点 | |||
| 
 | |||
| 选择节点,修改节点名,编辑节点代码 | |||
| 
 | |||
|  | |||
| 
 | |||
|  | |||
| 
 | |||
| ## 贡献 | |||
| 
 | |||
| 1. Fork 仓库 | |||
| 2. 创建分支 (`git checkout -b my-new-feature`) | |||
| 3. 提交修改 (`git commit -am 'Add some feature'`) | |||
| 4. 推送 (`git push origin my-new-feature`) | |||
| 5. 创建 PR | |||
| 
 | |||
| ## 欢迎fork和反馈 | |||
| 
 | |||
| 如有建议或意见,欢迎在 github [issues](https://github.com/imgcook/imove/issues) 区提问 | |||
| 
 | |||
| ## 协议 | |||
| 
 | |||
| 本仓库遵循 [MIT 协议](http://www.opensource.org/licenses/MIT) | |||
| 
 | |||
| ## 贡献者 ✨ | |||
| 
 | |||
| 感谢所有贡献的人 ([emoji key](https://allcontributors.org/docs/en/emoji-key)): | |||
| 
 | |||
| <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --> | |||
| <!-- prettier-ignore-start --> | |||
| <!-- markdownlint-disable --> | |||
| <!-- markdownlint-restore --> | |||
| <!-- prettier-ignore-end --> | |||
| <!-- ALL-CONTRIBUTORS-LIST:END --> | |||
| 
 | |||
| 本仓库遵循 [all-contributors](https://github.com/all-contributors/all-contributors) 规范,欢迎贡献! | |||
| @ -0,0 +1 @@ | |||
| {} | |||
| @ -1,28 +0,0 @@ | |||
| /// <reference types="cypress" />
 | |||
| 
 | |||
| context('Actions', () => { | |||
|     // 1. 打开http://localhost:8001/
 | |||
|     beforeEach(() => { | |||
|         cy.visit('http://localhost:8001/') | |||
|     }) | |||
| 
 | |||
|     // 2. 测试是否能将左侧的组件拖动到画布中
 | |||
|     it('the components on the left can be dragged into the canvas', () => { | |||
| 
 | |||
|     }) | |||
| 
 | |||
|     // 3. 测试是否可修改节点的显示名称、逻辑触发名称、代码、投放配置schema、依赖
 | |||
|     it('you can modify the display name, logical trigger name, code, delivery configuration schema, and dependencies of the node', () => { | |||
| 
 | |||
|     }) | |||
| 
 | |||
|     // 4. 测试能够run起来一个最小流程(写好代码)
 | |||
|     it('A minimal process can be run', () => { | |||
| 
 | |||
|     }) | |||
| 
 | |||
|     // 5. 测试更改节点位置,输出结果不变
 | |||
|     it('ensure that the node position is changed and the output result remains unchanged', () => { | |||
| 
 | |||
|     }) | |||
| }) | |||
| @ -0,0 +1,45 @@ | |||
| /// <reference types="cypress" />
 | |||
| 
 | |||
| context('Actions', () => { | |||
|   beforeEach(() => { | |||
|     cy.visit('http://localhost:8000/') | |||
|     cy.get('div.ant-modal-confirm-btns > button:nth-child(1)').click({ force: true }) | |||
|   }) | |||
| 
 | |||
|   function moveNodeAndSelect() { | |||
|     cy.get('svg > g > g.x6-graph-svg-stage > g:nth-child(1)').eq(0) | |||
|       .trigger('mousedown', { which: 1, force: true }) | |||
|       .trigger('mousemove', { clientX: 400, clientY: 300, force: true }) | |||
|       .trigger('mouseup', { force: true }) | |||
|     cy.get('svg > g > g.x6-graph-svg-stage > g:nth-child(1)').eq(1).click({ force: true }) // 点击移动到画布中的节点
 | |||
|   } | |||
| 
 | |||
|   function assertEdit(nth, content) { | |||
|     moveNodeAndSelect() | |||
|     const ele = cy.get(`#rc-tabs-0-panel-basic > div > div:nth-child(${nth}) > button`).eq(0) | |||
|     ele.should('have.text', '编 辑') | |||
|     ele.click({ force: true }) | |||
|     cy.get('.ant-modal-content').should('exist') | |||
|     cy.get('.ace_content').type(content) | |||
|     cy.get('button.ant-btn.ant-btn-primary').eq(0).click({ force: true }) | |||
|     ele.click({ force: true }) | |||
|     cy.get('.ace_content').should('contain', content) | |||
|   } | |||
| 
 | |||
|   it('修改节点名称', () => { | |||
|     moveNodeAndSelect() | |||
|     const ele = cy.get('#rc-tabs-0-panel-basic > div > div:nth-child(1) > input').eq(0) | |||
|     ele.clear({ force: true }) | |||
|     ele.type('新的开始') | |||
|     ele.should('have.value', '新的开始') | |||
|   }) | |||
|   it('修改节点代码', () => { | |||
|     assertEdit(2, 'console.log(1)') | |||
|   }) | |||
|   it('修改投放配置schema', () => { | |||
|     assertEdit(3, `"a":1`) | |||
|   }) | |||
|   it('修改npm依赖', () => { | |||
|     assertEdit(4, `"@ant-design/icons": "^4.0.6"`) | |||
|   }) | |||
| }) | |||
| @ -1,28 +0,0 @@ | |||
| /// <reference types="cypress" />
 | |||
| 
 | |||
| context('Actions', () => { | |||
|     // 1. 打开http://localhost:8001/
 | |||
|     beforeEach(() => { | |||
|         cy.visit('http://localhost:8001/') | |||
|     }) | |||
| 
 | |||
|     // 2. 测试是否能将左侧的组件拖动到画布中
 | |||
|     it('the components on the left can be dragged into the canvas', () => { | |||
| 
 | |||
|     }) | |||
| 
 | |||
|     // 3. 测试是否可修改节点的显示名称、逻辑触发名称、代码、投放配置schema、依赖
 | |||
|     it('you can modify the display name, logical trigger name, code, delivery configuration schema, and dependencies of the node', () => { | |||
| 
 | |||
|     }) | |||
| 
 | |||
|     // 4. 测试能够run起来一个最小流程(写好代码)
 | |||
|     it('A minimal process can be run', () => { | |||
| 
 | |||
|     }) | |||
| 
 | |||
|     // 5. 测试更改节点位置,输出结果不变
 | |||
|     it('ensure that the node position is changed and the output result remains unchanged', () => { | |||
| 
 | |||
|     }) | |||
| }) | |||
| @ -0,0 +1,42 @@ | |||
| /// <reference types="cypress" />
 | |||
| 
 | |||
| context('Actions', () => { | |||
|   beforeEach(() => { | |||
|     cy.visit('http://localhost:8000/') | |||
|     cy.get('div.ant-modal-confirm-btns > button:nth-child(1)').click({ force: true }) | |||
|   }) | |||
| 
 | |||
|   function moveNode(selector, x, y) { | |||
|     cy.get(selector).eq(0) | |||
|       .trigger('mousedown', { which: 1, force: true }) | |||
|       .trigger('mousemove', { clientX: x, clientY: y, force: true }) | |||
|       .trigger('mouseup', { force: true }) | |||
|   } | |||
| 
 | |||
|   // 节点/边操作测试
 | |||
|   it('添加 开始/分支/处理 节点', () => { | |||
|     const DEFAULT_COUNT = 1 | |||
|     moveNode('svg > g > g.x6-graph-svg-stage > g:nth-child(1) > circle', 300, 150) | |||
|     moveNode('svg > g > g.x6-graph-svg-stage > g:nth-child(2) > polygon', 400, 150) | |||
|     moveNode('svg > g > g.x6-graph-svg-stage > g:nth-child(3) > rect', 500, 150) | |||
|     cy.get('svg> g > g.x6-graph-svg-stage circle').its('length').should('be.gt', DEFAULT_COUNT) | |||
|     cy.get('svg> g > g.x6-graph-svg-stage polygon').its('length').should('be.gt', DEFAULT_COUNT) | |||
|     cy.get('svg> g > g.x6-graph-svg-stage rect').its('length').should('be.gt', DEFAULT_COUNT) | |||
|   }) | |||
| 
 | |||
|   it('边连线 & 删除节点/边', () => { | |||
|     moveNode('svg > g > g.x6-graph-svg-stage > g:nth-child(1) > circle', 300, 150) | |||
|     moveNode('svg > g > g.x6-graph-svg-stage > g:nth-child(2) > polygon', 400, 150) | |||
|     moveNode('svg > g > g.x6-graph-svg-stage > g:nth-child(3) > rect', 500, 150) | |||
|     const node1 = cy.get('svg > g > g.x6-graph-svg-stage > g:nth-child(1)').eq(1) | |||
|     const node2 = cy.get('svg > g > g.x6-graph-svg-stage > g:nth-child(2)').eq(1) | |||
|     const node3 = cy.get('svg > g > g.x6-graph-svg-stage > g:nth-child(3)').eq(1) | |||
|     node1.click('right', { force: true }).trigger('mousemove', { clientX: node2.clientX, clientY: node2.clientY, force: true }).trigger('mouseup', { force: true }) | |||
|     node2.click('right', { force: true }).trigger('mousemove', { clientX: node3.clientX, clientY: node3.clientY, force: true }).trigger('mouseup', { force: true }) | |||
|     cy.get('svg path:nth-child(1)').should('exist') | |||
|     cy.get('svg path:nth-child(1)').eq(1).click({ force: true }) | |||
|     cy.get('body').type('{del}') | |||
|     cy.get('svg path:nth-child(1)').eq(1).click({ force: true }) | |||
|     cy.get('body').type('{del}') | |||
|   }) | |||
| }) | |||
| @ -0,0 +1,100 @@ | |||
| /// <reference types="cypress" />
 | |||
| 
 | |||
| // 工具栏测试
 | |||
| context('Actions', () => { | |||
|   const toolSelector = (i, j, k) => { | |||
|     if (k) { | |||
|       return `.index-module_container__1D841 > :nth-child(${i}) > :nth-child(${j})> :nth-child(${k})` | |||
|     } else { | |||
|       return `.index-module_container__1D841 > :nth-child(${i}) > :nth-child(${j})` | |||
|     } | |||
|   } | |||
|   beforeEach(() => { | |||
|     cy.visit('http://localhost:8000/') | |||
|     cy.get('div.ant-modal-confirm-btns > button:nth-child(1)').click({ force: true }) | |||
|   }) | |||
| 
 | |||
|   function moveNode(selector, x, y) { | |||
|     cy.get(selector).eq(0) | |||
|       .trigger('mousedown', { which: 1, force: true }) | |||
|       .trigger('mousemove', { clientX: x, clientY: y, force: true }) | |||
|       .trigger('mouseup', { force: true }) | |||
|   } | |||
| 
 | |||
|   // 第一组
 | |||
|   it('保存', () => { | |||
|     const save = cy.get(toolSelector(1, 1)) | |||
|   }) | |||
|   it('适配窗口', () => { | |||
|     const fitWindow = cy.get(toolSelector(1, 2)) | |||
|   }) | |||
|   it('撤销', () => { | |||
|     const undo = cy.get(toolSelector(1, 3)) | |||
|     const node = 'svg > g > g.x6-graph-svg-stage > g:nth-child(1)' | |||
|     moveNode(node, 350, 300) | |||
|     moveNode(node, 350, 300) | |||
|     undo.click({ multiple: true, force: true }) | |||
|     undo.click({ multiple: true, force: true }) | |||
|     cy.get(node).should('have.length', 2) | |||
|   }) | |||
|   it('重做', () => { | |||
|     const redo = cy.get(toolSelector(1, 4)) | |||
|     const node = 'svg > g > g.x6-graph-svg-stage > g:nth-child(1)' | |||
|     moveNode(node, 350, 300) | |||
|     redo.click() | |||
|     redo.click() | |||
|     cy.get(node).should('have.length', 2) | |||
|   }) | |||
| 
 | |||
|   // 第二组
 | |||
|   it('缩小', () => { | |||
|     const zoomOut = cy.get(toolSelector(2, 1, 1)) | |||
|   }) | |||
|   it('放大', () => { | |||
|     const zoomIn = cy.get(toolSelector(2, 1, 2)) | |||
|   }) | |||
| 
 | |||
|   // 第三组
 | |||
|   it('修改字号', () => { | |||
|     const fontSize = cy.get(toolSelector(3, 1)) | |||
|   }) | |||
|   it('修改字重', () => { | |||
|     const fontWeight = cy.get(toolSelector(3, 2)) | |||
|   }) | |||
|   it('修改斜体', () => { | |||
|     const italic = cy.get(toolSelector(3, 3)) | |||
|   }) | |||
|   it('修改下划线', () => { | |||
|     const underline = cy.get(toolSelector(3, 4)) | |||
|   }) | |||
| 
 | |||
|   // 第四组
 | |||
|   it('修改文字颜色', () => { | |||
|     const textColor = cy.get(toolSelector(4, 1)) | |||
|   }) | |||
|   it('修改背景颜色', () => { | |||
|     const bgColor = cy.get(toolSelector(4, 2)) | |||
|   }) | |||
|   it('修改边框颜色', () => { | |||
|     const borderColor = cy.get(toolSelector(4, 3)) | |||
|   }) | |||
|   it('修改线条样式', () => { | |||
|     const lineStyle = cy.get(toolSelector(4, 4)) | |||
|   }) | |||
| 
 | |||
|   // 第五组
 | |||
|   it('修改水平方向对齐', () => { | |||
|     const align = cy.get(toolSelector(5, 1)) | |||
|   }) | |||
|   it('修改垂直方向对齐', () => { | |||
|     const vertical = cy.get(toolSelector(5, 2)) | |||
|   }) | |||
| 
 | |||
|   // 第六组
 | |||
|   it('修改层级置顶', () => { | |||
|     const top = cy.get(toolSelector(6, 1)) | |||
|   }) | |||
|   it('修改层级置底', () => { | |||
|     const bottom = cy.get(toolSelector(6, 2)) | |||
|   }) | |||
| }) | |||
| @ -1,15 +0,0 @@ | |||
| # umi project | |||
| 
 | |||
| ## Getting Started | |||
| 
 | |||
| Install dependencies, | |||
| 
 | |||
| ```bash | |||
| $ yarn | |||
| ``` | |||
| 
 | |||
| Start the dev server, | |||
| 
 | |||
| ```bash | |||
| $ yarn start | |||
| ``` | |||
| @ -0,0 +1,41 @@ | |||
| const path = require('path'); | |||
| const low = require('lowdb'); | |||
| const FileSync = require('lowdb/adapters/FileSync'); | |||
| 
 | |||
| const adapter = new FileSync(path.join(__dirname, './db.json')); | |||
| const db = low(adapter); | |||
| 
 | |||
| const queryGraph = (req: any, res: any) => { | |||
|   const { projectId = 'default' } = req.body; | |||
|   const data = db.get(projectId).value() || []; | |||
|   res.send({ status: 200, code: 0, success: true, data: { cells: data } }); | |||
| }; | |||
| 
 | |||
| const modifyGraph = (req: any, res: any) => { | |||
|   const { projectId = 'default', actions = [] } = req.body; | |||
|   const projectData = db.get(projectId).value() || []; | |||
|   actions.forEach((action: any) => { | |||
|     const { data, actionType } = action; | |||
|     if (actionType === 'create') { | |||
|       projectData.push(data); | |||
|     } else if (actionType === 'update') { | |||
|       const foundIdx = projectData.findIndex((item: any) => item.id === data.id); | |||
|       if (foundIdx > -1) { | |||
|         projectData[foundIdx] = data; | |||
|       } | |||
|     } else if (actionType === 'remove') { | |||
|       const foundIdx = projectData.findIndex((item: any) => item.id === data.id); | |||
|       if (foundIdx > -1) { | |||
|         projectData.splice(foundIdx, 1); | |||
|       } | |||
|     } | |||
|   }); | |||
|   db.set(projectId, projectData).write(); | |||
|   res.send({ status: 200, code: 0, success: true, data: [] }); | |||
| }; | |||
| 
 | |||
| 
 | |||
| export default { | |||
|   'POST /api/queryGraph': queryGraph, | |||
|   'POST /api/modifyGraph': modifyGraph, | |||
| }; | |||
| @ -0,0 +1,336 @@ | |||
| { | |||
|   "default": [ | |||
|     { | |||
|       "position": { | |||
|         "x": 420, | |||
|         "y": 90 | |||
|       }, | |||
|       "size": { | |||
|         "width": 80, | |||
|         "height": 40 | |||
|       }, | |||
|       "shape": "imove-behavior", | |||
|       "data": { | |||
|         "label": "处理", | |||
|         "configSchema": "{\n  \n}", | |||
|         "configData": {}, | |||
|         "dependencies": "{\n  \n}", | |||
|         "code": "export default async function(ctx) {\n  \n}" | |||
|       }, | |||
|       "ports": { | |||
|         "groups": { | |||
|           "top": { | |||
|             "position": "top", | |||
|             "attrs": { | |||
|               "circle": { | |||
|                 "r": 3, | |||
|                 "magnet": true, | |||
|                 "stroke": "#666", | |||
|                 "strokeWidth": 1, | |||
|                 "fill": "#fff" | |||
|               } | |||
|             } | |||
|           }, | |||
|           "right": { | |||
|             "position": "right", | |||
|             "attrs": { | |||
|               "circle": { | |||
|                 "r": 3, | |||
|                 "magnet": true, | |||
|                 "stroke": "#666", | |||
|                 "strokeWidth": 1, | |||
|                 "fill": "#fff" | |||
|               } | |||
|             } | |||
|           }, | |||
|           "bottom": { | |||
|             "position": "bottom", | |||
|             "attrs": { | |||
|               "circle": { | |||
|                 "r": 3, | |||
|                 "magnet": true, | |||
|                 "stroke": "#666", | |||
|                 "strokeWidth": 1, | |||
|                 "fill": "#fff" | |||
|               } | |||
|             } | |||
|           }, | |||
|           "left": { | |||
|             "position": "left", | |||
|             "attrs": { | |||
|               "circle": { | |||
|                 "r": 3, | |||
|                 "magnet": true, | |||
|                 "stroke": "#666", | |||
|                 "strokeWidth": 1, | |||
|                 "fill": "#fff" | |||
|               } | |||
|             } | |||
|           } | |||
|         }, | |||
|         "items": [ | |||
|           { | |||
|             "id": "top", | |||
|             "group": "top" | |||
|           }, | |||
|           { | |||
|             "id": "right", | |||
|             "group": "right" | |||
|           }, | |||
|           { | |||
|             "id": "bottom", | |||
|             "group": "bottom" | |||
|           }, | |||
|           { | |||
|             "id": "left", | |||
|             "group": "left" | |||
|           } | |||
|         ] | |||
|       }, | |||
|       "id": "81e87610-c749-4667-8382-00c62be30e2b", | |||
|       "zIndex": 1 | |||
|     }, | |||
|     { | |||
|       "position": { | |||
|         "x": 86, | |||
|         "y": 70 | |||
|       }, | |||
|       "size": { | |||
|         "width": 80, | |||
|         "height": 80 | |||
|       }, | |||
|       "shape": "imove-start", | |||
|       "data": { | |||
|         "label": "开始", | |||
|         "configSchema": "{\n  \n}", | |||
|         "configData": {}, | |||
|         "trigger": "start", | |||
|         "dependencies": "{\n  \n}", | |||
|         "code": "export default async function(ctx) {\n  \n}" | |||
|       }, | |||
|       "ports": { | |||
|         "groups": { | |||
|           "top": { | |||
|             "position": "top", | |||
|             "attrs": { | |||
|               "circle": { | |||
|                 "r": 3, | |||
|                 "magnet": true, | |||
|                 "stroke": "#666", | |||
|                 "strokeWidth": 1, | |||
|                 "fill": "#fff" | |||
|               } | |||
|             } | |||
|           }, | |||
|           "right": { | |||
|             "position": "right", | |||
|             "attrs": { | |||
|               "circle": { | |||
|                 "r": 3, | |||
|                 "magnet": true, | |||
|                 "stroke": "#666", | |||
|                 "strokeWidth": 1, | |||
|                 "fill": "#fff" | |||
|               } | |||
|             } | |||
|           }, | |||
|           "bottom": { | |||
|             "position": "bottom", | |||
|             "attrs": { | |||
|               "circle": { | |||
|                 "r": 3, | |||
|                 "magnet": true, | |||
|                 "stroke": "#666", | |||
|                 "strokeWidth": 1, | |||
|                 "fill": "#fff" | |||
|               } | |||
|             } | |||
|           }, | |||
|           "left": { | |||
|             "position": "left", | |||
|             "attrs": { | |||
|               "circle": { | |||
|                 "r": 3, | |||
|                 "magnet": true, | |||
|                 "stroke": "#666", | |||
|                 "strokeWidth": 1, | |||
|                 "fill": "#fff" | |||
|               } | |||
|             } | |||
|           } | |||
|         }, | |||
|         "items": [ | |||
|           { | |||
|             "id": "top", | |||
|             "group": "top" | |||
|           }, | |||
|           { | |||
|             "id": "right", | |||
|             "group": "right" | |||
|           }, | |||
|           { | |||
|             "id": "bottom", | |||
|             "group": "bottom" | |||
|           }, | |||
|           { | |||
|             "id": "left", | |||
|             "group": "left" | |||
|           } | |||
|         ] | |||
|       }, | |||
|       "id": "de868d18-9ec0-4dac-abbe-5cb9e3c20e2f", | |||
|       "zIndex": 2 | |||
|     }, | |||
|     { | |||
|       "position": { | |||
|         "x": 246, | |||
|         "y": 90 | |||
|       }, | |||
|       "size": { | |||
|         "width": 80, | |||
|         "height": 40 | |||
|       }, | |||
|       "shape": "imove-branch", | |||
|       "data": { | |||
|         "label": "判断", | |||
|         "configSchema": "{\n  \n}", | |||
|         "configData": {}, | |||
|         "dependencies": "{\n  \n}", | |||
|         "ports": { | |||
|           "right": { | |||
|             "condition": "true" | |||
|           }, | |||
|           "bottom": { | |||
|             "condition": "false" | |||
|           } | |||
|         }, | |||
|         "code": "export default async function(ctx) {\n  return true;\n}" | |||
|       }, | |||
|       "ports": { | |||
|         "groups": { | |||
|           "top": { | |||
|             "position": "top", | |||
|             "attrs": { | |||
|               "circle": { | |||
|                 "r": 3, | |||
|                 "magnet": true, | |||
|                 "stroke": "#666", | |||
|                 "strokeWidth": 1, | |||
|                 "fill": "#fff" | |||
|               } | |||
|             } | |||
|           }, | |||
|           "right": { | |||
|             "zIndex": 999999999, | |||
|             "position": "right", | |||
|             "attrs": { | |||
|               "circle": { | |||
|                 "r": 3, | |||
|                 "magnet": true, | |||
|                 "stroke": "#666", | |||
|                 "strokeWidth": 1, | |||
|                 "fill": "#fff" | |||
|               } | |||
|             }, | |||
|             "label": { | |||
|               "position": "right" | |||
|             } | |||
|           }, | |||
|           "bottom": { | |||
|             "zIndex": 999999999, | |||
|             "position": "bottom", | |||
|             "attrs": { | |||
|               "circle": { | |||
|                 "r": 3, | |||
|                 "magnet": true, | |||
|                 "stroke": "#666", | |||
|                 "strokeWidth": 1, | |||
|                 "fill": "#fff" | |||
|               } | |||
|             }, | |||
|             "label": { | |||
|               "position": "bottom" | |||
|             } | |||
|           }, | |||
|           "left": { | |||
|             "position": "left", | |||
|             "attrs": { | |||
|               "circle": { | |||
|                 "r": 3, | |||
|                 "magnet": true, | |||
|                 "stroke": "#666", | |||
|                 "strokeWidth": 1, | |||
|                 "fill": "#fff" | |||
|               } | |||
|             } | |||
|           } | |||
|         }, | |||
|         "items": [ | |||
|           { | |||
|             "id": "top", | |||
|             "group": "top" | |||
|           }, | |||
|           { | |||
|             "id": "right", | |||
|             "group": "right", | |||
|             "attrs": { | |||
|               "text": { | |||
|                 "text": "" | |||
|               } | |||
|             } | |||
|           }, | |||
|           { | |||
|             "id": "bottom", | |||
|             "group": "bottom", | |||
|             "attrs": { | |||
|               "text": { | |||
|                 "text": "否" | |||
|               } | |||
|             } | |||
|           }, | |||
|           { | |||
|             "id": "left", | |||
|             "group": "left" | |||
|           } | |||
|         ] | |||
|       }, | |||
|       "id": "a6da6684-96c8-4595-bec6-a94122b61e30", | |||
|       "zIndex": 3 | |||
|     }, | |||
|     { | |||
|       "shape": "edge", | |||
|       "id": "cbcbd0ea-4a2a-4d2a-8135-7b4b7d7ec50d", | |||
|       "source": { | |||
|         "cell": "de868d18-9ec0-4dac-abbe-5cb9e3c20e2f", | |||
|         "port": "right" | |||
|       }, | |||
|       "target": { | |||
|         "cell": "a6da6684-96c8-4595-bec6-a94122b61e30", | |||
|         "port": "left" | |||
|       }, | |||
|       "zIndex": 4 | |||
|     }, | |||
|     { | |||
|       "shape": "edge", | |||
|       "id": "ea10b718-1c66-4a3d-b29c-b0b20e6bba11", | |||
|       "source": { | |||
|         "cell": "a6da6684-96c8-4595-bec6-a94122b61e30", | |||
|         "port": "right" | |||
|       }, | |||
|       "target": { | |||
|         "cell": "81e87610-c749-4667-8382-00c62be30e2b", | |||
|         "port": "left" | |||
|       }, | |||
|       "zIndex": 5, | |||
|       "labels": [ | |||
|         { | |||
|           "attrs": { | |||
|             "label": { | |||
|               "text": "是" | |||
|             } | |||
|           } | |||
|         } | |||
|       ] | |||
|     } | |||
|   ] | |||
| } | |||
| @ -1,6 +1,7 @@ | |||
| { | |||
|   "packages": [ | |||
|     "packages/*" | |||
|     "packages/*", | |||
|     "example" | |||
|   ], | |||
|   "version": "0.3.3" | |||
| } | |||
| } | |||
| @ -1,43 +0,0 @@ | |||
| const path = require('path'); | |||
| const fs = require('fs-extra'); | |||
| 
 | |||
| const writeEntryFile = async (basePath, fileIds) => { | |||
|   const imports = []; | |||
|   const funcMaps = []; | |||
|   fileIds.forEach((id, idx) => { | |||
|     const funcName = `fn_${idx}`; | |||
|     imports.push(`import ${funcName} from './${id}';`); | |||
|     funcMaps.push(`'${id}': ${funcName}`); | |||
|   }); | |||
|   const fileContent = [ | |||
|     imports.join('\n'), | |||
|     `const nodeFns = {\n  ${funcMaps.join(',\n  ')}\n};`, | |||
|     'export default nodeFns;', | |||
|   ].join('\n'); | |||
|   const entryFilePath = path.join(basePath, 'index.js'); | |||
|   await fs.outputFile(entryFilePath, fileContent, { encoding: 'utf8', flag: 'w' }); | |||
| }; | |||
| 
 | |||
| const writeNodeCodes = async (basePath, dsl) => { | |||
|   const fileIds = []; | |||
|   const { cells = [] } = dsl; | |||
|   const nodes = cells.filter((cell) => cell.shape !== 'edge'); | |||
|   for (const { | |||
|     id, | |||
|     data: { code }, | |||
|   } of nodes) { | |||
|     fileIds.push(id); | |||
|     const filePath = path.join(basePath, id + '.js'); | |||
|     await fs.outputFile(filePath, code, { encoding: 'utf8', flag: 'w' }); | |||
|   } | |||
|   return fileIds; | |||
| }; | |||
| 
 | |||
| const setup = async (dsl, logicBasePath) => { | |||
|   const nodeCodesPath = path.join(logicBasePath, 'nodeFns'); | |||
|   await fs.remove(nodeCodesPath); | |||
|   const fileIds = await writeNodeCodes(nodeCodesPath, dsl); | |||
|   await writeEntryFile(nodeCodesPath, fileIds); | |||
| }; | |||
| 
 | |||
| module.exports = setup; | |||
| @ -1,29 +0,0 @@ | |||
| const path = require('path'); | |||
| const fs = require('fs-extra'); | |||
| 
 | |||
| const extractObj = (obj = {}, keys = []) => { | |||
|   const ret = {}; | |||
|   keys.forEach((key) => (ret[key] = obj[key])); | |||
|   return ret; | |||
| }; | |||
| 
 | |||
| const simplify = (dsl) => { | |||
|   const { cells = [] } = dsl; | |||
|   return { | |||
|     cells: cells.map((cell) => { | |||
|       if (cell.shape === 'edge') { | |||
|         return extractObj(cell, ['id', 'shape', 'source', 'target']); | |||
|       } else { | |||
|         return extractObj(cell, ['id', 'shape', 'data']); | |||
|       } | |||
|     }), | |||
|   }; | |||
| }; | |||
| 
 | |||
| const setup = async (dsl, logicBasePath) => { | |||
|   const simpilifiedDSL = simplify(dsl); | |||
|   const dslFilePath = path.join(logicBasePath, 'dsl.json'); | |||
|   await fs.writeFile(dslFilePath, JSON.stringify(simpilifiedDSL, null, 2)); | |||
| }; | |||
| 
 | |||
| module.exports = setup; | |||
| @ -0,0 +1,3 @@ | |||
| { | |||
|   "extends": "../../.babelrc" | |||
| } | |||
| @ -0,0 +1 @@ | |||
| node_modules | |||
| @ -0,0 +1 @@ | |||
| # @imove/compile-code | |||
| @ -0,0 +1,41 @@ | |||
| { | |||
|   "name": "@imove/compile-code", | |||
|   "version": "0.0.1", | |||
|   "description": "imove compile code", | |||
|   "main": "dist/core.common.js", | |||
|   "module": "dist/core.esm.js", | |||
|   "types": "dist/types/index.d.ts", | |||
|   "directories": { | |||
|     "dist": "dist" | |||
|   }, | |||
|   "files": [ | |||
|     "dist" | |||
|   ], | |||
|   "publishConfig": { | |||
|     "access": "public", | |||
|     "registry": "http://registry.npmjs.org/" | |||
|   }, | |||
|   "keywords": [ | |||
|     "imove", | |||
|     "compile code" | |||
|   ], | |||
|   "scripts": { | |||
|     "prepublishOnly": "node scripts/prepublish.js", | |||
|     "declare-type": "tsc --emitDeclarationOnly", | |||
|     "build": "rollup -c & npm run declare-type", | |||
|     "watch": "watch 'npm run build' ./src" | |||
|   }, | |||
|   "repository": { | |||
|     "type": "git", | |||
|     "url": "git+https://github.com/imgcook/iMove.git" | |||
|   }, | |||
|   "author": "smallstonesk", | |||
|   "license": "MIT", | |||
|   "dependencies": { | |||
|     "@antv/x6": "^1.5.1" | |||
|   }, | |||
|   "devDependencies": { | |||
|     "rollup": "^2.7.3", | |||
|     "watch": "^1.0.2" | |||
|   } | |||
| } | |||
| @ -0,0 +1,17 @@ | |||
| import path from 'path'; | |||
| import rollupBaseConfig from '../../rollup.config'; | |||
| import pkg from './package.json'; | |||
| 
 | |||
| export default Object.assign(rollupBaseConfig, { | |||
|   input: path.join(__dirname, './src/index.ts'), | |||
|   output: [ | |||
|     { | |||
|       file: pkg.main, | |||
|       format: 'cjs', | |||
|     }, | |||
|     { | |||
|       file: pkg.module, | |||
|       format: 'es', | |||
|     }, | |||
|   ] | |||
| }); | |||
| @ -0,0 +1,6 @@ | |||
| #!/usr/bin/env node
 | |||
| 
 | |||
| const path = require('path'); | |||
| const prePublish = require('../../../scripts/prepublish'); | |||
| 
 | |||
| prePublish('@imove/compile-code', path.join(__dirname, '../')); | |||
| @ -1,20 +1,16 @@ | |||
| const path = require('path'); | |||
| const fs = require('fs-extra'); | |||
| 
 | |||
| const INSERT_IMPORT_PLUGINS_COMMENT = '// import plugins here'; | |||
| const INSERT_USE_PLUGINS_COMMENT = '// use plugins here'; | |||
| 
 | |||
| const setup = async (plugins = [], logicBasePath) => { | |||
|   const entryFilePath = path.join(logicBasePath, 'index.js'); | |||
|   const codes = await fs.readFile(entryFilePath, 'utf8'); | |||
|   const modifiedContent = codes | |||
| const addPlugins = (originalCode: string = '', plugins: string[] = []): string => { | |||
|   const modifiedContent: string = originalCode | |||
|     .replace(new RegExp(INSERT_IMPORT_PLUGINS_COMMENT), () => { | |||
|       return plugins.map((plugin, index) => `import plugin${index} from '${plugin}';`).join('\n'); | |||
|     }) | |||
|     .replace(new RegExp(INSERT_USE_PLUGINS_COMMENT), () => { | |||
|       return plugins.map((_, index) => `logic.use(plugin${index});`).join('\n'); | |||
|     }); | |||
|   await fs.outputFile(entryFilePath, modifiedContent); | |||
|   return modifiedContent; | |||
| }; | |||
| 
 | |||
| module.exports = setup; | |||
| export default addPlugins; | |||
| @ -0,0 +1,52 @@ | |||
| import {Cell} from '@antv/x6'; | |||
| 
 | |||
| interface DSL { | |||
|   cells: Cell.Properties[]; | |||
| } | |||
| 
 | |||
| interface INodesFns { | |||
|   [fileName: string]: string; | |||
| } | |||
| 
 | |||
| const genEntryFile = (nodeIds: string[]): string => { | |||
|   const imports: string[] = []; | |||
|   const funcMaps: string[] = []; | |||
|   nodeIds.forEach((id, idx) => { | |||
|     const funcName = `fn_${idx}`; | |||
|     imports.push(`import ${funcName} from './${id}';`); | |||
|     funcMaps.push(`'${id}': ${funcName}`); | |||
|   }); | |||
|   const fileContent: string = [ | |||
|     imports.join('\n'), | |||
|     `const nodeFns = {\n  ${funcMaps.join(',\n  ')}\n};`, | |||
|     'export default nodeFns;', | |||
|   ].join('\n'); | |||
|   return fileContent; | |||
| }; | |||
| 
 | |||
| const genNodeFns = (dsl: DSL): INodesFns => { | |||
|   const nodeFns: INodesFns = {}; | |||
|   const { cells = [] } = dsl; | |||
|   const nodes = cells.filter((cell) => cell.shape !== 'edge'); | |||
|   for (const { | |||
|     id, | |||
|     shape, | |||
|     data: { label, code }, | |||
|   } of nodes) { | |||
|     const fileName: string = id + '.js'; | |||
|     const descData: string = `// ${shape}: ${label}\n`; | |||
|     const saveData: string = `${descData}\n${code}`; | |||
|     nodeFns[fileName] = saveData; | |||
|   } | |||
|   return nodeFns; | |||
| }; | |||
| 
 | |||
| const extract = (dsl: DSL): INodesFns => { | |||
|   const nodeFns = genNodeFns(dsl); | |||
|   const nodeIds = Object.keys(nodeFns).map(fileName => fileName.slice(0, -3)); | |||
|   const entryFileContent = genEntryFile(nodeIds); | |||
|   nodeFns['index.js'] = entryFileContent; | |||
|   return nodeFns; | |||
| }; | |||
| 
 | |||
| export default extract; | |||
| @ -0,0 +1,34 @@ | |||
| import {Cell} from '@antv/x6'; | |||
| import addPlugins from './addPlugins'; | |||
| import simplifyDSL from './simplifyDSL'; | |||
| import extractNodeFns from './extractNodeFns'; | |||
| import logicTpl from './template/logic'; | |||
| import indexTpl from './template/index'; | |||
| import contextTpl from './template/context'; | |||
| 
 | |||
| interface DSL { | |||
|   cells: Cell.Properties[]; | |||
| } | |||
| 
 | |||
| interface IOutput { | |||
|   'nodeFns': { | |||
|     [fileName: string]: string | |||
|   }; | |||
|   'context.js': string; | |||
|   'dsl.json': string; | |||
|   'index.js': string; | |||
|   'logic.js': string; | |||
| } | |||
| 
 | |||
| const compile = (dsl: DSL, plugins = []): IOutput => { | |||
|   const output: IOutput = { | |||
|     'nodeFns': extractNodeFns(dsl), | |||
|     "context.js": contextTpl, | |||
|     'dsl.json': JSON.stringify(simplifyDSL(dsl), null, 2), | |||
|     'index.js': addPlugins(indexTpl, plugins), | |||
|     'logic.js': logicTpl | |||
|   }; | |||
|   return output; | |||
| }; | |||
| 
 | |||
| export default compile; | |||
| @ -0,0 +1,32 @@ | |||
| import {Cell} from '@antv/x6'; | |||
| 
 | |||
| interface DSL { | |||
|   cells: Cell.Properties[]; | |||
| } | |||
| 
 | |||
| const extractObj = (obj: Cell.Properties = {}, keys: string[] = []): Cell.Properties => { | |||
|   const ret: Cell.Properties = {}; | |||
|   keys.forEach((key) => { | |||
|     if (obj[key]) { | |||
|       ret[key] = obj[key]; | |||
|     } | |||
|   }); | |||
|   return ret; | |||
| }; | |||
| 
 | |||
| const simplifyDSL = (dsl: DSL): Cell.Properties => { | |||
|   const { cells = [] } = dsl; | |||
|   return { | |||
|     cells: cells.map((cell) => { | |||
|       if (cell.shape === 'edge') { | |||
|         return extractObj(cell, ['id', 'shape', 'source', 'target']); | |||
|       } else { | |||
|         const newCell = extractObj(cell, ['id', 'shape', 'data']); | |||
|         newCell.data = extractObj(cell.data, ['trigger', 'configData', 'ports']); | |||
|         return newCell; | |||
|       } | |||
|     }), | |||
|   }; | |||
| }; | |||
| 
 | |||
| export default simplifyDSL; | |||
| @ -0,0 +1,7 @@ | |||
| { | |||
|   "extends": "../../tsconfig", | |||
|   "compilerOptions": { | |||
|     "outDir": "dist/types" | |||
|   }, | |||
|   "include": ["./src", "./types"] | |||
| } | |||
| @ -0,0 +1,86 @@ | |||
| import React from 'react'; | |||
| 
 | |||
| import 'antd/es/modal/style'; | |||
| import styles from './index.module.less'; | |||
| 
 | |||
| import JSZip from 'jszip'; | |||
| import { Modal } from 'antd'; | |||
| import { Graph } from '@antv/x6'; | |||
| import compileCode from '@imove/compile-code'; | |||
| import { base64Url2Blob, downloadFile } from '../../../utils'; | |||
| 
 | |||
| interface IExportModalProps { | |||
|   flowChart: Graph; | |||
|   visible: boolean; | |||
|   onClose: () => void; | |||
| } | |||
| 
 | |||
| const ExportModal: React.FC<IExportModalProps> = (props) => { | |||
| 
 | |||
|   const { flowChart, visible, onClose } = props; | |||
|   const onExportDSL = () => { | |||
|     const dsl = JSON.stringify(flowChart.toJSON(), null, 2); | |||
|     const blob = new Blob([dsl], { type: 'text/plain' }); | |||
|     downloadFile('imove.dsl.json', blob); | |||
|   }; | |||
|   const onExportCode = () => { | |||
|     const zip = new JSZip(); | |||
|     const dsl = flowChart.toJSON(); | |||
|     const output = compileCode(dsl); | |||
|     Helper.recursiveZip(zip, output); | |||
|     zip.generateAsync({ type: 'blob' }).then((blob) => { | |||
|       downloadFile('logic.zip', blob); | |||
|     }); | |||
|   }; | |||
|   const onExportFlowChart = () => { | |||
|     flowChart.toJPEG((dataUri: string) => { | |||
|       const blob = base64Url2Blob(dataUri); | |||
|       downloadFile('flowChart.png', blob); | |||
|     }); | |||
|   }; | |||
| 
 | |||
|   return ( | |||
|     <Modal | |||
|       width={700} | |||
|       visible={visible} | |||
|       footer={null} | |||
|       title={'导出'} | |||
|       onOk={onClose} | |||
|       onCancel={onClose} | |||
|     > | |||
|       <div className={styles.modalContent}> | |||
|         <div className={styles.downloadWrap} onClick={onExportDSL}> | |||
|           <img className={styles.picPreview} src="//img.alicdn.com/imgextra/i2/O1CN01lvanDL1YKp54hGgMS_!!6000000003041-2-tps-247-247.png" /> | |||
|           <span>DSL</span> | |||
|         </div> | |||
|         <div className={styles.downloadWrap} onClick={onExportCode}> | |||
|           <img className={styles.picPreview} src="//img.alicdn.com/imgextra/i3/O1CN01V3wg6S27JuqePTXZv_!!6000000007777-2-tps-247-247.png" /> | |||
|           <span>代码</span> | |||
|         </div> | |||
|         <div className={styles.downloadWrap} onClick={onExportFlowChart}> | |||
|           <img className={styles.picPreview} src="//img.alicdn.com/imgextra/i1/O1CN01sVItyC1exeLraUhf6_!!6000000003938-2-tps-247-247.png" /> | |||
|           <span>流程图</span> | |||
|         </div> | |||
|       </div> | |||
|     </Modal> | |||
|   ); | |||
| }; | |||
| 
 | |||
| const Helper = { | |||
|   recursiveZip(root: JSZip, json: any) { | |||
|     if (typeof json !== 'object' || json === null) { | |||
|       return; | |||
|     } | |||
|     for (const key in json) { | |||
|       const val = json[key]; | |||
|       if (typeof val === 'string') { | |||
|         root.file(key, val); | |||
|       } else { | |||
|         const dir = root.folder(key) as JSZip; | |||
|         Helper.recursiveZip(dir, val); | |||
|       } | |||
|     } | |||
|   } | |||
| } | |||
| 
 | |||
| export default ExportModal; | |||
| @ -0,0 +1,32 @@ | |||
| .container { | |||
|   margin-left: 20px; | |||
| } | |||
| 
 | |||
| .modalContent { | |||
| 
 | |||
|   display: flex; | |||
|   flex-direction: row; | |||
|   justify-content: space-around; | |||
|   width: 100%; | |||
|   padding: 30px 0; | |||
| 
 | |||
|   .downloadWrap { | |||
| 
 | |||
|     display: flex; | |||
|     flex-direction: column; | |||
|     justify-content: center; | |||
|     align-items: center; | |||
|     padding: 15px 20px 5px; | |||
|     border: 0.5px solid #EFEFEF; | |||
|      | |||
|     &:hover { | |||
|       cursor: pointer; | |||
|       background-color: #EFEFEF; | |||
|     } | |||
| 
 | |||
|     .picPreview { | |||
|       width: 150px; | |||
|       height: 150px; | |||
|     } | |||
|   } | |||
| } | |||
| @ -0,0 +1,33 @@ | |||
| import React, {useState} from 'react'; | |||
| 
 | |||
| import styles from './index.module.less'; | |||
| 
 | |||
| import { Button } from 'antd'; | |||
| import { Graph } from '@antv/x6'; | |||
| import ExportModal from './exportModal'; | |||
| 
 | |||
| interface IProps { | |||
|   flowChart: Graph; | |||
| } | |||
| 
 | |||
| const Export: React.FC<IProps> = props => { | |||
| 
 | |||
|   const {flowChart} = props; | |||
|   const [modalVisible, setModalVisible] = useState<boolean>(false); | |||
| 
 | |||
|   const onOpenModal = () => setModalVisible(true); | |||
|   const onCloseModal = () => setModalVisible(false); | |||
| 
 | |||
|   return ( | |||
|     <div className={styles.container}> | |||
|       <Button type={'primary'} size={'small'} onClick={onOpenModal}>导出</Button> | |||
|       <ExportModal | |||
|         flowChart={flowChart} | |||
|         visible={modalVisible} | |||
|         onClose={onCloseModal} | |||
|       /> | |||
|     </div> | |||
|   ); | |||
| }; | |||
| 
 | |||
| export default Export; | |||
| @ -0,0 +1,3 @@ | |||
| .container { | |||
|   margin-left: 20px; | |||
| } | |||
| @ -0,0 +1,49 @@ | |||
| import React, { useState } from 'react'; | |||
| 
 | |||
| import styles from './index.module.less'; | |||
| 
 | |||
| import { Graph } from '@antv/x6'; | |||
| import { Button, Upload, message } from 'antd'; | |||
| 
 | |||
| interface IProps { | |||
|   flowChart: Graph; | |||
| } | |||
| 
 | |||
| const ImportDSL: React.FC<IProps> = props => { | |||
| 
 | |||
|   const {flowChart} = props; | |||
|   const [disabled, setDisabled] = useState(false); | |||
|   const beforeUpload = (file: any) => { | |||
|     setDisabled(true); | |||
|     const reader = new FileReader(); | |||
|     reader.onload = (evt) => { | |||
|       setDisabled(false); | |||
|       if(!evt.target) { | |||
|         message.error('加载文件失败!'); | |||
|       } else { | |||
|         const dsl = evt.target.result as string; | |||
|         try { | |||
|           flowChart.fromJSON(JSON.parse(dsl)); | |||
|         } catch (err) { | |||
|           message.error('DSL解析失败!'); | |||
|         } | |||
|       } | |||
|     }; | |||
|     reader.readAsText(file); | |||
|     return false; | |||
|   }; | |||
| 
 | |||
|   return ( | |||
|     <Upload | |||
|       className={styles.container} | |||
|       accept={'.json'} | |||
|       disabled={disabled} | |||
|       showUploadList={false} | |||
|       beforeUpload={beforeUpload} | |||
|     > | |||
|       <Button type={'primary'} size={'small'}>导入DSL</Button> | |||
|     </Upload> | |||
|   ); | |||
| }; | |||
| 
 | |||
| export default ImportDSL; | |||
					Loading…
					
					
				
		Reference in new issue