├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── abc.json ├── lerna.json ├── package.json ├── packages ├── demo-g6 │ ├── CHANGELOG.md │ ├── README.md │ ├── build.json │ ├── build.plugin.js │ ├── package.json │ ├── public │ │ └── index.html │ ├── src │ │ ├── index.ts │ │ ├── static │ │ │ ├── assets.json │ │ │ └── schema.json │ │ └── tools │ │ │ └── tool-demo.tsx │ └── tsconfig.json ├── demo-x6 │ ├── CHANGELOG.md │ ├── README.md │ ├── build.json │ ├── build.plugin.js │ ├── package.json │ ├── public │ │ └── index.html │ ├── src │ │ ├── index.ts │ │ ├── plugins │ │ │ ├── delete-node-button.tsx │ │ │ ├── index.scss │ │ │ ├── save-schema-button.tsx │ │ │ ├── undo-rodo-pane.tsx │ │ │ └── x6-designer-extension.tsx │ │ └── static │ │ │ ├── assets.json │ │ │ └── schema.json │ └── tsconfig.json ├── g-react-renderer │ ├── CHANGELOG.md │ ├── README.md │ ├── build.json │ ├── package.json │ ├── src │ │ ├── common │ │ │ ├── commonConfig.ts │ │ │ ├── commonStyle.ts │ │ │ └── index.tsx │ │ ├── components │ │ │ ├── index.tsx │ │ │ └── textarea │ │ │ │ └── index.tsx │ │ ├── diff │ │ │ ├── index.js │ │ │ ├── patches.js │ │ │ ├── render.js │ │ │ ├── util.js │ │ │ └── vpatch.js │ │ ├── elements.ts │ │ ├── index.ts │ │ ├── layout │ │ │ ├── index.js │ │ │ ├── resolveDimensions.js │ │ │ ├── resolveStyles.js │ │ │ └── resolveTextLayout.ts │ │ ├── locale │ │ │ ├── en-us.js │ │ │ └── zh-cn.js │ │ ├── main.scss │ │ ├── render │ │ │ ├── elements │ │ │ │ ├── renderImage.ts │ │ │ │ ├── renderNode.ts │ │ │ │ ├── renderRect.ts │ │ │ │ ├── renderText.ts │ │ │ │ └── rendererLink.ts │ │ │ ├── index.ts │ │ │ └── operations │ │ │ │ └── transform.ts │ │ ├── renderer │ │ │ ├── g.ts │ │ │ ├── index.ts │ │ │ ├── propsEqual.js │ │ │ └── renderer.js │ │ ├── scss │ │ │ └── variables.scss │ │ └── util │ │ │ ├── index.ts │ │ │ ├── measureExactText.ts │ │ │ └── measureText.ts │ └── tsconfig.json ├── plugin-core │ ├── CHANGELOG.md │ ├── README.md │ ├── build.json │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── plugin-g6-designer │ ├── CHANGELOG.md │ ├── README.md │ ├── build.json │ ├── package.json │ ├── src │ │ ├── DesignerView.tsx │ │ ├── builtin-commands │ │ │ ├── index.ts │ │ │ ├── insert-item.ts │ │ │ ├── remove-item.ts │ │ │ ├── undo-redo.ts │ │ │ └── zoom-in-out.ts │ │ ├── designer.ts │ │ ├── graph │ │ │ ├── behaviors │ │ │ │ ├── click-item.ts │ │ │ │ ├── double-finger.ts │ │ │ │ ├── hover-item.ts │ │ │ │ └── index.ts │ │ │ ├── initGraph.tsx │ │ │ ├── interfaces.ts │ │ │ ├── layouts │ │ │ │ ├── index.ts │ │ │ │ └── layout │ │ │ │ │ ├── customLayout.js │ │ │ │ │ ├── customTreeGrid.js │ │ │ │ │ └── indented.ts │ │ │ ├── shapes │ │ │ │ └── default-shape.ts │ │ │ └── utils.tsx │ │ ├── index.ts │ │ └── items │ │ │ ├── edge │ │ │ └── index.tsx │ │ │ ├── index.less │ │ │ ├── index.tsx │ │ │ ├── node │ │ │ └── index.tsx │ │ │ └── state.ts │ └── tsconfig.json ├── plugin-materials-pane │ ├── README.md │ ├── build.json │ ├── package.json │ ├── src │ │ ├── icon.tsx │ │ ├── index.tsx │ │ └── pane │ │ │ ├── Icon │ │ │ ├── Component.tsx │ │ │ ├── component.svg │ │ │ ├── icon.svg │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ │ ├── components │ │ │ ├── Category │ │ │ │ ├── index.module.scss │ │ │ │ └── index.tsx │ │ │ ├── Component │ │ │ │ ├── index.module.scss │ │ │ │ └── index.tsx │ │ │ ├── DragGhost │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ ├── List │ │ │ │ ├── index.module.scss │ │ │ │ └── index.tsx │ │ │ └── Tab │ │ │ │ ├── index.module.scss │ │ │ │ └── index.tsx │ │ │ ├── index.module.scss │ │ │ ├── index.tsx │ │ │ ├── store │ │ │ └── index.ts │ │ │ └── utils │ │ │ └── transform.ts │ └── tsconfig.json ├── plugin-tools │ ├── CHANGELOG.md │ ├── README.md │ ├── build.json │ ├── package.json │ ├── src │ │ ├── common │ │ │ ├── command.ts │ │ │ ├── constants.ts │ │ │ ├── index.ts │ │ │ └── utils.ts │ │ ├── index.scss │ │ ├── index.ts │ │ ├── plugins │ │ │ ├── InsertItemPlugin.tsx │ │ │ ├── OperateButtonPlugin.tsx │ │ │ ├── RemoveItemPlugin.tsx │ │ │ ├── UndoRedoPlugin.tsx │ │ │ ├── ZoomPlugin.tsx │ │ │ └── index.ts │ │ └── tools │ │ │ ├── insertItem.tsx │ │ │ ├── logo.tsx │ │ │ ├── removeItem.tsx │ │ │ ├── save.tsx │ │ │ ├── zoomIn.tsx │ │ │ └── zoomOut.tsx │ └── tsconfig.json └── plugin-x6-designer │ ├── CHANGELOG.md │ ├── README.md │ ├── build.json │ ├── package.json │ ├── src │ ├── DesignerView.tsx │ ├── builtin-commands │ │ ├── index.ts │ │ ├── remove-item.ts │ │ ├── undo-redo.ts │ │ └── zoom-in-out.ts │ ├── designer.ts │ ├── graph │ │ ├── initEvents.ts │ │ ├── initGraph.tsx │ │ ├── register │ │ │ └── shape │ │ │ │ ├── const.ts │ │ │ │ ├── lce-circle.ts │ │ │ │ ├── lce-diamond.ts │ │ │ │ └── lce-rect.ts │ │ ├── registerShape.ts │ │ └── util.ts │ ├── index.tsx │ └── items │ │ ├── edge │ │ └── index.tsx │ │ ├── index.less │ │ ├── index.tsx │ │ ├── node │ │ └── index.tsx │ │ ├── state.ts │ │ └── utils.ts │ └── tsconfig.json ├── scripts ├── create.sh ├── publish.sh ├── setup.js ├── setup.sh └── start.sh ├── server └── index.js ├── templates ├── .babelrc ├── README.md ├── _tsconfig.json ├── build.json ├── package.json └── src │ └── index.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | yarn.lock 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (http://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | 43 | jspm_packages/ 44 | 45 | # Typescript v1 declaration files 46 | # typings/ 47 | 48 | # Optional npm cache directory 49 | .npm 50 | 51 | # Optional eslint cache 52 | .eslintcache 53 | 54 | # Optional REPL history 55 | .node_repl_history 56 | 57 | # Output of 'npm pack' 58 | *.tgz 59 | 60 | # Yarn Integrity file 61 | .yarn-integrity 62 | 63 | # dotenv environment variables file 64 | .env 65 | 66 | .idea 67 | # End of https://www.gitignore.io/api/node 68 | 69 | lib/ 70 | dist/ 71 | es/ 72 | build/ 73 | 74 | .DS_Store 75 | .cache 76 | 77 | package-lock.json 78 | git_log.sh 79 | node_modules/ 80 | 81 | stories/Test 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Alibaba 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Graph Editor Framework 2 | 简介:基于阿里巴巴低代码引擎的统一图框架(图编排,图分析等等) 3 | 4 | --- 5 | 6 | ### 框架依赖 7 | LCE:[lowcode engine]('https://lowcode-engine.cn/index') 8 | 9 | G6: [G6 图可视化引擎]('https://g6.antv.vision/zh') 10 | 11 | X6: [X6 图编辑引擎]('https://x6.antv.vision/zh') 12 | 13 | 14 | ### 开始调试 15 | #### 本地调试 16 | `npm install` 17 | 18 | `npm run setup` 19 | 20 | `npm run start:x6` 21 | 22 | ### Demo 参考 23 | https://github.com/alibaba/lowcode-demo/tree/main/demo-graph-x6 24 | -------------------------------------------------------------------------------- /abc.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lce-graph", 3 | "assets":{ 4 | "type":"command", 5 | "command":{ 6 | "cmd": [ 7 | "sh ./scripts/publish.sh $BUILD_DEST" 8 | ] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "4.0.0", 3 | "version": "1.0.12", 4 | "npmClient": "yarn", 5 | "useWorkspaces": true, 6 | "packages": [ 7 | "packages/*" 8 | ], 9 | "command": { 10 | "bootstrap": { 11 | "npmClientArgs": [ 12 | "--no-package-lock" 13 | ] 14 | }, 15 | "version": { 16 | "allowBranch": [ 17 | "master", 18 | "main", 19 | "release/*", 20 | "daily/*", 21 | "refactor/*" 22 | ] 23 | }, 24 | "publish": { 25 | "npmClient": "npm", 26 | "verifyRegistry": false, 27 | "verifyAccess": false, 28 | "ignoreChanges": [ 29 | "**/*.md", 30 | "**/test/**" 31 | ], 32 | "message": "chore(release): publish %v", 33 | "conventionalCommits": true 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@alilc/graph-editor", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*" 6 | ], 7 | "scripts": { 8 | "start:x6": "lerna exec --scope @alilc/lce-graph-demo-x6 -- npm run start", 9 | "start:g6": "lerna exec --scope @alilc/lce-graph-demo-g6 -- npm run start", 10 | "start": "./scripts/start.sh", 11 | "setup": "node ./scripts/setup.js", 12 | "build": "lerna run build --stream", 13 | "clean": "rm -rf node_modules && lerna clean -y", 14 | "create": "./scripts/create.sh", 15 | "lint": "npm-run-all --parallel tslint stylelint", 16 | "tslint": "eslint --cache --ext .ts,.tsx,.js,.jsx ./", 17 | "stylelint": "stylelint ./**/*.scss", 18 | "prettier": "prettier -c --write '**/*'", 19 | "pretty-quick": "pretty-quick --staged", 20 | "test": "jest", 21 | "test-live": "jest --watch", 22 | "coverage": "jest --coverage", 23 | "publish": "lerna publish patch --force-publish --exact --no-changelog", 24 | "publish:prerelease": "lerna publish prerelease --no-changelog" 25 | }, 26 | "devDependencies": { 27 | "@alib/build-scripts": "^0.1.18", 28 | "@alifd/build-plugin-lowcode": "^0.3.8", 29 | "@types/react-router": "5.1.18", 30 | "babel-jest": "^26.5.2", 31 | "build-plugin-component": "^1.12.0", 32 | "del": "^6.1.1", 33 | "execa": "^5.1.1", 34 | "f2elint": "^2.0.1", 35 | "gulp": "^4.0.2", 36 | "husky": "^7.0.4", 37 | "lerna": "^4.0.0", 38 | "rimraf": "^3.0.2", 39 | "typescript": "4.6.2", 40 | "yarn": "^1.22.17" 41 | }, 42 | "engines": { 43 | "node": ">=14.17.0 <18" 44 | }, 45 | "tnpm": { 46 | "mode": "yarn", 47 | "lockfile": "enable" 48 | }, 49 | "publishConfig": { 50 | "registry": "https://registry.antfin-inc.com" 51 | } 52 | } -------------------------------------------------------------------------------- /packages/demo-g6/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [1.0.3-alpha.7](https://gitlab.alibaba-inc.com/graph-editor-engine/graph-editor/compare/v1.0.3-alpha.6...v1.0.3-alpha.7) (2023-02-08) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * 修复由于 exportName 不正确导致的无法显示配置的问题 ([9cafbe8](https://gitlab.alibaba-inc.com/graph-editor-engine/graph-editor/commit/9cafbe80d47ce08e4919ea929c63be8979ed1ff9)) 12 | 13 | 14 | ### Features 15 | 16 | * 调整用外部资源 ([49e4f53](https://gitlab.alibaba-inc.com/graph-editor-engine/graph-editor/commit/49e4f537901b243d8f14af6ad2e606a0d95e7fe6)) 17 | * 调整自定义command方式 ([aa714a3](https://gitlab.alibaba-inc.com/graph-editor-engine/graph-editor/commit/aa714a382a14f8723d910f6a74d17e2ca66735c0)) 18 | * 调整g6的commanManager及自定义command实现 ([146516e](https://gitlab.alibaba-inc.com/graph-editor-engine/graph-editor/commit/146516e4b2eb03f38f6aab9a2217e00b6d8c343b)) 19 | * 关闭配置节点、边、行为的配置 ([9626b2f](https://gitlab.alibaba-inc.com/graph-editor-engine/graph-editor/commit/9626b2fbbdb70f6df3d13c5745c9cf00df2af6a0)) 20 | * 统一g6 x6两边designer ([585a0e0](https://gitlab.alibaba-inc.com/graph-editor-engine/graph-editor/commit/585a0e055224ce097a90727585f71fbbf3e5d90c)) 21 | 22 | 23 | 24 | 25 | 26 | ## [1.0.3-alpha.5](https://gitlab.alibaba-inc.com/graph-editor-engine/graph-editor/compare/v1.0.3-alpha.4...v1.0.3-alpha.5) (2023-01-31) 27 | 28 | **Note:** Version bump only for package @alilc/lce-graph-demo-g6 29 | 30 | 31 | 32 | 33 | 34 | ## [1.0.3-alpha.3](https://gitlab.alibaba-inc.com/graph-editor-engine/graph-editor/compare/v1.0.3-alpha.2...v1.0.3-alpha.3) (2023-01-31) 35 | 36 | **Note:** Version bump only for package @alilc/lce-graph-demo-g6 37 | 38 | 39 | 40 | 41 | 42 | ## [1.0.3-alpha.2](https://gitlab.alibaba-inc.com/graph-editor-engine/graph-editor/compare/v1.0.3-alpha.1...v1.0.3-alpha.2) (2023-01-31) 43 | 44 | **Note:** Version bump only for package @ali/lce-graph-demo-g6 45 | 46 | 47 | 48 | 49 | 50 | ## [1.0.1-alpha.13](https://gitlab.alibaba-inc.com/graph-editor-engine/graph-editor/compare/v1.0.1-alpha.12...v1.0.1-alpha.13) (2022-09-27) 51 | 52 | 53 | ### Features 54 | 55 | * 支持动态changeData ([976e7da](https://gitlab.alibaba-inc.com/graph-editor-engine/graph-editor/commit/976e7daf3369e9b297ba10bd9e38f9bb1b567795)) 56 | 57 | 58 | 59 | 60 | 61 | ## [1.0.1-alpha.12](https://gitlab.alibaba-inc.com/graph-editor-engine/graph-editor/compare/v1.0.1-alpha.11...v1.0.1-alpha.12) (2022-09-21) 62 | 63 | 64 | ### Features 65 | 66 | * 从物料里读取边配置 ([5df686d](https://gitlab.alibaba-inc.com/graph-editor-engine/graph-editor/commit/5df686de62b5d4e6fa173c04c3524f8f5d9a7bcb)) 67 | * 删除common独立包,整合入tools ([4c0107b](https://gitlab.alibaba-inc.com/graph-editor-engine/graph-editor/commit/4c0107b15e6754ad2efe2b5363aeb19bed1d8d38)) 68 | 69 | 70 | 71 | 72 | 73 | ## [1.0.1-alpha.2](https://gitlab.alibaba-inc.com/graph-editor-engine/graph-editor/compare/v1.0.1-alpha.0...v1.0.1-alpha.2) (2022-09-15) 74 | 75 | **Note:** Version bump only for package @ali/lce-graph-demo-g6 76 | 77 | 78 | 79 | 80 | 81 | ## 1.0.1-alpha.0 (2022-09-15) 82 | 83 | 84 | ### Features 85 | 86 | * 剥离bootstrap依赖 ([e6f99c3](https://gitlab.alibaba-inc.com/graph-editor-engine/graph-editor/commit/e6f99c3e885cec7d1d600dbfcc106c993169943f)) 87 | * 导出g-canvas-renderer,临时关闭fontSize设置 ([980afbf](https://gitlab.alibaba-inc.com/graph-editor-engine/graph-editor/commit/980afbf173440decbed8153d039b8c6787ea46d0)) 88 | * 统一g6/x6的init方式 ([de04c89](https://gitlab.alibaba-inc.com/graph-editor-engine/graph-editor/commit/de04c8922ac6f66e77ece1cf42c93cb08dcbe547)) 89 | * demo跑通,无须build ([441bb7a](https://gitlab.alibaba-inc.com/graph-editor-engine/graph-editor/commit/441bb7aa2f7e377fc64fc3f0b653e0c05b9400be)) 90 | * x6剥离bootstrap依赖 ([ccd8230](https://gitlab.alibaba-inc.com/graph-editor-engine/graph-editor/commit/ccd8230525ae1f7997a0784e1a79ee37f4b3ad86)) 91 | -------------------------------------------------------------------------------- /packages/demo-g6/README.md: -------------------------------------------------------------------------------- 1 | TODO 2 | --- 3 | -------------------------------------------------------------------------------- /packages/demo-g6/build.json: -------------------------------------------------------------------------------- 1 | { 2 | "entry": { 3 | "index": "./src/index.ts" 4 | }, 5 | "publicPath": "/", 6 | "vendor": false, 7 | "externals": { 8 | "react": "var window.React", 9 | "react-dom": "var window.ReactDOM", 10 | "prop-types": "var window.PropTypes", 11 | "@antv/g6": "var window.G6", 12 | "@alifd/next": "var window.Next", 13 | "@alilc/lowcode-engine": "var window.AliLowCodeEngine", 14 | "@alilc/lowcode-editor-core": "var window.AliLowCodeEngine.common.editorCabin", 15 | "@alilc/lowcode-editor-skeleton": "var window.AliLowCodeEngine.common.skeletonCabin", 16 | "@alilc/lowcode-designer": "var window.AliLowCodeEngine.common.designerCabin", 17 | "@alilc/lowcode-engine-ext": "var window.AliLowCodeEngineExt", 18 | "moment": "var window.moment", 19 | "lodash": "var window._" 20 | }, 21 | "devServer": { 22 | "liveReload": false, 23 | "hot": false 24 | }, 25 | "polyfill": false, 26 | "browserslist": "chrome >= 60", 27 | "plugins": [ 28 | "build-plugin-react-app", 29 | [ 30 | "build-plugin-fusion", 31 | { 32 | "externalNext": true 33 | } 34 | ], 35 | [ 36 | "build-plugin-moment-locales", 37 | { 38 | "locales": [ 39 | "zh-cn", 40 | "en-us" 41 | ] 42 | } 43 | ], 44 | "./build.plugin.js" 45 | ] 46 | } -------------------------------------------------------------------------------- /packages/demo-g6/build.plugin.js: -------------------------------------------------------------------------------- 1 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); 2 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 3 | // const BundleBuddyWebpackPlugin = require("bundle-buddy-webpack-plugin"); 4 | const webpack = require('webpack'); 5 | const Config = require('webpack-chain'); 6 | const packageInfo = require('./package.json'); 7 | 8 | module.exports = ({ context, onGetWebpackConfig }) => { 9 | onGetWebpackConfig((config) => { 10 | config.resolve.extensions.add('.less').end(); 11 | config.plugin('DefinePlugin').use(new webpack.DefinePlugin({ 12 | VERSION: JSON.stringify(packageInfo.version) 13 | })); 14 | config.resolve 15 | .plugin('tsconfigpaths') 16 | .use(TsconfigPathsPlugin, [{ 17 | configFile: './tsconfig.json', 18 | }]); 19 | // config.plugin('webpack-bundle-analyzer').use(BundleAnalyzerPlugin); 20 | // config.devtool('source-map'); 21 | // config.plugin('BundleBuddyWebpackPlugin').use(new BundleBuddyWebpackPlugin()); 22 | // if (context.command === 'start') { 23 | // config.devtool('inline-source-map'); 24 | // } 25 | }); 26 | }; -------------------------------------------------------------------------------- /packages/demo-g6/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@alilc/lce-graph-demo-g6", 3 | "version": "1.0.12", 4 | "description": "for Graph Editor Engine", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "private": true, 8 | "files": [ 9 | "lib", 10 | "es" 11 | ], 12 | "scripts": { 13 | "start": "build-scripts start", 14 | "test": "jest -c jest.config.js" 15 | }, 16 | "license": "MIT", 17 | "publishConfig": { 18 | "access": "public", 19 | "registry": "https://registry.npmjs.org/" 20 | }, 21 | "dependencies": { 22 | "@alilc/lowcode-engine": "^1" 23 | }, 24 | "devDependencies": { 25 | "@alib/build-scripts": "^0.1.18", 26 | "build-plugin-component": "^1.10.0", 27 | "build-plugin-fusion": "^0.1.0", 28 | "build-plugin-moment-locales": "^0.1.0", 29 | "build-plugin-react-app": "^1.7.13", 30 | "tsconfig-paths-webpack-plugin": "^3.3.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/demo-g6/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 设计器 7 | 8 | 9 | 10 | 11 | 12 | 22 | 25 | 26 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 58 | 59 | 60 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /packages/demo-g6/src/index.ts: -------------------------------------------------------------------------------- 1 | import { plugins, init as _init } from '@alilc/lowcode-engine'; 2 | import PluginG6Designer from '@alilc/lce-graph-g6-designer'; 3 | import { 4 | logo, 5 | InsertItemPlugin, 6 | OperateButtonPlugin, 7 | ZoomPlugin, 8 | RemoveItemPlugin, 9 | UndoRedoPlugin 10 | } from '@alilc/lce-graph-tools'; 11 | import ToolDemo from './tools/tool-demo'; 12 | import PluginCore from '@alilc/lce-graph-core'; 13 | import assets from './static/assets.json'; 14 | import schema from './static/schema.json'; 15 | 16 | (async function main() { 17 | // 新增节点数据模型 18 | const newNodeModel = { 19 | componentName: 'DemoNode', 20 | props: { 21 | collapsed: false, 22 | type: 'default-shape', 23 | itemColor: 'default', 24 | }, 25 | } 26 | 27 | // 核心插件 28 | await plugins.register(PluginCore, { 29 | assets, 30 | schema 31 | }); 32 | 33 | await plugins.register(PluginG6Designer, { 34 | type: 'tree', 35 | behaviors: [], 36 | graphConfig: [], 37 | layoutConfig: { 38 | compactBox: { 39 | direction: 'TB', 40 | } 41 | } 42 | }); 43 | 44 | // tools 45 | await plugins.register(logo); 46 | await plugins.register(ZoomPlugin); 47 | await plugins.register(RemoveItemPlugin); 48 | await plugins.register(OperateButtonPlugin, [ 49 | { 50 | callback: (schema: any) => { 51 | console.log('保存回调', schema) 52 | }, 53 | name: 'save', 54 | text: '保存', 55 | hotkey: 'command+s', 56 | }, 57 | { 58 | callback: (schema: any) => { 59 | console.log('发布回调', schema) 60 | }, 61 | name: 'publish', 62 | text: '发布', 63 | hotkey: 'command+p', 64 | }, 65 | { 66 | callback: (schema: any) => { 67 | console.log('自定义操作按钮回调', schema) 68 | }, 69 | name: 'test', 70 | text: '测试自定义按钮', 71 | hotkey: 'command+0', 72 | btnProps: { type: 'secondary' }, 73 | }, 74 | ]); 75 | 76 | await plugins.register(InsertItemPlugin, { 77 | model: newNodeModel 78 | }); 79 | await plugins.register(UndoRedoPlugin); 80 | // 自定义插件 81 | await plugins.register(ToolDemo); 82 | 83 | _init(); 84 | })(); 85 | -------------------------------------------------------------------------------- /packages/demo-g6/src/static/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | { 4 | "package": "@alilc/lce-g6-items", 5 | "version": "0.1.5", 6 | "library": "BizComps", 7 | "urls": [ 8 | "https://uipaas-assets.com/prod/npm/@alilc/lce-g6-items/1.0.4/build/lowcode/render/default/view.js", 9 | "https://uipaas-assets.com/prod/npm/@alilc/lce-g6-items/1.0.4/build/lowcode/render/default/view.css" 10 | ], 11 | "editUrls": [ 12 | "https://uipaas-assets.com/prod/npm/@alilc/lce-g6-items/1.0.4/build/lowcode/view.js", 13 | "https://uipaas-assets.com/prod/npm/@alilc/lce-g6-items/1.0.4/build/lowcode/view.css" 14 | ], 15 | "advancedUrls": { 16 | "default": [ 17 | "https://uipaas-assets.com/prod/npm/@alilc/lce-g6-items/1.0.4/build/lowcode/render/default/view.js", 18 | "https://uipaas-assets.com/prod/npm/@alilc/lce-g6-items/1.0.4/build/lowcode/render/default/view.css" 19 | ] 20 | }, 21 | "advancedEditUrls": {} 22 | } 23 | ], 24 | "components": [ 25 | { 26 | "exportName": "AlilcLceG6ItemsMeta", 27 | "npm": { 28 | "package": "@alilc/lce-g6-items", 29 | "version": "0.1.5" 30 | }, 31 | "url": "https://uipaas-assets.com/prod/npm/@alilc/lce-g6-items/1.0.4/build/lowcode/meta.js", 32 | "urls": { 33 | "default": "https://uipaas-assets.com/prod/npm/@alilc/lce-g6-items/1.0.4/build/lowcode/meta.js" 34 | }, 35 | "advancedUrls": { 36 | "default": [ 37 | "https://uipaas-assets.com/prod/npm/@alilc/lce-g6-items/1.0.4/build/lowcode/meta.js" 38 | ] 39 | } 40 | } 41 | ], 42 | "sort": { 43 | "groupList": [ 44 | "精选组件", 45 | "原子组件" 46 | ], 47 | "categoryList": [ 48 | "基础元素", 49 | "布局容器类", 50 | "表格类", 51 | "表单详情类", 52 | "帮助类", 53 | "对话框类", 54 | "业务类", 55 | "通用", 56 | "引导", 57 | "信息输入", 58 | "信息展示", 59 | "信息反馈" 60 | ] 61 | }, 62 | "groupList": [ 63 | "精选组件", 64 | "原子组件" 65 | ], 66 | "ignoreComponents": {} 67 | } -------------------------------------------------------------------------------- /packages/demo-g6/src/static/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "componentName": "Page", 3 | "children": [ 4 | { 5 | "id": "1", 6 | "componentName": "DemoNode", 7 | "props": { 8 | "hidden": true, 9 | "collapsed": false, 10 | "itemColor": "default", 11 | "itemName": "节点标题", 12 | "type": "default-shape" 13 | }, 14 | "children": [ 15 | { 16 | "id": "1.1", 17 | "componentName": "DemoNode", 18 | "props": { 19 | "hidden": true, 20 | "collapsed": false, 21 | "itemColor": "default", 22 | "itemName": "节点标题1.1", 23 | "type": "default-shape" 24 | } 25 | }, 26 | { 27 | "id": "1.2", 28 | "componentName": "DemoNode", 29 | "props": { 30 | "hidden": true, 31 | "collapsed": false, 32 | "itemColor": "default", 33 | "itemName": "节点标题1.2", 34 | "type": "default-shape" 35 | } 36 | } 37 | ] 38 | } 39 | ] 40 | } -------------------------------------------------------------------------------- /packages/demo-g6/src/tools/tool-demo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ILowCodePluginContext } from '@alilc/lowcode-engine'; 3 | import { G6Designer } from '@alilc/lce-graph-g6-designer'; 4 | class Tool extends React.Component<{ ctx: ILowCodePluginContext }, any> { 5 | handleClick() { 6 | const { ctx } = this.props; 7 | // 触发自定义命令 8 | ctx.event.emit('toolDemoEvent', '我是自定义工具信息'); 9 | } 10 | render() { 11 | return ( 12 |
13 | 测试自定义工具 14 |
15 | ) 16 | } 17 | } 18 | 19 | const ToolDemo = (ctx: ILowCodePluginContext) => { 20 | return { 21 | name: 'toolDemo', 22 | async init() { 23 | const { skeleton, plugins } = ctx; 24 | const g6Designer = plugins['plugin-g6-designer'] as G6Designer; 25 | // 实现命令回调 26 | g6Designer.registerCommand('toolDemoEvent', (ctx, graph, data) => { alert(data) }); 27 | skeleton.add({ 28 | name: 'toolDemo', 29 | area: 'toolbar', 30 | type: 'Widget', 31 | props: { 32 | align: 'left' 33 | }, 34 | content: Tool, 35 | contentProps: { 36 | ctx 37 | }, 38 | }); 39 | }, 40 | }; 41 | }; 42 | ToolDemo.pluginName = 'toolDemo'; 43 | 44 | export default ToolDemo; -------------------------------------------------------------------------------- /packages/demo-g6/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib" 5 | }, 6 | "include": [ 7 | "./src/" 8 | ] 9 | } -------------------------------------------------------------------------------- /packages/demo-x6/README.md: -------------------------------------------------------------------------------- 1 | TODO 2 | --- 3 | -------------------------------------------------------------------------------- /packages/demo-x6/build.json: -------------------------------------------------------------------------------- 1 | { 2 | "entry": { 3 | "index": "./src/index.ts" 4 | }, 5 | "publicPath": "/", 6 | "vendor": false, 7 | "externals": { 8 | "react": "var window.React", 9 | "react-dom": "var window.ReactDOM", 10 | "prop-types": "var window.PropTypes", 11 | "@antv/x6": "var window.X6", 12 | "@alifd/next": "var window.Next", 13 | "@alilc/lowcode-engine": "var window.AliLowCodeEngine", 14 | "@alilc/lowcode-editor-core": "var window.AliLowCodeEngine.common.editorCabin", 15 | "@alilc/lowcode-editor-skeleton": "var window.AliLowCodeEngine.common.skeletonCabin", 16 | "@alilc/lowcode-designer": "var window.AliLowCodeEngine.common.designerCabin", 17 | "@alilc/lowcode-engine-ext": "var window.AliLowCodeEngineExt", 18 | "moment": "var window.moment", 19 | "lodash": "var window._" 20 | }, 21 | "devServer": { 22 | "liveReload": false, 23 | "hot": false 24 | }, 25 | "polyfill": false, 26 | "browserslist": "chrome >= 60", 27 | "plugins": [ 28 | "build-plugin-react-app", 29 | [ 30 | "build-plugin-fusion", 31 | { 32 | "externalNext": true 33 | } 34 | ], 35 | [ 36 | "build-plugin-moment-locales", 37 | { 38 | "locales": [ 39 | "zh-cn", 40 | "en-us" 41 | ] 42 | } 43 | ], 44 | "./build.plugin.js" 45 | ] 46 | } -------------------------------------------------------------------------------- /packages/demo-x6/build.plugin.js: -------------------------------------------------------------------------------- 1 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); 2 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 3 | // const BundleBuddyWebpackPlugin = require("bundle-buddy-webpack-plugin"); 4 | const webpack = require('webpack'); 5 | const Config = require('webpack-chain'); 6 | const packageInfo = require('./package.json'); 7 | 8 | module.exports = ({ context, onGetWebpackConfig }) => { 9 | onGetWebpackConfig((config) => { 10 | config.resolve.extensions.add('.less').end(); 11 | config.plugin('DefinePlugin').use(new webpack.DefinePlugin({ 12 | VERSION: JSON.stringify(packageInfo.version) 13 | })); 14 | config.resolve 15 | .plugin('tsconfigpaths') 16 | .use(TsconfigPathsPlugin, [{ 17 | configFile: './tsconfig.json', 18 | }]); 19 | // config.plugin('webpack-bundle-analyzer').use(BundleAnalyzerPlugin); 20 | // config.devtool('source-map'); 21 | // config.plugin('BundleBuddyWebpackPlugin').use(new BundleBuddyWebpackPlugin()); 22 | // if (context.command === 'start') { 23 | // config.devtool('inline-source-map'); 24 | // } 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /packages/demo-x6/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@alilc/lce-graph-demo-x6", 3 | "version": "1.0.12", 4 | "description": "for Graph Editor Engine", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "private": true, 8 | "files": [ 9 | "lib", 10 | "es" 11 | ], 12 | "scripts": { 13 | "start": "build-scripts start", 14 | "test": "jest -c jest.config.js" 15 | }, 16 | "license": "MIT", 17 | "publishConfig": { 18 | "access": "public", 19 | "registry": "https://registry.npmjs.org/" 20 | }, 21 | "dependencies": { 22 | "@alilc/lowcode-engine": "^1" 23 | }, 24 | "devDependencies": { 25 | "@alib/build-scripts": "^0.1.18", 26 | "build-plugin-component": "^1.10.0", 27 | "build-plugin-fusion": "^0.1.0", 28 | "build-plugin-moment-locales": "^0.1.0", 29 | "build-plugin-react-app": "^1.7.13", 30 | "tsconfig-paths-webpack-plugin": "^3.3.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/demo-x6/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 阿里低代码引擎 Demo 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 40 | 42 | 43 | 44 | 45 | 52 |
53 | 54 | 55 | -------------------------------------------------------------------------------- /packages/demo-x6/src/index.ts: -------------------------------------------------------------------------------- 1 | import { plugins, init, project } from '@alilc/lowcode-engine'; 2 | import PluginX6Designer from '@alilc/lce-graph-x6-designer'; 3 | import PluginMaterialsPane from '@alilc/lce-graph-materials-pane'; 4 | import PluginCore from '@alilc/lce-graph-core'; 5 | import { RemoveItemPlugin, OperateButtonPlugin, UndoRedoPlugin, ZoomPlugin, logo } from '@alilc/lce-graph-tools'; 6 | import PluginX6DesignerExtension from './plugins/x6-designer-extension'; 7 | import assets from './static/assets.json'; 8 | import schema from './static/schema.json'; 9 | 10 | (async function main() { 11 | await plugins.register(PluginCore, { 12 | assets, 13 | schema 14 | }); 15 | await plugins.register(PluginX6Designer); 16 | await plugins.register(PluginMaterialsPane); 17 | await plugins.register(PluginX6DesignerExtension); 18 | await plugins.register(logo); 19 | await plugins.register(RemoveItemPlugin); 20 | await plugins.register(OperateButtonPlugin, [ 21 | { 22 | callback: (schema: any) => { 23 | console.log('保存回调', schema) 24 | }, 25 | name: 'save', 26 | text: '保存', 27 | hotkey: 'command+s', 28 | }, 29 | { 30 | callback: (schema: any) => { 31 | console.log('发布回调', schema) 32 | }, 33 | name: 'publish', 34 | text: '发布', 35 | hotkey: 'command+p', 36 | }, 37 | { 38 | callback: (schema: any) => { 39 | console.log('自定义操作按钮回调', schema) 40 | }, 41 | name: 'test', 42 | text: '测试自定义按钮', 43 | hotkey: 'command+0', 44 | btnProps: { type: 'secondary' }, 45 | }, 46 | ]); 47 | await plugins.register(UndoRedoPlugin); 48 | await plugins.register(ZoomPlugin); 49 | init(); 50 | })(); 51 | -------------------------------------------------------------------------------- /packages/demo-x6/src/plugins/delete-node-button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { hotkey, project, Node } from '@alilc/lowcode-engine'; 3 | import { isFormEvent } from '@alilc/lowcode-utils'; 4 | import { IDesigner } from '@alilc/lce-graph-x6-designer'; 5 | 6 | interface IProps { 7 | x6Designer: IDesigner 8 | } 9 | 10 | export default class DeleteNode extends React.Component { 11 | onDelete = (e: any) => { 12 | if (isFormEvent(e)) return; 13 | const selectedNodes = project.currentDocument?.selection.getNodes(); 14 | if (selectedNodes?.length) { 15 | selectedNodes.forEach(node => { 16 | // 可被删除判断 17 | if (node?.componentMeta?.getMetadata().tags?.includes('node')) { 18 | const allNodes = Array.from(project.currentDocument?.nodesMap.values() || []); 19 | // 相关线 20 | const lines = allNodes.filter(n => (n?.componentMeta?.getMetadata().tags?.includes('edge') && (n.getPropValue('source') === node.id || n.getPropValue('target') === node.id))); 21 | lines.forEach(line => { 22 | project.currentDocument?.removeNode(line.id); 23 | }); 24 | } 25 | project.currentDocument?.removeNode(node.id); 26 | }); 27 | } 28 | } 29 | 30 | componentDidMount() { 31 | // 覆盖原有删除 32 | hotkey.bind(['backspace', 'del'], (e) => { 33 | this.onDelete(e); 34 | }); 35 | } 36 | 37 | render() { 38 | return
删除
39 | } 40 | } -------------------------------------------------------------------------------- /packages/demo-x6/src/plugins/index.scss: -------------------------------------------------------------------------------- 1 | .undo-redo-pane { 2 | display: flex; 3 | margin-left: 12px; 4 | justify-content: space-between; 5 | width: 60px; 6 | } -------------------------------------------------------------------------------- /packages/demo-x6/src/plugins/save-schema-button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Button } from '@alifd/next'; 3 | import { project } from '@alilc/lowcode-engine'; 4 | 5 | export default class SaveSchema extends React.Component { 6 | onClick = () => { 7 | const schema = project.currentDocument!.exportSchema(); 8 | console.log(schema); 9 | } 10 | 11 | render() { 12 | return 13 | } 14 | } -------------------------------------------------------------------------------- /packages/demo-x6/src/plugins/undo-rodo-pane.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Button } from '@alifd/next'; 3 | import { project } from '@alilc/lowcode-engine'; 4 | 5 | interface IProps { 6 | } 7 | 8 | export default class UndoRedoPane extends React.Component { 9 | onUndo = () => { 10 | console.log(this.props); 11 | project.currentDocument?.history.back(); 12 | } 13 | 14 | onRedo = () => { 15 | project.currentDocument?.history.forward(); 16 | } 17 | 18 | render() { 19 | return ( 20 |
21 |
撤销
22 |
恢复
23 |
24 | ) 25 | } 26 | } -------------------------------------------------------------------------------- /packages/demo-x6/src/plugins/x6-designer-extension.tsx: -------------------------------------------------------------------------------- 1 | import { ILowCodePluginContext, skeleton } from '@alilc/lowcode-engine'; 2 | import { IDesigner } from '@alilc/lce-graph-x6-designer'; 3 | import * as React from 'react'; 4 | import { Graph, Markup } from '@antv/x6'; 5 | import ReactDOM from 'react-dom'; 6 | import './index.scss'; 7 | 8 | /** 9 | * X6 Designer 业务自定义扩展插件 10 | */ 11 | function pluginX6DesignerExtension(ctx: ILowCodePluginContext) { 12 | return { 13 | init() { 14 | // 获取 x6 designer 内置插件的导出 api 15 | const x6Designer = ctx.plugins['plugin-x6-designer'] as IDesigner; 16 | 17 | x6Designer.onNodeRender((model, node) => { 18 | // @ts-ignore 19 | // 自定义 node 渲染逻辑 20 | const { name, title } = model.propsData; 21 | node.attr('text/textWrap/text', title || name); 22 | }); 23 | 24 | x6Designer.onEdgeRender((model, edge) => { 25 | // @ts-ignore 26 | const { source, target, sourcePortId, targetPortId } = model.propsData; 27 | console.log(sourcePortId, targetPortId); 28 | requestAnimationFrame(() => { 29 | edge.setSource({ cell: source, port: sourcePortId }); 30 | edge.setTarget({ cell: target, port: targetPortId }); 31 | }); 32 | 33 | // https://x6.antv.vision/zh/docs/tutorial/intermediate/edge-labels x6 标签模块 34 | // appendLabel 会触发 onEdgeLabelRender 35 | edge.appendLabel({ 36 | markup: Markup.getForeignObjectMarkup(), 37 | attrs: { 38 | fo: { 39 | width: 120, 40 | height: 30, 41 | x: -60, 42 | y: -15, 43 | }, 44 | }, 45 | }); 46 | }); 47 | 48 | x6Designer.onEdgeLabelRender((args) => { 49 | const { selectors } = args 50 | const content = selectors.foContent as HTMLDivElement 51 | if (content) { 52 | ReactDOM.render(
自定义 react 标签
, content) 53 | } 54 | }) 55 | } 56 | } 57 | } 58 | 59 | pluginX6DesignerExtension.pluginName = 'plugin-x6-designer-extension'; 60 | 61 | export default pluginX6DesignerExtension; -------------------------------------------------------------------------------- /packages/demo-x6/src/static/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | { 4 | "package": "@alilc/lce-x6-items", 5 | "version": "1.0.7", 6 | "library": "AlilcLceX6Items", 7 | "urls": [ 8 | "https://uipaas-assets.com/prod/npm/@alilc/lce-x6-items/1.0.7/build/lowcode/render/default/view.js", 9 | "https://uipaas-assets.com/prod/npm/@alilc/lce-x6-items/1.0.7/build/lowcode/render/default/view.css" 10 | ], 11 | "editUrls": [ 12 | "https://uipaas-assets.com/prod/npm/@alilc/lce-x6-items/1.0.7/build/lowcode/view.js", 13 | "https://uipaas-assets.com/prod/npm/@alilc/lce-x6-items/1.0.7/build/lowcode/view.css" 14 | ], 15 | "advancedUrls": { 16 | "default": [ 17 | "https://uipaas-assets.com/prod/npm/@alilc/lce-x6-items/1.0.7/build/lowcode/render/default/view.js", 18 | "https://uipaas-assets.com/prod/npm/@alilc/lce-x6-items/1.0.7/build/lowcode/render/default/view.css" 19 | ] 20 | }, 21 | "advancedEditUrls": {} 22 | } 23 | ], 24 | "components": [ 25 | { 26 | "exportName": "AlilcLceX6ItemsMeta", 27 | "npm": { 28 | "package": "@alilc/lce-x6-items", 29 | "version": "1.0.7" 30 | }, 31 | "url": "https://uipaas-assets.com/prod/npm/@alilc/lce-x6-items/1.0.7/build/lowcode/meta.js", 32 | "urls": { 33 | "default": "https://uipaas-assets.com/prod/npm/@alilc/lce-x6-items/1.0.7/build/lowcode/meta.js" 34 | }, 35 | "advancedUrls": { 36 | "default": [ 37 | "https://uipaas-assets.com/prod/npm/@alilc/lce-x6-items/1.0.7/build/lowcode/meta.js" 38 | ] 39 | } 40 | } 41 | ], 42 | "sort": { 43 | "groupList": [ 44 | "精选组件", 45 | "原子组件" 46 | ], 47 | "categoryList": [ 48 | "基础元素", 49 | "布局容器类", 50 | "表格类", 51 | "表单详情类", 52 | "帮助类", 53 | "对话框类", 54 | "业务类", 55 | "通用", 56 | "引导", 57 | "信息输入", 58 | "信息展示", 59 | "信息反馈" 60 | ] 61 | }, 62 | "groupList": [ 63 | "精选组件", 64 | "原子组件" 65 | ], 66 | "ignoreComponents": {} 67 | } -------------------------------------------------------------------------------- /packages/demo-x6/src/static/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "componentName": "Page", 3 | "id": "node_dockcviv8fo1", 4 | "fileName": "", 5 | "props": {}, 6 | "children": [ 7 | { 8 | "id": "node-start", 9 | "componentName": "StartEnd", 10 | "title": "开始节点", 11 | "props": { 12 | "name": "开始节点", 13 | "position": { 14 | "x": 82, 15 | "y": 71 16 | } 17 | } 18 | }, 19 | { 20 | "id": "node_ockn610kx9m", 21 | "componentName": "SendEmail", 22 | "title": "发送邮件", 23 | "props": { 24 | "name": "发送邮件", 25 | "position": { 26 | "x": 84, 27 | "y": 263 28 | } 29 | } 30 | }, 31 | { 32 | "id": "node-end", 33 | "componentName": "StartEnd", 34 | "title": "结束节点", 35 | "props": { 36 | "name": "结束节点", 37 | "position": { 38 | "x": 82, 39 | "y": 455 40 | } 41 | } 42 | }, 43 | { 44 | "id": "node_ockn610kx9b", 45 | "componentName": "Line", 46 | "title": "线", 47 | "props": { 48 | "name": "线", 49 | "source": "node-start", 50 | "target": "node_ockn610kx9m", 51 | "sourcePortId": "b", 52 | "targetPortId": "t" 53 | } 54 | }, 55 | { 56 | "id": "node_ockn610kx9a", 57 | "componentName": "Line", 58 | "title": "线", 59 | "props": { 60 | "name": "线", 61 | "source": "node_ockn610kx9m", 62 | "target": "node-end", 63 | "sourcePortId": "b", 64 | "targetPortId": "t" 65 | } 66 | } 67 | ] 68 | } -------------------------------------------------------------------------------- /packages/demo-x6/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib" 5 | }, 6 | "include": [ 7 | "./src/", 8 | "build.plugin.js" 9 | ] 10 | } -------------------------------------------------------------------------------- /packages/g-react-renderer/README.md: -------------------------------------------------------------------------------- 1 | ### node 的样式的编写规则 2 | 3 | #### 是否可拖拽 4 | 5 | > 鼠标 mousedown 一个 node 节点并且移动的时候,是否可以移动画布上的内容 6 | 7 | ```tsx 8 | // 节点的属性 9 | export interface INodeProps { 10 | itemName: string; 11 | itemColor: string; 12 | img: string; 13 | itemDesc?: string; 14 | selected: boolean; 15 | hovered: boolean; 16 | error: boolean; 17 | // 是否可拖拽,默认可拖拽 18 | draggable?: boolean; 19 | } 20 | 21 | // 如果想要在某个shape上覆盖 nodeProps: INodeProps 上的 draggable 属性,可以按照如下设置 22 | const DemoNode = (props: INodeProps) => { 23 | return ( 24 | 25 | 28 | 内容 29 | 30 | 31 | ) 32 | } 33 | 34 | export default DemoNode; 35 | ``` 36 | 37 | #### 透明度相关 38 | 39 | > - 透明度相关属性包括:opacity,fillOpacity,strokeOpacity, 适用的元素:Rect, Image; 40 | > - fillOpacity 和 strokeOpacity 的优先级高于 opacity; 41 | > - 透明度还需要考虑元素的父子嵌套关系,子元素的透明度 = 父元素的透明度 * 自身透明度,透明度默认值为:globalAlpha,未设置则为 1; 42 | 43 | - opacity: Number 范围 [0, 1] 44 | - fillOpacity: Number 范围 [0, 1] 45 | - strokeOpacity: Number 范围 [0, 1] 46 | 47 | ```tsx 48 | export interface IOpacityStyle { 49 | opacity?: number; 50 | fillOpacity?: number; 51 | strokeOpacity?: number; 52 | } 53 | 54 | const DemoNode = (props: INodeProps) => { 55 | return ( 56 | 57 | 63 | 内容 64 | 65 | 66 | ) 67 | } 68 | ``` 69 | 70 | -------------------------------------------------------------------------------- /packages/g-react-renderer/build.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "build-plugin-component" 5 | ] 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /packages/g-react-renderer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@alilc/g-react-renderer", 3 | "version": "1.0.12", 4 | "description": "for Graph Editor Engine", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "files": [ 8 | "lib", 9 | "es" 10 | ], 11 | "scripts": { 12 | "test": "jest -c jest.config.js", 13 | "build": "build-scripts build --skip-demo", 14 | "build:watch": "build-scripts build --skip-demo --watch", 15 | "prepublish": "npm run build" 16 | }, 17 | "license": "MIT", 18 | "publishConfig": { 19 | "access": "public", 20 | "registry": "https://registry.npmjs.org/" 21 | }, 22 | "devDependencies": { 23 | "@alib/build-scripts": "^0.1.18", 24 | "@ice/spec": "^0.1.1", 25 | "@types/ramda": "^0.27.1", 26 | "@types/react": "^16.3.0", 27 | "@types/react-dom": "^16.3.0", 28 | "build-plugin-component": "^1.10.0", 29 | "build-plugin-fusion": "^0.1.0", 30 | "build-plugin-modular-import": "^0.1.0", 31 | "build-plugin-moment-locales": "^0.1.0", 32 | "enzyme": "^3.11.0", 33 | "enzyme-adapter-react-16": "^1.14.0", 34 | "enzyme-to-json": "^3.5.0", 35 | "eslint": "^6.0.1", 36 | "jest-environment-enzyme": "^7.1.2", 37 | "lerna": "^5.1.8", 38 | "react": "^16.3.0", 39 | "react-dom": "^16.3.0", 40 | "react-test-renderer": "^16.8.6" 41 | }, 42 | "dependencies": { 43 | "@antv/g-canvas": "^0.5.12", 44 | "@antv/util": "^2.0.14", 45 | "@react-pdf/font": "^2.0.10", 46 | "@react-pdf/layout": "^2.0.15", 47 | "@react-pdf/stylesheet": "^2.0.8", 48 | "@react-pdf/textkit": "^2.0.5", 49 | "@react-pdf/yoga": "^2.0.2", 50 | "list-diff2": "^0.1.4", 51 | "lodash": "^4.17.21", 52 | "ramda": "^0.27.1", 53 | "react-reconciler": "^0.26.2", 54 | "scheduler": "^0.20.2", 55 | "vision-types": "^1.0.4" 56 | }, 57 | "gitHead": "7ed863fe077671622a9901f670a9311aa85c3615" 58 | } 59 | -------------------------------------------------------------------------------- /packages/g-react-renderer/src/common/commonStyle.ts: -------------------------------------------------------------------------------- 1 | import { 2 | StyleSheet, 3 | } from '../renderer'; 4 | 5 | export default StyleSheet.create({ 6 | center: { 7 | alignSelf: 'center', 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /packages/g-react-renderer/src/common/index.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/lowcode-graph/01df0d10dc3430e295678d11dd1a2eb5199f4b21/packages/g-react-renderer/src/common/index.tsx -------------------------------------------------------------------------------- /packages/g-react-renderer/src/components/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as TextArea } from './textarea'; -------------------------------------------------------------------------------- /packages/g-react-renderer/src/diff/render.js: -------------------------------------------------------------------------------- 1 | function render (vNode) { 2 | const element = document.createElement(vNode.tagName); 3 | const props = vNode.props; 4 | 5 | for (const key in props) { 6 | const value = props[key]; 7 | element.setAttribute(key, value); 8 | } 9 | 10 | vNode.children && vNode.children( child => { 11 | const childElement = typeof child === 'string' ? document.createTextNode(child) : render(child); 12 | element.appendChild(childElement); 13 | }); 14 | 15 | return element; 16 | } 17 | 18 | export default render; -------------------------------------------------------------------------------- /packages/g-react-renderer/src/diff/util.js: -------------------------------------------------------------------------------- 1 | function type(obj) { 2 | return Object.prototype.toString.call(obj).replace(/\[object\s|\]/g, ""); 3 | } 4 | 5 | function isArray(list) { 6 | return type(list) === "Array"; 7 | } 8 | 9 | function isObject(obj) { 10 | return type(obj) === "Object"; 11 | } 12 | 13 | function isString(str) { 14 | return type(str) === "String"; 15 | } 16 | 17 | function isNotEmptyObj(obj) { 18 | return isObject(obj) && JSON.stringify(obj) != "{}"; 19 | } 20 | 21 | function objForEach(obj, fn) { 22 | isNotEmptyObj(obj) && Object.keys(obj).forEach(fn); 23 | } 24 | 25 | function aryForEach(ary, fn) { 26 | ary.length && ary.forEach(fn); 27 | } 28 | 29 | function setAttr(node, key, value) { 30 | switch (key) { 31 | case "style": 32 | node.style.cssText = value; 33 | break; 34 | case "value": 35 | var tagName = node.tagName || ""; 36 | tagName = tagName.toLowerCase(); 37 | if (tagName === "input" || tagName === "textarea") { 38 | node.value = value; 39 | } else { 40 | // if it is not a input or textarea, use `setAttribute` to set 41 | node.setAttribute(key, value); 42 | } 43 | break; 44 | default: 45 | node.setAttribute(key, value); 46 | break; 47 | } 48 | } 49 | 50 | function toArray(data) { 51 | if (!data) { 52 | return []; 53 | } 54 | const ary = []; 55 | aryForEach(data, item => { 56 | ary.push(item); 57 | }); 58 | 59 | return ary; 60 | } 61 | 62 | export { 63 | isArray, 64 | isObject, 65 | isString, 66 | isNotEmptyObj, 67 | objForEach, 68 | aryForEach, 69 | setAttr, 70 | toArray, 71 | }; 72 | -------------------------------------------------------------------------------- /packages/g-react-renderer/src/diff/vpatch.js: -------------------------------------------------------------------------------- 1 | const patch = {}; 2 | patch.REMOVE = 0; 3 | patch.REPLACE = 1; // node replace 4 | patch.TEXT = 2; // text replace 5 | patch.PROPS = 3; 6 | patch.INSERT = 4; 7 | patch.REORDER = 5; 8 | 9 | export default patch; -------------------------------------------------------------------------------- /packages/g-react-renderer/src/elements.ts: -------------------------------------------------------------------------------- 1 | export const Shape = 'SHAPE'; 2 | export const Rect = 'RECT'; 3 | export const Text = 'TEXT'; 4 | export const Image = 'IMAGE'; 5 | export const IconFont = 'ICONFONT'; 6 | export const TextInstance = 'TEXT_INSTANCE'; 7 | 8 | declare global { 9 | namespace JSX { 10 | interface IntrinsicElements { 11 | 'SHAPE': { 12 | name?: string; 13 | link?: string; 14 | style?: any; 15 | children?: any; 16 | }, 17 | 'RECT': { 18 | name: string; 19 | link?: string; 20 | style?: any; 21 | children?: any; 22 | }, 23 | 'IMAGE': { 24 | name: string; 25 | src: string; 26 | link?: string; 27 | style?: any; 28 | clip?: string; 29 | alt?: string; 30 | }, 31 | 'TEXT': { 32 | name: string, 33 | link?: string; 34 | style?: any, 35 | children?: any, 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/g-react-renderer/src/index.ts: -------------------------------------------------------------------------------- 1 | import './main.scss'; 2 | import { GRenderer } from './renderer'; 3 | 4 | export * from './components'; 5 | 6 | export * from './renderer'; 7 | 8 | export { GRenderer }; 9 | 10 | export default GRenderer; 11 | 12 | -------------------------------------------------------------------------------- /packages/g-react-renderer/src/layout/index.js: -------------------------------------------------------------------------------- 1 | import * as R from 'ramda'; 2 | 3 | import resolveStyles from './resolveStyles'; 4 | import resolveDimensions from './resolveDimensions'; 5 | 6 | const layout = R.compose( 7 | resolveDimensions, 8 | resolveStyles, 9 | ); 10 | 11 | export default layout; 12 | -------------------------------------------------------------------------------- /packages/g-react-renderer/src/layout/resolveStyles.js: -------------------------------------------------------------------------------- 1 | import * as R from 'ramda'; 2 | import stylesheet from '@react-pdf/stylesheet'; 3 | 4 | /** 5 | * Resolves node styles 6 | * 7 | * @param {Object} container 8 | * @param {Object} document node 9 | * @returns {Object} node (and subnodes) with resolved styles 10 | */ 11 | const resolveNodeStyles = container => node => { 12 | return R.evolve({ 13 | style: stylesheet(container), 14 | children: R.map(resolveNodeStyles(container)), 15 | })(node); 16 | }; 17 | 18 | /** 19 | * Resolves shape styles 20 | * 21 | * @param {Object} shape 22 | * @returns {Object} document page with resolved styles 23 | */ 24 | const resolveShapeStyles = shape => { 25 | const box = R.prop('box', shape); 26 | const style = R.prop('style', shape); 27 | const container = R.isEmpty(box) ? style : box; 28 | 29 | return R.evolve({ 30 | style: stylesheet(container), 31 | children: R.map(resolveNodeStyles(container)), 32 | })(shape); 33 | }; 34 | 35 | /** 36 | * Resolves document styles 37 | * 38 | * @param {Object} document root 39 | * @returns {Object} document root with resolved styles 40 | */ 41 | export default R.evolve({ 42 | children: R.map(resolveShapeStyles), 43 | }); 44 | -------------------------------------------------------------------------------- /packages/g-react-renderer/src/layout/resolveTextLayout.ts: -------------------------------------------------------------------------------- 1 | import * as R from 'ramda'; 2 | //@ts-ignore 3 | import * as P from '@react-pdf/primitives'; 4 | //@ts-ignore 5 | import layoutText from '@react-pdf/layout/lib/text/layoutText'; 6 | 7 | const isType = R.propEq('type'); 8 | 9 | const isSvg = isType(P.Svg); 10 | 11 | const isText = isType(P.Text); 12 | 13 | const isNotSvg = R.complement(isSvg); 14 | 15 | const shouldLayoutText = (node: any) => isText(node) && !node.lines; 16 | 17 | /** 18 | * Performs text layout on text node if wasn't calculated before. 19 | * Text layout is usually performed on Yoga's layout process (via setMeasureFunc), 20 | * but we need to layout those nodes with fixed width and height. 21 | * 22 | * @param {Object} node 23 | * @returns {Object} layout node 24 | */ 25 | // @ts-ignore 26 | const resolveTextLayout = (node: any) => { 27 | // @ts-ignore 28 | const mapChild = (child: any) => resolveTextLayout(child); 29 | 30 | return R.compose( 31 | R.evolve({ 32 | children: R.map(R.when(isNotSvg, mapChild)), 33 | }), 34 | R.when( 35 | shouldLayoutText, 36 | R.compose( 37 | R.converge(R.assoc('lines'), [ 38 | R.converge(layoutText, [ 39 | R.identity, 40 | R.path(['box', 'width']), 41 | R.path(['box', 'height']), 42 | ]), 43 | R.identity, 44 | ]), 45 | ), 46 | ), 47 | )(node); 48 | }; 49 | 50 | export default resolveTextLayout; 51 | -------------------------------------------------------------------------------- /packages/g-react-renderer/src/locale/en-us.js: -------------------------------------------------------------------------------- 1 | export default { 2 | locale: 'en-us', 3 | name: 'My Name', 4 | }; 5 | -------------------------------------------------------------------------------- /packages/g-react-renderer/src/locale/zh-cn.js: -------------------------------------------------------------------------------- 1 | export default { 2 | locale: 'zh-cn', 3 | name: '我的名字', 4 | }; 5 | -------------------------------------------------------------------------------- /packages/g-react-renderer/src/main.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/lowcode-graph/01df0d10dc3430e295678d11dd1a2eb5199f4b21/packages/g-react-renderer/src/main.scss -------------------------------------------------------------------------------- /packages/g-react-renderer/src/render/elements/renderImage.ts: -------------------------------------------------------------------------------- 1 | import { getShadowStyle, getOpacityStyle } from '../../util'; 2 | 3 | const renderImage = (group: any) => (node: any) => { 4 | const { left, top, width, height } = node.box; 5 | const { clip, src, draggable } = node.props; 6 | const { 7 | shadowOffsetX, 8 | shadowOffsetY, 9 | shadowBlur, 10 | shadowColor, 11 | opacity, 12 | fillOpacity, 13 | strokeOpacity, 14 | cursor, 15 | zIndex, 16 | } = node.style; 17 | let nodeDraggable = node.draggable || draggable; 18 | nodeDraggable = nodeDraggable !== false; 19 | 20 | // shadow 21 | const shadowStyle = getShadowStyle({ 22 | shadowOffsetX, 23 | shadowOffsetY, 24 | shadowBlur, 25 | shadowColor, 26 | }); 27 | 28 | // opacity 29 | const opacityStyle = getOpacityStyle({ 30 | opacity, 31 | fillOpacity, 32 | strokeOpacity, 33 | }); 34 | 35 | const image = group.addShape('image', { 36 | attrs: { 37 | // Common Attr 38 | name: `g-react-item-${node.name || Math.random()}`, 39 | // 暂未支持,可实现 40 | // fill,stroke,lineWidth,lineDash 41 | ...shadowStyle, 42 | ...opacityStyle, 43 | cursor, 44 | // Image Attr 45 | x: left, 46 | y: top, 47 | width, 48 | height, 49 | img: src, 50 | }, 51 | draggable: nodeDraggable, 52 | zIndex: zIndex || 0 53 | }); 54 | 55 | 56 | if (clip === 'circle') { 57 | // clip circle, 3.2.10暂不支持 58 | image.setClip({ 59 | type: 'circle', // 支持 circle、rect、ellipse、Polygon 及自定义 path clip 60 | attrs: { 61 | r: width / 2, 62 | x: left + width / 2, 63 | y: top + height / 2, 64 | } 65 | }); 66 | } 67 | 68 | return node; 69 | }; 70 | export const updateImage = (targetElement: any) => (newProps: any, newStyle: any, oldVNode: any) => { 71 | // 优先使用新的clip属性 72 | const clip = newProps.clip || oldVNode.props.clip; 73 | let newAttr = { ...newProps }; 74 | if (newProps.left !== undefined) { 75 | newAttr.x = newProps.left; 76 | } 77 | if (newProps.top !== undefined) { 78 | newAttr.y = newProps.top; 79 | } 80 | 81 | if (Object.keys(newStyle).length > 0) { 82 | const { 83 | // 阴影 84 | shadowOffsetX, 85 | shadowOffsetY, 86 | shadowBlur, 87 | shadowColor, 88 | // 透明度 89 | opacity, 90 | fillOpacity, 91 | strokeOpacity, 92 | } = newStyle; 93 | // shadow 94 | const shadowStyle = getShadowStyle({ 95 | shadowOffsetX, 96 | shadowOffsetY, 97 | shadowBlur, 98 | shadowColor, 99 | }); 100 | // opacity 101 | const opacityStyle = getOpacityStyle({ 102 | opacity, 103 | fillOpacity, 104 | strokeOpacity, 105 | }); 106 | // assign 107 | newAttr = { 108 | ...newAttr, 109 | ...shadowStyle, 110 | ...opacityStyle, 111 | }; 112 | } 113 | 114 | targetElement.attr(newAttr); 115 | 116 | if (clip === 'circle') { 117 | // clip circle, 3.2.10暂不支持 118 | const width = newAttr.width || oldVNode.box.width; 119 | const height = newAttr.height || oldVNode.box.height; 120 | const left = newAttr.left || oldVNode.box.left; 121 | const top = newAttr.top || oldVNode.box.top; 122 | targetElement.setClip({ 123 | type: 'circle', // 支持 circle、rect、ellipse、Polygon 及自定义 path clip 124 | attrs: { 125 | r: width / 2, 126 | x: left + width / 2, 127 | y: top + height / 2, 128 | } 129 | }); 130 | } 131 | } 132 | 133 | export default renderImage; 134 | -------------------------------------------------------------------------------- /packages/g-react-renderer/src/render/elements/renderNode.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import * as R from 'ramda'; 3 | import * as ELE from '../../elements'; 4 | import transform from '../operations/transform'; 5 | 6 | import renderText from '../elements/renderText'; 7 | import renderRect from '../elements/renderRect'; 8 | import renderImage from '../elements/renderImage'; 9 | 10 | const isText = R.propEq('type', ELE.Text); 11 | const isRect = R.propEq('type', ELE.Rect); 12 | const isImage = R.propEq('type', ELE.Image); 13 | 14 | const shouldRenderChildren = (v: any) => !isText(v); 15 | 16 | const renderChildren = (nodeGroup: any) => (node: any) => { 17 | const subGroup = nodeGroup.addGroup(); 18 | 19 | if (node.box) { 20 | // 偏移子元素 21 | subGroup.translate(node.box.left, node.box.top); 22 | } 23 | 24 | R.compose(R.forEach(renderNode(subGroup)), R.pathOr([], ['children']))(node); 25 | return node; 26 | }; 27 | 28 | const renderNode = (nodeGroup: any) => (node: any) => { 29 | return R.compose( 30 | R.when(shouldRenderChildren, renderChildren(nodeGroup)), 31 | R.cond([ 32 | [isText, renderText(nodeGroup)], 33 | [isRect, renderRect(nodeGroup)], 34 | [isImage, renderImage(nodeGroup)], 35 | [R.T, R.identity], 36 | ]), 37 | transform(nodeGroup), 38 | // @ts-ignore 39 | )(node) 40 | }; 41 | 42 | export default renderNode; 43 | -------------------------------------------------------------------------------- /packages/g-react-renderer/src/render/elements/renderRect.ts: -------------------------------------------------------------------------------- 1 | import { getShadowStyle, getOpacityStyle } from "../../util"; 2 | import { isNumber } from 'lodash'; 3 | 4 | export const renderRect = (nodeGroup: any) => (node: any) => { 5 | const { top, left, width, height } = node.box; 6 | const { link, draggable } = node.props; 7 | let nodeDraggable = node.draggable || draggable; 8 | nodeDraggable = nodeDraggable !== false; 9 | const { 10 | backgroundColor, 11 | borderTopLeftRadius, 12 | borderBottomRightRadius, 13 | borderBottomLeftRadius, 14 | borderTopRightRadius, 15 | borderColor, 16 | borderLeftWidth, 17 | borderLeftColor, 18 | shadowOffsetX, 19 | shadowOffsetY, 20 | shadowBlur, 21 | shadowColor, 22 | opacity, 23 | fillOpacity, 24 | strokeOpacity, 25 | cursor, 26 | // 旋转角度 27 | rotate, 28 | // 偏移 {x: number, y: number } 29 | translate, 30 | zIndex 31 | } = node.style; 32 | 33 | // FIXME: HACK 34 | const lineWidth = borderLeftWidth || 0; 35 | const stroke = borderLeftColor || ''; 36 | const radius = [ 37 | borderTopLeftRadius || 0, 38 | borderTopRightRadius || 0, 39 | borderBottomRightRadius || 0, 40 | borderBottomLeftRadius || 0, 41 | ]; 42 | 43 | // shadow 44 | const shadowStyle = getShadowStyle({ 45 | shadowOffsetX, 46 | shadowOffsetY, 47 | shadowBlur, 48 | shadowColor, 49 | }); 50 | 51 | // opacity 52 | const opacityStyle = getOpacityStyle({ 53 | opacity, 54 | fillOpacity, 55 | strokeOpacity, 56 | }); 57 | 58 | const rect = nodeGroup.addShape('rect', { 59 | attrs: { 60 | // Common Attr 61 | name: `g-react-item-${node.name || Math.random()}`, 62 | fill: backgroundColor, 63 | stroke, 64 | lineWidth, 65 | // 暂未支持,可实现 66 | // lineDash, 67 | ...shadowStyle, 68 | ...opacityStyle, 69 | cursor: cursor || (link && 'pointer'), 70 | // Rect Attr 71 | x: left, 72 | y: top, 73 | width, 74 | height, 75 | radius, 76 | // Extra Attr 77 | link, 78 | }, 79 | draggable: nodeDraggable, 80 | zIndex: zIndex || 0 81 | }); 82 | 83 | if (rotate) { 84 | rect.rotate(rotate); 85 | } 86 | 87 | if (translate && (isNumber(translate.x) && isNumber(translate.y))) { 88 | rect.translate(translate.x, translate.y); 89 | } 90 | 91 | return node; 92 | }; 93 | 94 | export const updateRect = (targetElement: any) => (newProps: any, newStyle: any) => { 95 | let newAttr = { ...newProps }; 96 | if (newProps.left !== undefined) { 97 | newAttr.x = newProps.left; 98 | } 99 | if (newProps.top !== undefined) { 100 | newAttr.y = newProps.top; 101 | } 102 | if (Object.keys(newStyle).length > 0) { 103 | const { 104 | backgroundColor, 105 | borderTopLeftRadius, 106 | borderBottomRightRadius, 107 | borderBottomLeftRadius, 108 | borderTopRightRadius, 109 | borderLeftWidth, 110 | borderLeftColor, 111 | shadowOffsetX, 112 | shadowOffsetY, 113 | shadowBlur, 114 | shadowColor, 115 | opacity, 116 | fillOpacity, 117 | strokeOpacity, 118 | } = newStyle; 119 | 120 | // FIXME: HACK 121 | const lineWidth = borderLeftWidth || 0; 122 | const stroke = borderLeftColor || ''; 123 | const radius = [ 124 | borderTopLeftRadius || 0, 125 | borderTopRightRadius || 0, 126 | borderBottomRightRadius || 0, 127 | borderBottomLeftRadius || 0, 128 | ]; 129 | 130 | // shadow 131 | const shadowStyle = getShadowStyle({ 132 | shadowOffsetX, 133 | shadowOffsetY, 134 | shadowBlur, 135 | shadowColor, 136 | }); 137 | 138 | // opacity 139 | const opacityStyle = getOpacityStyle({ 140 | opacity, 141 | fillOpacity, 142 | strokeOpacity, 143 | }); 144 | 145 | newAttr = { 146 | ...newAttr, 147 | fill: backgroundColor, 148 | radius, 149 | stroke, 150 | lineWidth, 151 | ...shadowStyle, 152 | ...opacityStyle, 153 | }; 154 | } 155 | targetElement.attr(newAttr); 156 | } 157 | 158 | export default renderRect; 159 | -------------------------------------------------------------------------------- /packages/g-react-renderer/src/render/elements/rendererLink.ts: -------------------------------------------------------------------------------- 1 | // 该区域可点击 2 | -------------------------------------------------------------------------------- /packages/g-react-renderer/src/render/index.ts: -------------------------------------------------------------------------------- 1 | import renderNode from './elements/renderNode'; 2 | 3 | const renderShape = (gContainerGroup: any) => (shape: any) => { 4 | // create parent group 5 | const shapeGroup = gContainerGroup.addGroup(); 6 | const elements = shape.children || []; 7 | elements.forEach(renderNode(shapeGroup)); 8 | }; 9 | 10 | const render = (gContainerGroup: any, shape: any) => { 11 | renderShape(gContainerGroup)(shape); 12 | return gContainerGroup; 13 | }; 14 | 15 | export default render; 16 | -------------------------------------------------------------------------------- /packages/g-react-renderer/src/render/operations/transform.ts: -------------------------------------------------------------------------------- 1 | import * as R from 'ramda'; 2 | 3 | const applySingleTransformation = (ctx: any, transform: any) => { 4 | const { operation, value } = transform; 5 | 6 | switch (operation) { 7 | case 'scale': { 8 | const [scaleX, scaleY] = value; 9 | // ctx.scale(scaleX, scaleY, { origin }); 10 | break; 11 | } 12 | 13 | case 'rotate': { 14 | const [angle] = value; 15 | // ctx.rotate(angle, { origin }); 16 | break; 17 | } 18 | 19 | case 'translate': { 20 | const [x, y] = value; 21 | // ctx.translate(x, y, { origin }); 22 | break; 23 | } 24 | 25 | case 'matrix': { 26 | // ctx.transform(...value); 27 | break; 28 | } 29 | 30 | default: { 31 | console.error(`Transform operation: '${operation}' doesn't supported`); 32 | } 33 | } 34 | }; 35 | 36 | const applyTransformations = (ctx: any, node: any) => { 37 | if (!node.origin) return node; 38 | 39 | const origin = [node.origin.left, node.origin.top]; 40 | const operations = (node.style && node.style.transform) || []; 41 | 42 | operations.forEach((operation: any) => { 43 | applySingleTransformation(ctx, operation); 44 | }); 45 | 46 | return node; 47 | }; 48 | 49 | export default R.curryN(2, applyTransformations); 50 | -------------------------------------------------------------------------------- /packages/g-react-renderer/src/renderer/g.ts: -------------------------------------------------------------------------------- 1 | // render to g6 node 2 | import layoutShape from '../layout'; 3 | import renderShape from '../render'; 4 | import { createRenderer } from './renderer'; 5 | import diff from '../diff'; 6 | import patch from '../diff/patches'; 7 | export class GRenderer { 8 | 9 | constructor(element: any, renderer?: any, callback?: any) { 10 | this.element = element; 11 | this.renderer = renderer; 12 | this.callback = callback; 13 | } 14 | 15 | element: any = null; 16 | renderer: any = null; 17 | callback: any = null; 18 | layout: any = null; 19 | newLayout: any = null; 20 | 21 | calculateLayout() { 22 | const container = { type: 'ROOT', shape: null }; 23 | 24 | let renderer = this.renderer || createRenderer({ onChange: this.callback ? this.callback : () => { } }); 25 | 26 | const newRoot = renderer.createContainer(container); 27 | 28 | const updateContainer = element => { 29 | renderer.updateContainer(element, newRoot, null); 30 | }; 31 | 32 | if (this.element) updateContainer(this.element); 33 | // cache layout 34 | this.layout = layoutShape(container.shape); 35 | return this.layout; 36 | } 37 | 38 | reCalculateLayout(element: any) { 39 | this.element = element; 40 | const container = { type: 'ROOT', shape: null }; 41 | 42 | let renderer = this.renderer || createRenderer({ onChange: this.callback ? this.callback : () => { } }); 43 | 44 | const newRoot = renderer.createContainer(container); 45 | 46 | const updateContainer = (element: any) => { 47 | renderer.updateContainer(element, newRoot, null); 48 | }; 49 | 50 | if (this.element) updateContainer(this.element); 51 | // cache layout 52 | this.newLayout = layoutShape(container.shape); 53 | return this.newLayout; 54 | } 55 | 56 | render(gGroupContainer: any, layoutInfo: any) { 57 | const layout = layoutInfo || this.layout || this.calculateLayout(); 58 | 59 | return renderShape(gGroupContainer, layout); 60 | } 61 | 62 | update(gGroupContainer: any, layoutInfo: any, element: any) { 63 | // 更新节点 兜底第一次渲染 64 | const layout = layoutInfo || this.newLayout || this.reCalculateLayout(element) || this.layout || this.calculateLayout(); 65 | const patches = diff(this.layout, this.newLayout); 66 | patch(this.layout, patches, gGroupContainer); 67 | // 更新完节点把最新layout覆盖老layout 68 | this.layout = this.newLayout; 69 | 70 | // const shape = gGroupContainer.findAll( 71 | // (allItem: any) => allItem.attr('name') && allItem.attr('name').indexOf('g-react') > -1, 72 | // ); 73 | // shape.forEach((shapeItem: any) => { 74 | // shapeItem.remove(); 75 | // }); 76 | // return renderShape(gGroupContainer, layout); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/g-react-renderer/src/renderer/index.ts: -------------------------------------------------------------------------------- 1 | const StyleSheet = { 2 | create: (s: any) => s, 3 | }; 4 | 5 | export { StyleSheet }; 6 | 7 | export { GRenderer } from './g'; 8 | 9 | export * from '../elements'; 10 | -------------------------------------------------------------------------------- /packages/g-react-renderer/src/renderer/propsEqual.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-continue */ 2 | 3 | /** 4 | * Checks if two sets of props are equal (recursively) 5 | * 6 | * @param {Object} props A 7 | * @param {Object} props B 8 | * @returns {Boolean} props equals? 9 | * 10 | */ 11 | const propsEqual = (a, b) => { 12 | const oldPropsKeys = Object.keys(a); 13 | const newPropsKeys = Object.keys(b); 14 | 15 | if (oldPropsKeys.length !== newPropsKeys.length) { 16 | return false; 17 | } 18 | 19 | for (let i = 0; i < oldPropsKeys.length; i += 1) { 20 | const propName = oldPropsKeys[i]; 21 | 22 | if (propName === 'render' && !a[propName] !== !b[propName]) { 23 | return false; 24 | } 25 | 26 | if (propName !== 'children' && a[propName] !== b[propName]) { 27 | if ( 28 | typeof a[propName] === 'object' && 29 | typeof b[propName] === 'object' && 30 | propsEqual(a[propName], b[propName]) 31 | ) { 32 | continue; 33 | } 34 | 35 | return false; 36 | } 37 | 38 | if ( 39 | propName === 'children' && 40 | (typeof a[propName] === 'string' || typeof b[propName] === 'string') 41 | ) { 42 | return a[propName] === b[propName]; 43 | } 44 | } 45 | 46 | return true; 47 | }; 48 | 49 | export default propsEqual; 50 | -------------------------------------------------------------------------------- /packages/g-react-renderer/src/scss/variables.scss: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | /* write style here */ 4 | @import '~@alifd/next/variables.scss'; 5 | 6 | // 必须设置,请严格遵循该包含default的写法。 7 | $css-prefix: 'next-' !default; 8 | $deep-css-prefix: 'deep-' !default; 9 | 10 | // 使用到的主题扩展变量,内置默认值 11 | $color-fill1-5: #BBC3CC !default; 12 | $color-fill1-6: #A5AFBC !default; 13 | $color-fill1-7: #79889B !default; 14 | $color-fill1-8: #4C6079 !default; 15 | $color-fill1-9: #1F3858 !default; 16 | 17 | // default for theme-97 18 | $color-brand1-2: #FFF0E5 !default; 19 | $color-brand1-3: #FFF7F0 !default; 20 | 21 | // default for theme-254 22 | //$color-brand1-2: #E5F1FD !default; 23 | //$color-brand1-3: #F0F7FF !default; 24 | 25 | -------------------------------------------------------------------------------- /packages/g-react-renderer/src/util/index.ts: -------------------------------------------------------------------------------- 1 | import { get, isNumber } from 'lodash'; 2 | 3 | interface IShadowStyle { 4 | shadowOffsetX?: string | number; 5 | shadowOffsetY?: string | number 6 | shadowBlur?: string | number 7 | shadowColor?: string 8 | } 9 | 10 | interface IOpacityStyle { 11 | opacity?: number; 12 | fillOpacity?: number; 13 | strokeOpacity?: number; 14 | } 15 | 16 | const localeTemp = get(window, 'g_config.locale') || 'zh_CN'; 17 | const locale = /^zh[-_]cn$/i.test(localeTemp) ? 'zh_CN' : 'en_US' 18 | const varList = get(window, `arrangement[${locale}]`) || {}; 19 | 20 | export const i18n = (key: string) => { 21 | return varList[key] || key; 22 | } 23 | 24 | export const formatCssNumber = (value: number | string) => { 25 | const val = +(`${value}`).replace('px', ''); 26 | return isNaN(val) ? 0 : val; 27 | } 28 | 29 | /** 30 | * 判断是否合法的透明度值,范围是 [0, 1] 31 | * @param value Number 透明度值 32 | * @returns Boolean 33 | */ 34 | export const isAlphaNumber = (value: any): boolean => { 35 | return isNumber(value) && (value >= 0 && value <= 1); 36 | } 37 | 38 | /** 39 | * 获取元素的阴影样式 40 | * @param style IShadowStyle 阴影样式 41 | * @returns IShadowStyle 42 | */ 43 | export const getShadowStyle = (style: IShadowStyle): IShadowStyle => { 44 | const { 45 | shadowOffsetX, 46 | shadowOffsetY, 47 | shadowBlur, 48 | shadowColor, 49 | } = style; 50 | const shadowStyle = {}; 51 | if (shadowColor) { 52 | Object.assign(shadowStyle, { 53 | shadowColor, 54 | shadowBlur: formatCssNumber(shadowBlur || 0), 55 | shadowOffsetX: formatCssNumber(shadowOffsetX || 0), 56 | shadowOffsetY: formatCssNumber(shadowOffsetY || 0), 57 | }) 58 | } 59 | return shadowStyle; 60 | } 61 | 62 | /** 63 | * 获取元素的透明度样式 64 | * @param style IOpacityStyle 透明度样式 65 | * @returns IOpacityStyle 66 | */ 67 | export const getOpacityStyle = (style: IOpacityStyle): IOpacityStyle => { 68 | const opacityStyle: IOpacityStyle = {}; 69 | Object.keys(style).forEach(key => { 70 | const value = style[key as keyof IOpacityStyle] 71 | if (isAlphaNumber(value)) { 72 | opacityStyle[key as keyof IOpacityStyle] = value; 73 | } 74 | }); 75 | return opacityStyle; 76 | } 77 | -------------------------------------------------------------------------------- /packages/g-react-renderer/src/util/measureExactText.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/lowcode-graph/01df0d10dc3430e295678d11dd1a2eb5199f4b21/packages/g-react-renderer/src/util/measureExactText.ts -------------------------------------------------------------------------------- /packages/g-react-renderer/src/util/measureText.ts: -------------------------------------------------------------------------------- 1 | import { isString, memoize, values } from '@antv/util'; 2 | 3 | let ctx: CanvasRenderingContext2D | null; 4 | 5 | /** 6 | * 获取 canvas context 7 | */ 8 | export function getCanvasContext() { 9 | if (!ctx) { 10 | ctx = document.createElement('canvas').getContext('2d'); 11 | } 12 | 13 | return ctx; 14 | } 15 | 16 | /** 17 | * 计算文本在画布中的宽高 18 | * @param text 文本 19 | * @param font 字体 20 | */ 21 | export const measureText = memoize( 22 | (text: string, font: any = {}): any => { 23 | const { fontSize = 12, fontFamily = 'PingFang SC', fontWeight, fontStyle, fontVariant } = font; 24 | 25 | // return { 26 | // // 使用近似的文本高度 27 | // // https://stackoverflow.com/questions/1134586/how-can-you-find-the-height-of-text-on-an-html-canvas 28 | // height: fontSize * 1.2, 29 | // width: fontSize * text.length, 30 | // }; 31 | 32 | const ctx = getCanvasContext(); 33 | if (ctx) { 34 | // @see https://developer.mozilla.org/zh-CN/docs/Web/CSS/font 35 | ctx.font = [fontStyle, fontWeight, fontVariant, `${fontSize}px`, fontFamily].join(' '); 36 | const metrics = ctx.measureText(isString(text) ? text : ''); 37 | // metrics 38 | // actualBoundingBoxAscent: 8.63671875 39 | // actualBoundingBoxDescent: 2.654296875 40 | // actualBoundingBoxLeft: -1.025390625 41 | // actualBoundingBoxRight: 98.595703125 42 | // fontBoundingBoxAscent: 11 43 | // fontBoundingBoxDescent: 3 44 | // width: 99.380859375 45 | return { 46 | // 使用近似的文本高度 47 | // https://stackoverflow.com/questions/1134586/how-can-you-find-the-height-of-text-on-an-html-canvas 48 | height: fontSize * 1.2, 49 | width: metrics.width, 50 | }; 51 | } 52 | return { 53 | height: 0, 54 | width: 0, 55 | }; 56 | }, 57 | (text: string, font = {}) => [text, ...values(font)].join('') 58 | ); 59 | -------------------------------------------------------------------------------- /packages/g-react-renderer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib" 5 | }, 6 | "include": [ 7 | "./src/" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/plugin-core/README.md: -------------------------------------------------------------------------------- 1 | ## lowcode-graph-core 2 | -------------------------------------------------------------------------------- /packages/plugin-core/build.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "build-plugin-component" 5 | ] 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /packages/plugin-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@alilc/lce-graph-core", 3 | "version": "1.0.12", 4 | "description": "for Graph Editor Engine", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "files": [ 8 | "lib", 9 | "es" 10 | ], 11 | "scripts": { 12 | "test": "jest -c jest.config.js", 13 | "build": "build-scripts build --skip-demo", 14 | "build:watch": "build-scripts build --skip-demo --watch", 15 | "prepublish": "npm run build" 16 | }, 17 | "license": "MIT", 18 | "publishConfig": { 19 | "access": "public", 20 | "registry": "https://registry.npmjs.org/" 21 | }, 22 | "dependencies": { 23 | "@alilc/lowcode-plugin-inject": "^1.2.2" 24 | }, 25 | "devDependencies": { 26 | "@alib/build-scripts": "^0.1.18", 27 | "build-plugin-component": "^1.10.0" 28 | }, 29 | "gitHead": "7ed863fe077671622a9901f670a9311aa85c3615" 30 | } 31 | -------------------------------------------------------------------------------- /packages/plugin-core/src/index.ts: -------------------------------------------------------------------------------- 1 | import { AssetLoader } from '@alilc/lowcode-utils'; 2 | import { IPublicModelPluginContext } from '@alilc/lowcode-types'; 3 | import Inject, { injectAssets } from '@alilc/lowcode-plugin-inject'; 4 | 5 | const PluginCore = (ctx: IPublicModelPluginContext, options: any) => { 6 | return { 7 | async init() { 8 | const { material, project, plugins } = ctx; 9 | const { assets, schema } = options; 10 | await plugins.register(Inject); 11 | // 注册 components,加载所有的 meta js 12 | await material.setAssets(await injectAssets(assets)); 13 | 14 | // 加载 schema 15 | project.openDocument(schema); 16 | 17 | // 简单处理 init 时候直接 load 所有组件 view 18 | const loader = new AssetLoader(); 19 | const componentsAssets = assets.packages.map((asset: any) => asset.urls).flat(); 20 | await loader.load(componentsAssets); 21 | }, 22 | } 23 | } 24 | 25 | PluginCore.pluginName = 'PluginCore'; 26 | PluginCore.meta = { 27 | preferenceDeclaration: { 28 | title: '参数定义', 29 | properties: [ 30 | { 31 | key: 'assets', 32 | type: 'object', 33 | description: '资产包', 34 | }, 35 | { 36 | key: 'schema', 37 | type: 'object', 38 | description: 'schema 描述', 39 | }, 40 | ], 41 | }, 42 | } 43 | export default PluginCore; -------------------------------------------------------------------------------- /packages/plugin-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib" 5 | }, 6 | "include": [ 7 | "./src/" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/plugin-g6-designer/README.md: -------------------------------------------------------------------------------- 1 | ## g6 画布插件 -------------------------------------------------------------------------------- /packages/plugin-g6-designer/build.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "build-plugin-component" 5 | ] 6 | ] 7 | } -------------------------------------------------------------------------------- /packages/plugin-g6-designer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@alilc/lce-graph-g6-designer", 3 | "version": "1.0.12", 4 | "description": "for Graph Editor Engine", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "files": [ 8 | "lib", 9 | "es" 10 | ], 11 | "scripts": { 12 | "test": "jest -c jest.config.js", 13 | "build": "build-scripts build --skip-demo", 14 | "build:watch": "build-scripts build --skip-demo --watch", 15 | "prepublish": "npm run build" 16 | }, 17 | "license": "MIT", 18 | "publishConfig": { 19 | "access": "public", 20 | "registry": "https://registry.npmjs.org/" 21 | }, 22 | "dependencies": { 23 | "@alilc/lce-graph-tools": "1.0.12", 24 | "@alilc/lowcode-utils": "^1.0.0", 25 | "@antv/hierarchy": "^0.6.8" 26 | }, 27 | "devDependencies": { 28 | "@alib/build-scripts": "^0.1.18", 29 | "@antv/g6": "^4.6.4", 30 | "build-plugin-component": "^1.10.0" 31 | }, 32 | "gitHead": "7ed863fe077671622a9901f670a9311aa85c3615" 33 | } 34 | -------------------------------------------------------------------------------- /packages/plugin-g6-designer/src/DesignerView.tsx: -------------------------------------------------------------------------------- 1 | import { createElement, PureComponent } from "react"; 2 | import { ILowCodePluginContext, project } from '@alilc/lowcode-engine'; 3 | import { Editor } from '@alilc/lowcode-editor-core'; 4 | import { initTreeGraph, initGraph } from "./graph/initGraph"; 5 | import Items from "./items"; 6 | import { render } from "react-dom"; 7 | import { CommandManager } from "@alilc/lce-graph-tools"; 8 | import { Graph, TreeGraph } from "@antv/g6"; 9 | import { rootState } from './items/state'; 10 | import g6Designer from "./designer"; 11 | 12 | interface IProps { 13 | editor: Editor; 14 | ctx: ILowCodePluginContext; 15 | commandManager: CommandManager; 16 | type: 'tree' | 'graph'; 17 | graphConfig?: any; 18 | layoutConfig?: any; 19 | } 20 | 21 | 22 | 23 | export default class DesignerView extends PureComponent { 24 | private container: HTMLDivElement; 25 | private nodesContainer: HTMLDivElement; 26 | 27 | refContainer = (container: HTMLDivElement) => { 28 | this.container = container; 29 | } 30 | 31 | refNodesContainer = (container: HTMLDivElement) => { 32 | this.nodesContainer = container; 33 | } 34 | 35 | componentDidMount() { 36 | g6Designer.setGraphConfig(this.props.graphConfig || {}); 37 | g6Designer.setLayoutConfig(this.props.layoutConfig || {}); 38 | 39 | // @ts-ignore 40 | let graph: Graph | TreeGraph | null = null; 41 | if (rootState.type === 'tree') { 42 | // 树布局 43 | graph = initTreeGraph(this.container, rootState.getGraphData.bind(rootState)); 44 | } else { 45 | // 图布局 46 | graph = initGraph(this.container); 47 | } 48 | if (graph) { 49 | // g6Designer start 50 | g6Designer.init(this.props.ctx, graph); 51 | 52 | render( 53 | createElement(Items, { 54 | graph 55 | }), 56 | this.nodesContainer 57 | ); 58 | } 59 | } 60 | 61 | render() { 62 | return ( 63 |
64 |
65 |
66 | ) 67 | } 68 | } -------------------------------------------------------------------------------- /packages/plugin-g6-designer/src/builtin-commands/index.ts: -------------------------------------------------------------------------------- 1 | export * from './insert-item'; 2 | export * from './remove-item'; 3 | export * from './zoom-in-out'; 4 | export * from './undo-redo'; -------------------------------------------------------------------------------- /packages/plugin-g6-designer/src/builtin-commands/insert-item.ts: -------------------------------------------------------------------------------- 1 | import { ILowCodePluginContext } from '@alilc/lowcode-engine'; 2 | export const insertChild = (ctx: ILowCodePluginContext, graph: any, options: { selectedNodes: any, model: any }) => { 3 | const { model } = options; 4 | const node = ctx.project.currentDocument?.createNode(model) 5 | ctx.project.currentDocument?.insertNode(options.selectedNodes[0], node); 6 | } 7 | 8 | export const insertSibling = (ctx: ILowCodePluginContext, graph: any, options: { selectedNodes: any, model: any }) => { 9 | const { model } = options; 10 | const node = ctx.project.currentDocument?.createNode(model); 11 | const currentSelectedNode = options.selectedNodes[0]; 12 | ctx.project.currentDocument?.insertNode(currentSelectedNode.parent, node, currentSelectedNode.index + 1); 13 | } -------------------------------------------------------------------------------- /packages/plugin-g6-designer/src/builtin-commands/remove-item.ts: -------------------------------------------------------------------------------- 1 | import { ILowCodePluginContext } from '@alilc/lowcode-engine'; 2 | export const removeItem = (ctx: ILowCodePluginContext, graph: any, selectedNodes: any[]) => { 3 | selectedNodes?.forEach(node => { ctx.project.currentDocument?.removeNode(node.id) }); 4 | } -------------------------------------------------------------------------------- /packages/plugin-g6-designer/src/builtin-commands/undo-redo.ts: -------------------------------------------------------------------------------- 1 | import { IPublicModelPluginContext } from '@alilc/lowcode-types'; 2 | export const undo = (ctx: IPublicModelPluginContext, graph: any) => { 3 | ctx.project.currentDocument?.history.back(); 4 | } 5 | export const redo = (ctx: IPublicModelPluginContext, graph: any) => { 6 | ctx.project.currentDocument?.history.forward(); 7 | } -------------------------------------------------------------------------------- /packages/plugin-g6-designer/src/builtin-commands/zoom-in-out.ts: -------------------------------------------------------------------------------- 1 | import { ILowCodePluginContext } from '@alilc/lowcode-engine'; 2 | export const zoomIn = (ctx: ILowCodePluginContext, graph: any) => { 3 | graph.zoom(1.1); 4 | } 5 | export const zoomOut = (ctx: ILowCodePluginContext, graph: any) => { 6 | graph.zoom(0.9); 7 | } -------------------------------------------------------------------------------- /packages/plugin-g6-designer/src/graph/behaviors/click-item.ts: -------------------------------------------------------------------------------- 1 | import { isEdge, getGraphState, clearSelectedState } from '../utils'; 2 | import { ItemState, GraphState, EditorEvent } from '@alilc/lce-graph-tools'; 3 | import { Behavior } from '../interfaces'; 4 | import * as G6 from '@antv/g6'; 5 | import { get } from 'lodash'; 6 | import { rootState } from '../../items/state'; 7 | 8 | export interface ClickItemBehavior extends Behavior { 9 | /** 处理点击事件 */ 10 | handleItemClick({ item }: { item: G6.Item }): void; 11 | /** 处理画布点击 */ 12 | handleCanvasClick(): void; 13 | /** 处理按键按下 */ 14 | handleKeyDown(e: KeyboardEvent): void; 15 | /** 处理按键抬起 */ 16 | handleKeyUp(e: KeyboardEvent): void; 17 | } 18 | 19 | export interface DefaultConfig { 20 | /** 是否支持多选 */ 21 | multiple: boolean; 22 | /** 是否按下多选 */ 23 | keydown: boolean; 24 | /** 多选按键码值 */ 25 | keyCode: number; 26 | } 27 | 28 | const clickItemBehavior: ClickItemBehavior & ThisType = { 29 | getDefaultCfg(): DefaultConfig { 30 | return { 31 | multiple: true, 32 | keydown: false, 33 | keyCode: 16, 34 | }; 35 | }, 36 | 37 | getEvents() { 38 | return { 39 | 'node:click': 'handleItemClick', 40 | 'edge:click': 'handleItemClick', 41 | 'canvas:click': 'handleCanvasClick', 42 | keydown: 'handleKeyDown', 43 | keyup: 'handleKeyUp', 44 | }; 45 | }, 46 | 47 | handleItemClick(e) { 48 | const eleName = get(e, 'target.attrs.name'); 49 | if (eleName === 'collapse') return; 50 | const { item } = e; 51 | if (isEdge(item)) { 52 | return; 53 | } 54 | const nodeId = item.get('id'); 55 | const isSelected = item && item.hasState(ItemState.Selected); 56 | if (this.multiple && this.keydown) { 57 | const currentSelected = rootState.selected; 58 | if (isSelected) { 59 | rootState.setSelected(currentSelected.filter(itemId => itemId !== nodeId)); 60 | } else { 61 | rootState.setSelected(currentSelected.concat(nodeId)); 62 | } 63 | } else { 64 | if (isSelected) { 65 | rootState.setSelected([]); 66 | } else { 67 | rootState.setSelected([nodeId]); 68 | } 69 | } 70 | }, 71 | 72 | handleCanvasClick() { 73 | const { graph } = this; 74 | clearSelectedState(graph!); 75 | rootState.setSelected([]); 76 | }, 77 | 78 | handleKeyDown(e) { 79 | this.keydown = (e.keyCode || e.which) === this.keyCode; 80 | }, 81 | 82 | handleKeyUp() { 83 | this.keydown = false; 84 | }, 85 | }; 86 | 87 | export default { 88 | config: clickItemBehavior, 89 | name: 'click-item' 90 | }; 91 | -------------------------------------------------------------------------------- /packages/plugin-g6-designer/src/graph/behaviors/double-finger.ts: -------------------------------------------------------------------------------- 1 | import { Behavior } from '../interfaces'; 2 | 3 | export interface DoubleFingerDragCanvas extends Behavior { 4 | onWheel(ev: any): void; 5 | } 6 | 7 | const doubleFingerDragCanvas: DoubleFingerDragCanvas = { 8 | getEvents() { 9 | return { 10 | wheel: 'onWheel' 11 | }; 12 | }, 13 | onWheel(ev) { 14 | if (ev.ctrlKey) { 15 | const canvas = this.graph!.get('canvas'); 16 | const pixelRatio = canvas.get('pixelRatio') || 1; 17 | const point = canvas.getPointByClient(ev.clientX, ev.clientY); 18 | let ratio = this.graph!.getZoom(); 19 | if (ev.wheelDelta > 0) { 20 | ratio += ratio * 0.05; 21 | } else { 22 | ratio -= ratio * 0.05; 23 | } 24 | this.graph!.zoomTo(ratio, { 25 | x: point.x / pixelRatio, 26 | y: point.y / pixelRatio, 27 | }); 28 | } else { 29 | const x = ev.deltaX || ev.movementX; 30 | const y = ev.deltaY || ev.movementY; 31 | this.graph!.translate(-x, -y); 32 | } 33 | ev.preventDefault(); 34 | } 35 | }; 36 | 37 | export default { 38 | config: doubleFingerDragCanvas, 39 | name: 'double-finger-drag-canvas' 40 | } 41 | -------------------------------------------------------------------------------- /packages/plugin-g6-designer/src/graph/behaviors/hover-item.ts: -------------------------------------------------------------------------------- 1 | import { get } from 'lodash'; 2 | import { Behavior } from '../interfaces'; 3 | import * as G6 from '@antv/g6'; 4 | import { rootState } from '../../items/state'; 5 | 6 | export interface HoverItemBehavior extends Behavior { 7 | /** 处理鼠标进入 */ 8 | handleItemMouseenter({ item }: { item: G6.Item }): void; 9 | /** 处理鼠标移出 */ 10 | handleItemMouseleave({ item }: { item: G6.Item }): void; 11 | } 12 | 13 | const hoverItemBehavior: HoverItemBehavior = { 14 | getEvents() { 15 | return { 16 | 'node:mouseenter': 'handleItemMouseenter', 17 | 'edge:mouseenter': 'handleItemMouseenter', 18 | 'node:mouseleave': 'handleItemMouseleave', 19 | 'edge:mouseleave': 'handleItemMouseleave', 20 | }; 21 | }, 22 | 23 | handleItemMouseenter(e) { 24 | const eleName = get(e, 'target.attrs.name'); 25 | if (eleName === 'collapse') return; 26 | const { item } = e; 27 | rootState.setHovered(item.get('id')); 28 | }, 29 | 30 | handleItemMouseleave() { 31 | rootState.setHovered(''); 32 | }, 33 | }; 34 | 35 | export default { 36 | config: hoverItemBehavior, 37 | name: 'hover-item' 38 | } 39 | -------------------------------------------------------------------------------- /packages/plugin-g6-designer/src/graph/behaviors/index.ts: -------------------------------------------------------------------------------- 1 | import { default as clickItem } from './click-item'; 2 | import { default as doubleFinger } from './double-finger'; 3 | import { default as hoverItem } from './hover-item'; 4 | 5 | export default [clickItem, hoverItem, doubleFinger]; -------------------------------------------------------------------------------- /packages/plugin-g6-designer/src/graph/initGraph.tsx: -------------------------------------------------------------------------------- 1 | import G6, { GraphOptions } from '@antv/g6'; 2 | import { MixedTreeLayout } from './layouts'; 3 | import g6Designer from '../designer'; 4 | import { get } from 'lodash'; 5 | 6 | export const initTreeGraph = (container: HTMLElement, graphData: Function) => { 7 | // 适应画布 8 | const getContainerSize = () => { 9 | const leftPanel = document.querySelector('.lc-left-area')?.clientWidth || 0; 10 | const rightPanel = document.querySelector('.lc-right-area')?.clientWidth || 0; 11 | return { 12 | width: document.body.offsetWidth - leftPanel - rightPanel, 13 | height: (document.querySelector('.lc-main-area')?.clientHeight || 0), 14 | }; 15 | }; 16 | const treeLayout = new MixedTreeLayout(g6Designer.layoutConfig); 17 | 18 | // @ts-ignore 19 | const defaultOption = { 20 | container, 21 | fitCenter: true, 22 | fitView: true, 23 | linkCenter: true, 24 | animate: false, 25 | groupByTypes: true, 26 | modes: { 27 | default: [ 28 | 'drag-canvas', 29 | 'click', 30 | 'click-item', 31 | 'hover-item', 32 | 'double-finger-drag-canvas', 33 | { 34 | type: 'collapse-expand', 35 | trigger: 'click', 36 | shouldBegin: (e) => { 37 | const eleName = get(e, 'target.attrs.name'); 38 | if (eleName === 'collapse') return true; 39 | return false; 40 | }, 41 | }, 42 | ], 43 | }, 44 | defaultEdge: { 45 | type: 'trident', 46 | }, 47 | defaultNode: { 48 | type: 'demo-shape' 49 | }, 50 | layout: (data: any) => { 51 | const layoutData = treeLayout.layout(data); 52 | return layoutData; 53 | }, 54 | width: getContainerSize().width || 500, 55 | height: getContainerSize().height || 500, 56 | } as GraphOptions; 57 | 58 | const treeGraph = new G6.TreeGraph({ 59 | ...defaultOption, 60 | ...g6Designer.graphConfig 61 | }); 62 | return treeGraph; 63 | } 64 | 65 | export const initGraph = (container: HTMLElement) => { 66 | // 适应画布 67 | const getContainerSize = () => { 68 | const leftPanel = document.querySelector('.lc-left-area')?.clientWidth || 0; 69 | const rightPanel = document.querySelector('.lc-right-area')?.clientWidth || 0; 70 | return { 71 | width: document.body.offsetWidth - leftPanel - rightPanel, 72 | height: document.querySelector('.lc-main-area')?.clientHeight || 0, 73 | }; 74 | }; 75 | const defaultOption = { 76 | container, 77 | fitCenter: true, 78 | linkCenter: true, 79 | animate: false, 80 | groupByTypes: true, 81 | modes: { 82 | default: [ 83 | 'drag-canvas', 84 | 'click', 85 | 'click-item', 86 | 'hover-item', 87 | 'double-finger-drag-canvas', 88 | // 'collapse-expand' 89 | ], 90 | }, 91 | defaultEdge: { 92 | type: 'trident', 93 | }, 94 | defaultNode: { 95 | type: 'demo-shape' 96 | }, 97 | width: getContainerSize().width || 500, 98 | height: getContainerSize().height || 500, 99 | } as GraphOptions; 100 | 101 | const graph = new G6.Graph({ 102 | ...defaultOption, 103 | ...g6Designer.graphConfig 104 | }); 105 | 106 | return graph; 107 | } -------------------------------------------------------------------------------- /packages/plugin-g6-designer/src/graph/layouts/layout/customLayout.js: -------------------------------------------------------------------------------- 1 | import TreeLayout from "@antv/hierarchy/lib/layout/base"; 2 | import util from '@antv/hierarchy/lib/util'; 3 | 4 | function getBBoxTopDistance(root) { 5 | if (!root) return 0; 6 | if (root.height === root.totalHeight) { 7 | return 0; 8 | } 9 | let startY = 0; 10 | root.eachNode(node => { 11 | if (node.y < startY) { 12 | startY = node.y; 13 | } 14 | }); 15 | return root.y - startY; 16 | } 17 | 18 | function getBBoxBottomDistance(root) { 19 | if (!root) return 0; 20 | if (root.height === root.totalHeight) { 21 | return 0; 22 | } 23 | let endY = 0; 24 | root.eachNode(node => { 25 | if (node.y + node.height > endY) { 26 | endY = node.y + node.height; 27 | } 28 | }); 29 | return endY - root.y; 30 | } 31 | 32 | 33 | class CompactBoxTreeLayout extends TreeLayout { 34 | execute() { 35 | const me = this; 36 | this.rootNode.children.forEach((item, index) => { 37 | if (index !== 0) { 38 | item.x = me.rootNode.children[0].x; 39 | const preNode = me.rootNode.children[index - 1]; 40 | const preNodeSubLayout = preNode.data.subLayout; 41 | const itemSubLayout = item.data.subLayout; 42 | item.y = preNode.y + preNodeSubLayout && getBBoxBottomDistance(preNodeSubLayout) || preNode.height + getBBoxTopDistance(itemSubLayout); 43 | } 44 | }); 45 | return this.rootNode; 46 | } 47 | } 48 | 49 | const DEFAULT_OPTIONS = { 50 | }; 51 | 52 | function customLayout(root, options) { 53 | options = util.assign({}, DEFAULT_OPTIONS, options); 54 | return new CompactBoxTreeLayout(root, options).execute(); 55 | } 56 | 57 | export default customLayout; -------------------------------------------------------------------------------- /packages/plugin-g6-designer/src/graph/layouts/layout/customTreeGrid.js: -------------------------------------------------------------------------------- 1 | import TreeLayout from "@antv/hierarchy/lib/layout/base"; 2 | import util from '@antv/hierarchy/lib/util'; 3 | 4 | 5 | function transTree(root, callback) { 6 | let nodes = [root]; 7 | let current; 8 | while (current = nodes.shift()) { 9 | callback(current); 10 | if (current.children) { 11 | nodes = nodes.concat(current.children); 12 | } 13 | } 14 | } 15 | 16 | class CustomTreeGrid extends TreeLayout { 17 | execute() { 18 | // 树转换成二维数组 19 | const me = this; 20 | const tMap = {}; 21 | transTree(this.rootNode, (node) => { 22 | if (tMap[node.depth]) { 23 | tMap[node.depth].push(node); 24 | } else { 25 | tMap[node.depth] = [node]; 26 | } 27 | node.depth = node.data.depth || node.depth; 28 | }); 29 | Object.keys(tMap).forEach(i => { 30 | // 按行 31 | if (i > 0) { 32 | tMap[i].forEach((j, index) => { 33 | // 按列 34 | if (index > 0) { 35 | const preNode = tMap[i][index - 1]; 36 | j.x = preNode.x + preNode.width + me.options.vGap; 37 | j.y = preNode.y || 0; 38 | j.lineMaxHeight = j.height > (preNode.lineMaxHeight || preNode.height || 0) ? j.height : (preNode.lineMaxHeight || preNode.height || 0); 39 | } else { 40 | const preLine = tMap[i - 1]; 41 | j.y = (preLine[0].y || 0) + (preLine[preLine.length - 1].lineMaxHeight || preLine[preLine.length - 1].height || 0) + me.options.hGap; 42 | j.lineMaxHeight = j.height; 43 | } 44 | }); 45 | const lastNode = tMap[i][tMap[i].length - 1]; 46 | const lastNodeRight = lastNode.x + lastNode.width; 47 | tMap[i].forEach((j, index) => { 48 | // 每个节点都带上本行最大高度 49 | if (j.data) { 50 | j.data.lineMaxHeight = lastNode.lineMaxHeight - 2 * lastNode.vgap; // 不包含gap的最大高度 51 | } 52 | // 居中处理 53 | j.x = j.x - (lastNodeRight - this.rootNode.width)/2; 54 | }); 55 | } 56 | }); 57 | this.rootNode.hasAlign = true; 58 | return this.rootNode; 59 | } 60 | } 61 | 62 | 63 | const DEFAULT_OPTIONS = { 64 | vGap: 20, 65 | hGap: 20, 66 | }; 67 | 68 | function customLayout(root, options) { 69 | options = util.assign({}, DEFAULT_OPTIONS, options); 70 | return new CustomTreeGrid(root, options).execute(); 71 | } 72 | 73 | export default customLayout; -------------------------------------------------------------------------------- /packages/plugin-g6-designer/src/graph/layouts/layout/indented.ts: -------------------------------------------------------------------------------- 1 | import TreeLayout from '@antv/hierarchy/lib/layout/base'; 2 | 3 | const DEFAULT_INDENT = 20; 4 | /* 纵向布局 */ 5 | function positionNode(node: any, previousNode: any, parent: any, dx: number) { 6 | let externalGap = 0; 7 | if ( 8 | previousNode // 存在上个节点 9 | && previousNode.data 10 | && (previousNode.data.children && previousNode.data.children.length > 0) 11 | ) { 12 | externalGap = 15; 13 | } 14 | // 控制left offset,要与edges.js里的source anchor匹配 15 | if (parent) { 16 | if (parent.width < 100) { 17 | node.leftOffset = node.width / 2 + parent.leftOffset; 18 | } else { 19 | node.leftOffset = node.width / 2 + parent.leftOffset - parent.width / 2 + 50; 20 | } 21 | } else { 22 | node.leftOffset = node.width / 2; 23 | } 24 | node.x += dx * node.depth + node.leftOffset; 25 | node.y = (previousNode ? previousNode.y + previousNode.height / 2 : 0) 26 | + node.height / 2 + externalGap; 27 | } 28 | 29 | const indentedTree = (root: any, indent = DEFAULT_INDENT) => { 30 | let previousNode: any = null; 31 | root.eachNode((node: any) => { 32 | positionNode(node, previousNode, node.parent, indent); 33 | previousNode = node; 34 | }); 35 | }; 36 | 37 | const VALID_DIRECTIONS = [ 38 | 'LR', // left to right 39 | 'RL', // right to left 40 | ]; 41 | const DEFAULT_DIRECTION = VALID_DIRECTIONS[0]; 42 | 43 | class IndentedLayout extends TreeLayout { 44 | execute() { 45 | const me: any = this; 46 | const options = me.options; 47 | const root = me.rootNode; 48 | options.isHorizontal = true; 49 | const indent = options.indent; 50 | const direction = options.direction || DEFAULT_DIRECTION; 51 | if (direction && VALID_DIRECTIONS.indexOf(direction) === -1) { 52 | throw new TypeError(`Invalid direction: ${direction}`); 53 | } 54 | if (direction === VALID_DIRECTIONS[0]) { // LR 55 | indentedTree(root, indent); 56 | } else if (direction === VALID_DIRECTIONS[1]) { // RL 57 | indentedTree(root, indent); 58 | root.right2left(); 59 | } 60 | return root; 61 | } 62 | } 63 | 64 | const DEFAULT_OPTIONS = { 65 | }; 66 | 67 | function indentedLayout(root: any, options: any) { 68 | options = Object.assign({}, DEFAULT_OPTIONS, options); 69 | // @ts-ignore 70 | return new IndentedLayout(root, options).execute(); 71 | } 72 | 73 | export default indentedLayout; 74 | -------------------------------------------------------------------------------- /packages/plugin-g6-designer/src/graph/shapes/default-shape.ts: -------------------------------------------------------------------------------- 1 | const defaultShape = { 2 | config: { 3 | draw(model: any, group: any) { 4 | const { size = [300, 180] } = model || {}; 5 | const keyShape = group.addShape('rect', { 6 | attrs: { 7 | width: size[0] - 8, // 内部有margin10 8 | height: size[1] - 8, 9 | name: 'key-shape', 10 | fill: '#fff', 11 | radius: 6 12 | }, 13 | className: 'wrapper-border', 14 | }); 15 | const gRenderer = (window as any).gRendererMaps[model.id]; 16 | gRenderer && gRenderer.render(group); 17 | group.sort(); 18 | 19 | return keyShape; 20 | }, 21 | 22 | update(model: any, item: any) { 23 | const gRenderer = (window as any).gRendererMaps[model.id]; 24 | const group = item.getContainer(); 25 | gRenderer && gRenderer.update(group); 26 | const wrapper = group.findByClassName('wrapper-border'); 27 | wrapper && wrapper.attr({ 28 | width: model.width, 29 | height: model.height 30 | }); 31 | } 32 | }, 33 | name: 'default-shape', 34 | }; 35 | export default defaultShape; 36 | -------------------------------------------------------------------------------- /packages/plugin-g6-designer/src/index.ts: -------------------------------------------------------------------------------- 1 | import { ILowCodePluginContext, project } from '@alilc/lowcode-engine'; 2 | import DesignerView from './DesignerView'; 3 | // import MaterialPane from './MaterialPane'; 4 | import { rootState } from './items/state'; 5 | import g6Designer, { G6Designer } from './designer'; 6 | 7 | export interface IOptions { 8 | type: 'tree' | 'graph', 9 | graphConfig: any, 10 | layoutConfig: any 11 | } 12 | 13 | /** 14 | * plugin G6 designer 15 | * @param ctx 16 | * @returns 17 | */ 18 | const PluginG6Designer = ( 19 | ctx: ILowCodePluginContext, 20 | options: IOptions = { 21 | type: 'tree', 22 | graphConfig: {}, 23 | layoutConfig: {} 24 | }) => { 25 | return { 26 | exports() { 27 | return g6Designer; 28 | }, 29 | init() { 30 | const { skeleton, project } = ctx; 31 | skeleton.remove({ 32 | name: 'designer', 33 | area: 'mainArea', 34 | type: 'Widget' 35 | }); 36 | skeleton.add({ 37 | area: 'mainArea', 38 | name: 'designer', 39 | type: 'Widget', 40 | content: DesignerView, 41 | contentProps: { 42 | ctx, 43 | ...options 44 | } 45 | }); 46 | // bind nodes state 47 | rootState.init(project.currentDocument, options.type); 48 | project.onChangeDocument((doc) => { 49 | rootState.disposeDocumentEvent(); 50 | rootState.init(project.currentDocument, options.type); 51 | }); 52 | } 53 | } 54 | } 55 | 56 | PluginG6Designer.pluginName = 'plugin-g6-designer'; 57 | PluginG6Designer.meta = { 58 | preferenceDeclaration: { 59 | title: 'g6图类型', 60 | properties: [ 61 | { 62 | key: 'type', 63 | type: 'string', 64 | description: '用户自定义g6图类型', 65 | }, 66 | { 67 | key: 'graphConfig', 68 | type: 'any', 69 | description: '图配置' 70 | }, 71 | { 72 | key: 'layoutConfig', 73 | type: 'any', 74 | description: '布局配置' 75 | } 76 | ] 77 | } 78 | }; 79 | export default PluginG6Designer; 80 | export { G6Designer }; 81 | -------------------------------------------------------------------------------- /packages/plugin-g6-designer/src/items/edge/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Graph, TreeGraph, ModelConfig } from '@antv/g6'; 3 | import { project } from '@alilc/lowcode-engine'; 4 | import { Node } from '@alilc/lowcode-engine'; 5 | export interface IModelConfig extends ModelConfig { 6 | id: string; 7 | [key: string]: any; 8 | } 9 | interface Props { 10 | onMountEdge: (edge: Node) => void; 11 | onUnMountEdge: (edge: any) => void; 12 | 13 | graph: Graph | TreeGraph; 14 | lceNode: Node; 15 | } 16 | 17 | class EdgeComponent extends React.PureComponent { 18 | 19 | componentDidMount() { 20 | const { lceNode } = this.props; 21 | 22 | // 收集 edge 统一添加到画布 23 | this.props.onMountEdge(lceNode); 24 | 25 | // set view state 26 | this.setViewState(); 27 | 28 | // model 更新渲染 29 | project.currentDocument?.onChangeNodeProp(({ key, oldValue, newValue, node }) => { 30 | 31 | }); 32 | } 33 | 34 | componentDidUpdate(prevProps: Props) { 35 | } 36 | 37 | setViewState() { 38 | } 39 | 40 | componentWillUnmount() { 41 | // 删除节点 42 | this.props.onUnMountEdge(this.props.lceNode.propsData); 43 | } 44 | 45 | render() { 46 | return null; 47 | } 48 | 49 | } 50 | 51 | export default EdgeComponent; 52 | 53 | -------------------------------------------------------------------------------- /packages/plugin-g6-designer/src/items/index.less: -------------------------------------------------------------------------------- 1 | .x6-widget-selection-box { 2 | z-index: -10; 3 | } 4 | 5 | .x6-widget-stencil-content { 6 | border: 1px solid #EDEDED; 7 | box-shadow: 0 4px 15px 0 rgb(31 56 88 / 15%); 8 | border-radius: 4px; 9 | background-color: #FFFFFF; 10 | } 11 | 12 | .next-overlay-wrapper .next-balloon { 13 | padding: 10px 16px; 14 | } 15 | 16 | .x6-port-body { 17 | &:hover { 18 | color: #4C6079; 19 | transform: scale(1.3); 20 | } 21 | } -------------------------------------------------------------------------------- /packages/plugin-g6-designer/src/items/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import NodeComponent, { IModelConfig } from './node'; 4 | import EdgeComponent from './edge'; 5 | import { Graph, TreeGraph } from '@antv/g6'; 6 | import { EdgeComponentName, rootState, transTree } from "./state"; 7 | import { Node } from '@alilc/lowcode-engine'; 8 | import { merge } from 'lodash'; 9 | 10 | import './index.less'; 11 | 12 | 13 | 14 | interface Props { 15 | graph: Graph | TreeGraph; 16 | } 17 | 18 | /** 19 | * 渲染节点 & 线条组件 20 | * 响应数据: nodes 21 | */ 22 | @observer 23 | class Items extends React.PureComponent { 24 | constructor(props: any) { 25 | super(props); 26 | (window as any).gRendererMaps = {}; 27 | } 28 | 29 | tree: any = {}; 30 | mounted: boolean = false; // 是否 didMounted 31 | 32 | componentDidMount() { 33 | const { graph } = this.props; 34 | const data = rootState.getGraphData(); 35 | graph.data(data); 36 | graph.paint(); 37 | graph.render(); 38 | graph.fitView(); 39 | this.mounted = true; 40 | } 41 | 42 | onMountNode = (node: Node) => { 43 | const { graph } = this.props; 44 | if (this.mounted) { 45 | if (rootState.type === 'tree') { 46 | const t: any = node.parent!.schema.children; 47 | const newChildren = t.map((i: any) => { 48 | transTree(i, (j: any) => { 49 | merge(j, j.props); 50 | }); 51 | return i; 52 | }); 53 | (graph as TreeGraph).updateChildren(newChildren, node.parent!.id); 54 | // (graph as TreeGraph).addChild(merge(node.propsData, node.propsData!.nodeLayout, { id: node.id }), node.parent!.id); 55 | rootState.setSelected([node.id]); 56 | } 57 | } 58 | } 59 | 60 | onUnMountNode = (nodeId: string) => { 61 | const { graph } = this.props; 62 | if (this.mounted) { 63 | if (rootState.type === 'tree') { 64 | (graph as TreeGraph).removeChild(nodeId); 65 | } 66 | } 67 | } 68 | 69 | onMountEdge = (edge: Node) => { 70 | const { graph } = this.props; 71 | if (this.mounted) { 72 | graph.addItem('edge', edge.propsData); 73 | } 74 | } 75 | 76 | onUnMountEdge = (edge: IModelConfig) => { 77 | const { graph } = this.props; 78 | 79 | if (this.mounted) { 80 | graph.removeItem(edge.id); 81 | } 82 | } 83 | 84 | render() { 85 | const { graph } = this.props; 86 | return ( 87 |
88 | { 89 | rootState.nodes.map(item => { 90 | const curId = item.id; 91 | if (item.componentName === EdgeComponentName) { 92 | return ( 93 | 100 | ) 101 | } else { 102 | return ( 103 | ) 114 | } 115 | }) 116 | } 117 | {/* */} 118 |
119 | ) 120 | } 121 | } 122 | 123 | export default Items; 124 | -------------------------------------------------------------------------------- /packages/plugin-g6-designer/src/items/node/index.tsx: -------------------------------------------------------------------------------- 1 | import { Graph, TreeGraph, ModelConfig } from '@antv/g6'; 2 | import React from 'react'; 3 | import { Node as LceNode } from '@alilc/lowcode-engine'; 4 | import { ItemState } from '@alilc/lce-graph-tools'; 5 | import { calcLayout } from '../../graph/utils'; 6 | 7 | export interface IModelConfig extends ModelConfig { 8 | id: string; 9 | [key: string]: any; 10 | } 11 | 12 | interface Props { 13 | onMountNode: (node: any) => void; 14 | onUnMountNode: (nodeId: string) => void; 15 | 16 | graph: Graph | TreeGraph; 17 | lceNode: LceNode; 18 | 19 | selected: boolean; 20 | hovered: boolean; 21 | error: boolean; 22 | changeFlag: number | undefined; 23 | } 24 | 25 | /** 26 | * node component for x6 node render 27 | */ 28 | class NodeComponent extends React.PureComponent { 29 | componentDidMount() { 30 | // 添加节点 31 | const { lceNode, selected, hovered, error } = this.props; 32 | // 计算element canvas定位 33 | const node = calcLayout(lceNode, selected, hovered, error); 34 | if (node) { 35 | const { width, height } = node.box; 36 | lceNode.setPropValue('height', height); 37 | lceNode.setPropValue('width', width); 38 | lceNode.setPropValue('size', [width, height]); 39 | } 40 | // 注册节点config 41 | this.props.onMountNode(lceNode); 42 | } 43 | 44 | componentDidUpdate(prevProps: Props) { 45 | const { changeFlag, selected, hovered, error } = this.props; 46 | if (prevProps.changeFlag !== changeFlag || selected !== prevProps.selected || hovered !== prevProps.hovered || error !== prevProps.error) { 47 | this.updateNode(); 48 | } 49 | if (selected !== prevProps.selected || hovered !== prevProps.hovered || error !== prevProps.error) { 50 | this.setNodeState(); 51 | } 52 | } 53 | 54 | updateNode() { 55 | const { lceNode, graph, selected, hovered, error } = this.props; 56 | const nodeLayout = calcLayout(lceNode, selected, hovered, error); 57 | if (nodeLayout) { 58 | const oldWidth = lceNode.getPropValue('width'); 59 | const oldHeight = lceNode.getPropValue('height'); 60 | const { width, height } = nodeLayout.box; 61 | lceNode.setPropValue('height', height); 62 | lceNode.setPropValue('width', width); 63 | lceNode.setPropValue('size', [width, height]); 64 | graph.updateItem(lceNode.id, { 65 | ...lceNode.propsData, 66 | height, 67 | width, 68 | size: [width, height], 69 | }); 70 | if (Math.abs(+oldWidth - +width) > 1 || Math.abs(+oldHeight - +height) > 1) { 71 | graph.layout(); 72 | } 73 | } else { 74 | graph.updateItem(lceNode.id, { 75 | ...lceNode.propsData 76 | }); 77 | graph.layout(); 78 | } 79 | } 80 | 81 | setNodeState() { 82 | const { lceNode, graph, selected, hovered, error } = this.props; 83 | const currentNode = graph.findById(lceNode.id); 84 | graph!.setItemState(currentNode, ItemState.Selected, selected); 85 | graph!.setItemState(currentNode, ItemState.Active, hovered); 86 | } 87 | 88 | componentWillUnmount() { 89 | // 删除节点 90 | const { lceNode } = this.props; 91 | this.props.onUnMountNode(lceNode.id); 92 | } 93 | 94 | render() { 95 | return null; 96 | } 97 | } 98 | 99 | export default NodeComponent; 100 | 101 | -------------------------------------------------------------------------------- /packages/plugin-g6-designer/src/items/state.ts: -------------------------------------------------------------------------------- 1 | import { observable, action, makeObservable } from "mobx"; 2 | import { DocumentModel } from '@alilc/lowcode-shell'; 3 | import { merge } from 'lodash'; 4 | 5 | export function transTree(node: any, callback: Function, parent?: any, depth = 0) { 6 | let flag = callback(node, parent, depth); 7 | if (flag != null) { 8 | return flag; 9 | } else { 10 | if (node.children && node.children.length) { 11 | for (let i = 0; i < node.children.length; i++) { 12 | const child = node.children[i]; 13 | flag = flag || transTree(child, callback, node, depth + 1); 14 | if (flag != null) { 15 | return flag; 16 | } 17 | } 18 | } 19 | } 20 | return null; 21 | } 22 | export class RootState { 23 | @observable selected: string[] = []; // select 态,节点 蓝色背景 & 蓝色框 24 | @observable hovered: string = ''; // hover 态,节点蓝色框 25 | @observable error: string[] = []; // error 态,节点红框 26 | @observable nodes: any[] = []; // 单独存一份 nodes 状态数据 27 | document: DocumentModel | null; 28 | type: 'graph' | 'tree' = 'tree'; 29 | 30 | documentEvent: (Function | void)[] = []; 31 | 32 | constructor() { 33 | makeObservable(this); 34 | } 35 | 36 | @action 37 | setNodes(nodes: any[]) { 38 | this.nodes = nodes; 39 | } 40 | 41 | getGraphData() { 42 | if (this.type === 'tree') { 43 | // 树布局 44 | const tree = this.document?.root?.exportSchema(); 45 | const availableTree = Array.isArray(tree?.children) && tree?.children[0] || {}; 46 | transTree(availableTree, (node: any) => { 47 | merge(node, node.props); 48 | }); 49 | return availableTree; 50 | } else { 51 | // 图布局 52 | } 53 | } 54 | 55 | disposeDocumentEvent() { 56 | this.documentEvent.forEach(d => d && d()); 57 | this.documentEvent = []; 58 | } 59 | 60 | init(document: DocumentModel | null, type: 'graph' | 'tree') { 61 | this.type = type; 62 | if (document) { 63 | this.document = document; 64 | this.setNodes(Array.from(document.nodesMap.values() || []).filter(node => node.componentName !== 'Page')); 65 | this.documentEvent = [ 66 | document.onImportSchema(() => { 67 | this.setNodes(Array.from(document.nodesMap.values() || []).filter(node => node.componentName !== 'Page')); 68 | }), 69 | document.onMountNode((node: { node: Node }) => { 70 | this.setNodes(this.nodes.concat(node.node)); 71 | }), 72 | document.onRemoveNode((node) => { 73 | const { prevSibling, nextSibling, parent } = node; 74 | const nodes = this.nodes.filter(v => v.id !== node.id); 75 | this.setNodes(nodes); 76 | // 支持连续删除节点 77 | setTimeout(() => { 78 | if (prevSibling) { 79 | this.setSelected([prevSibling.id]); 80 | } else if (nextSibling) { 81 | this.setSelected([nextSibling.id]); 82 | } else if (parent) { 83 | this.setSelected([parent.id]); 84 | } 85 | }, 100); 86 | }), 87 | document.onChangeNodeProp((data: any) => { 88 | const { node, key } = data; 89 | // 节点属性被更新时,判断时应用中间产物 90 | if (!['height', 'width', 'size', 'element'].includes(key)) { 91 | const { id } = node; 92 | this.nodes.map((item: any) => { 93 | if (item.id === id) { 94 | item.propsData.changeFlag = Math.random(); 95 | } 96 | return item; 97 | }); 98 | }; 99 | }), 100 | // 撤销恢复不会触发 onAddNode 和 onRemoveNode 事件 101 | document.history.onChangeState((...data: any) => { 102 | this.setNodes(Array.from(document.nodesMap.values() || []).filter(node => node.componentName !== 'Page')); 103 | }), 104 | ]; 105 | } 106 | } 107 | 108 | @action 109 | setSelected(ids: string[]) { 110 | this.selected = ids; 111 | this.document?.selection.selectAll(ids); 112 | } 113 | 114 | @action 115 | setHovered(id: string) { 116 | this.hovered = id; 117 | this.document?.detecting.capture(id); 118 | } 119 | 120 | @action 121 | setError(ids: string[]) { 122 | this.error = ids; 123 | } 124 | 125 | } 126 | 127 | export const rootState = new RootState(); -------------------------------------------------------------------------------- /packages/plugin-g6-designer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib" 5 | }, 6 | "include": [ 7 | "./src/" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/plugin-materials-pane/README.md: -------------------------------------------------------------------------------- 1 | TODO 2 | --- 3 | -------------------------------------------------------------------------------- /packages/plugin-materials-pane/build.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "build-plugin-component" 5 | ] 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /packages/plugin-materials-pane/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@alilc/lce-graph-materials-pane", 3 | "version": "1.0.12", 4 | "description": "for Graph Editor Engine", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "files": [ 8 | "lib", 9 | "es" 10 | ], 11 | "scripts": { 12 | "test": "jest -c jest.config.js", 13 | "build": "build-scripts build --skip-demo", 14 | "build:watch": "build-scripts build --skip-demo --watch" 15 | }, 16 | "license": "MIT", 17 | "publishConfig": { 18 | "access": "public", 19 | "registry": "https://registry.npmjs.org/" 20 | }, 21 | "devDependencies": { 22 | "@alib/build-scripts": "^0.1.18", 23 | "build-plugin-component": "^1.10.0" 24 | }, 25 | "gitHead": "7ed863fe077671622a9901f670a9311aa85c3615" 26 | } 27 | -------------------------------------------------------------------------------- /packages/plugin-materials-pane/src/icon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import IconBase from '@ali/vu-icon-base'; 3 | 4 | export default function Icon() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /packages/plugin-materials-pane/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { IPublicModelPluginContext } from "@alilc/lowcode-types"; 3 | import Pane from "./pane"; 4 | 5 | const Icon = 6 | const PluginMaterialsPane = (ctx: IPublicModelPluginContext) => { 7 | return { 8 | // 插件名,注册环境下唯一 9 | name: "LclaActionsPane", 10 | // 依赖的插件(插件名数组) 11 | dep: [], 12 | // 插件对外暴露的数据和方法 13 | exports() { 14 | return { 15 | data: "你可以把插件的数据这样对外暴露", 16 | func: () => { 17 | console.log("方法也是一样"); 18 | }, 19 | }; 20 | }, 21 | // 插件的初始化函数,在引擎初始化之后会立刻调用 22 | init() { 23 | // material.setAssets(AssetsJson); 24 | // 你可以拿到其他插件暴露的方法和属性 25 | // const { data, func } = ctx.plugins.pluginA; 26 | // func(); 27 | ctx.skeleton.add({ 28 | name: "logicActionPane", 29 | area: "leftArea", 30 | type: "PanelDock", 31 | content: , 32 | props: { 33 | align: 'left', 34 | icon: Icon, 35 | description: "动作库", 36 | }, 37 | panelProps: { 38 | area: 'leftFixedArea', 39 | floatable: true, 40 | hideTitleBar: false, 41 | title: "动作库", 42 | width: 192, 43 | }, 44 | }); 45 | 46 | ctx.skeleton.showPanel('logicActionPane'); 47 | }, 48 | }; 49 | }; 50 | 51 | PluginMaterialsPane.pluginName = 'plugin-materials-pane'; 52 | 53 | export default PluginMaterialsPane; -------------------------------------------------------------------------------- /packages/plugin-materials-pane/src/pane/Icon/Component.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default function SvgComponent({ className }) { 4 | return ( 5 | 14 | ic_component_placeholder 15 | 16 | 17 | 22 | 27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /packages/plugin-materials-pane/src/pane/Icon/component.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | ic_component_placeholder 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /packages/plugin-materials-pane/src/pane/Icon/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/plugin-materials-pane/src/pane/Icon/index.module.scss: -------------------------------------------------------------------------------- 1 | .icon { 2 | display: inline-block; 3 | 4 | width: 20px; 5 | height: 20px; 6 | 7 | > svg { 8 | height: 100%; 9 | width: 100%; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/plugin-materials-pane/src/pane/Icon/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import cls from 'classnames/bind'; 3 | import Svg from './icon.svg'; 4 | import style from './index.module.scss'; 5 | 6 | const cx = cls.bind(style); 7 | 8 | export default function Icon() { 9 | return ( 10 |
11 | 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /packages/plugin-materials-pane/src/pane/components/Category/index.module.scss: -------------------------------------------------------------------------------- 1 | .category { 2 | // padding: 0 8px; 3 | 4 | scrollbar-width: none; /* firefox */ 5 | -ms-overflow-style: none; /* IE 10+ */ 6 | 7 | &::-webkit-scrollbar { 8 | display: none; /* Chrome Safari */ 9 | } 10 | 11 | .header { 12 | height: 32px; 13 | display: flex; 14 | flex-direction: row; 15 | align-items: center; 16 | border-top: 1px solid #eaeef2; 17 | } 18 | 19 | .title { 20 | flex: 1; 21 | font-size: 12px; 22 | color: rgba(0,0,0,0.8); 23 | } 24 | 25 | .icon { 26 | width: 32px; 27 | flex-grow: 0; 28 | flex-shrink: 0; 29 | text-align: center; 30 | cursor: pointer; 31 | transition: transform 0.2s ease-in-out; 32 | 33 | &.expand { 34 | transform: rotate(90deg); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/plugin-materials-pane/src/pane/components/Category/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import cls from "classnames/bind"; 3 | import { Animate, Icon } from "@alifd/next"; 4 | import style from "./index.module.scss"; 5 | 6 | const cx = cls.bind(style); 7 | 8 | interface Props { 9 | name: string; 10 | children?: React.ReactElement; 11 | } 12 | 13 | interface State { 14 | expand: boolean; 15 | } 16 | 17 | export default class Category extends React.Component { 18 | state = { 19 | expand: true, 20 | }; 21 | 22 | height = 0; 23 | 24 | handleToggle = () => { 25 | this.setState((state) => { 26 | return { 27 | expand: !state.expand, 28 | }; 29 | }); 30 | }; 31 | 32 | beforeEnter = (node) => { 33 | this.height = node.offsetHeight; 34 | node.style.height = "0px"; 35 | }; 36 | 37 | onEnter = (node) => { 38 | node.style.height = `${this.height}px`; 39 | }; 40 | 41 | afterEnter = (node) => { 42 | this.height = null; 43 | node.style.height = null; 44 | }; 45 | 46 | beforeLeave = (node) => { 47 | node.style.height = `${node.offsetHeight}px`; 48 | }; 49 | 50 | onLeave = (node) => { 51 | node.style.height = "0px"; 52 | }; 53 | 54 | afterLeave = (node) => { 55 | node.style.height = null; 56 | }; 57 | 58 | render() { 59 | const { children, name } = this.props; 60 | const { expand } = this.state; 61 | return ( 62 |
63 |
64 |
65 | 70 |
71 |
{name}
72 |
73 | 82 | {expand ? children : null} 83 | 84 |
85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /packages/plugin-materials-pane/src/pane/components/Component/index.module.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: space-between; 6 | padding: 12px 8px 8px; 7 | width: calc(100 / 3 * 0.01); 8 | height: 64px; 9 | flex-grow: 0; 10 | flex-shrink: 0; 11 | border-right: 1px solid #eaeaea; 12 | border-bottom: 1px solid #eaeaea; 13 | box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.15); 14 | transition: box-shadow 0.2s ease; 15 | 16 | &:hover { 17 | box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.15); 18 | border-color: transparent; 19 | } 20 | 21 | &:nth-child(3n) { 22 | border-right: none; 23 | } 24 | 25 | &:nth-child(3n+1):nth-last-child(-n+3), 26 | &:nth-child(3n+1):nth-last-child(-n+3)~& { 27 | border-bottom: none; 28 | } 29 | 30 | .icon { 31 | width: 20px; 32 | height: 20px; 33 | margin: 0 1px; 34 | display: flex; 35 | justify-content: center; 36 | align-items: center; 37 | margin-right: 4px; 38 | 39 | >img { 40 | width: 100%; 41 | } 42 | } 43 | 44 | .name { 45 | width: 48px; 46 | height: 20px; 47 | line-height: 20px; 48 | white-space: nowrap; 49 | text-overflow: ellipsis; 50 | overflow: hidden; 51 | color: rgba(0, 0, 0, 0.6); 52 | text-align: center; 53 | } 54 | } -------------------------------------------------------------------------------- /packages/plugin-materials-pane/src/pane/components/Component/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import cls from 'classnames/bind'; 3 | import Svg from '../../Icon/Component'; 4 | import { Balloon } from '@alifd/next'; 5 | import style from './index.module.scss'; 6 | import { getTextReader, StandardComponentMeta } from '../../utils/transform'; 7 | 8 | const cx = cls.bind(style); 9 | 10 | interface Props { 11 | data: any; 12 | } 13 | 14 | interface State { 15 | icon: string | React.ReactNode; 16 | snippet: any; 17 | } 18 | 19 | export default class Component extends React.Component { 20 | static getDerivedStateFromProps(props) { 21 | const { data } = props; 22 | const { icon, snippets = [] } = data; 23 | const snippet = snippets[0]; 24 | const screenshot = snippet?.screenshot ?? icon; 25 | 26 | return { 27 | icon: screenshot, 28 | snippet, 29 | }; 30 | } 31 | 32 | state = { 33 | icon: '', 34 | snippet: null, 35 | }; 36 | 37 | t: (s) => string; 38 | 39 | constructor(props) { 40 | super(props); 41 | this.t = getTextReader('zh_CN'); 42 | } 43 | 44 | renderIcon() { 45 | const { icon } = this.state; 46 | 47 | if (!icon) { 48 | return ; 49 | } 50 | 51 | if (typeof icon === 'string') { 52 | return ; 53 | } 54 | 55 | if (typeof icon === 'function') { 56 | const X = icon as any; 57 | return ; 58 | } 59 | 60 | return icon; 61 | } 62 | 63 | render() { 64 | const { data } = this.props; 65 | const { title } = data; 66 | const { snippet } = this.state; 67 | 68 | return ( 69 |
70 |
{this.renderIcon()}
71 |
{this.t(title)}
72 |
73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/plugin-materials-pane/src/pane/components/DragGhost/index.scss: -------------------------------------------------------------------------------- 1 | .lc-ghost-group { 2 | box-sizing: border-box; 3 | position: fixed; 4 | z-index: 99999; 5 | width: 100px; 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | pointer-events: none; 10 | background-color: rgba(0, 0, 0, 0.4); 11 | box-shadow: 0 0 6px grey; 12 | transform: translate(-10%, -50%); 13 | .lc-ghost { 14 | .lc-ghost-title { 15 | text-align: center; 16 | font-size: var(--font-size-text); 17 | text-overflow: ellipsis; 18 | color: var(--color-text-light); 19 | white-space: nowrap; 20 | overflow: hidden; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/plugin-materials-pane/src/pane/components/DragGhost/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Title } from '@alilc/lowcode-editor-core'; 3 | import { I18nData, NodeSchema } from '@alilc/lowcode-types'; 4 | import './index.scss'; 5 | 6 | type offBinding = () => any; 7 | 8 | export default class DragGhost extends React.Component { 9 | 10 | private dispose: offBinding[] = []; 11 | private titles: (string | I18nData | React.ReactElement)[] | null = null; 12 | private _dragon = this.props.dragon; 13 | private _material = this.props.material; 14 | 15 | constructor(props: any) { 16 | super(props); 17 | this.state = { 18 | x: 0, 19 | y: 0 20 | } 21 | this.dispose = [ 22 | this._dragon.onDragstart(e => { 23 | if (e.originalEvent.type.slice(0, 4) === 'drag') { 24 | return; 25 | } 26 | this.titles = this.getTitles(e.dragObject); 27 | this.setState({ 28 | x: e.globalX, 29 | y: e.globalY 30 | }) 31 | }), 32 | this._dragon.onDrag(e => { 33 | this.setState({ 34 | x: e.globalX, 35 | y: e.globalY 36 | }) 37 | }), 38 | this._dragon.onDragend(() => { 39 | this.titles = null; 40 | this.setState({ 41 | x: 0, 42 | y: 0 43 | }) 44 | }), 45 | ]; 46 | } 47 | 48 | getTitles(dragObject: any) { 49 | if (dragObject && dragObject.type === "node") { 50 | return dragObject.nodes.map((node) => node.title); 51 | } 52 | 53 | const dataList = Array.isArray(dragObject.data) ? dragObject.data : [dragObject.data]; 54 | 55 | return dataList.map((item: NodeSchema, i) => (this._material.getComponentMeta(item.componentName).title)); 56 | } 57 | 58 | componentWillUnmount() { 59 | if (this.dispose) { 60 | this.dispose.forEach(off => off()); 61 | } 62 | } 63 | 64 | renderGhostGroup() { 65 | return this.titles?.map((title, i) => { 66 | const ghost = ( 67 |
68 | 69 | </div> 70 | ); 71 | return ghost; 72 | }); 73 | } 74 | 75 | render() { 76 | if (!this.titles || !this.titles.length) { 77 | return null; 78 | } 79 | 80 | return ( 81 | <div 82 | className="lc-ghost-group" 83 | style={{ 84 | left: this.state.x, 85 | top: this.state.y, 86 | }} 87 | > 88 | {this.renderGhostGroup()} 89 | </div> 90 | ); 91 | } 92 | } -------------------------------------------------------------------------------- /packages/plugin-materials-pane/src/pane/components/List/index.module.scss: -------------------------------------------------------------------------------- 1 | .cards { 2 | display: flex; 3 | flex-wrap: wrap; 4 | border-top: 1px solid #eaeef2; 5 | } 6 | -------------------------------------------------------------------------------- /packages/plugin-materials-pane/src/pane/components/List/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import cls from 'classnames/bind'; 3 | import style from './index.module.scss'; 4 | 5 | const cx = cls.bind(style); 6 | 7 | interface Props { 8 | className?: string; 9 | children?: React.ReactNode 10 | } 11 | 12 | export default class List extends React.Component<Props> { 13 | render() { 14 | const { className } = this.props; 15 | return <div className={cls(className, cx('cards'))}>{this.props.children}</div>; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/plugin-materials-pane/src/pane/components/Tab/index.module.scss: -------------------------------------------------------------------------------- 1 | .tab { 2 | display: flex; 3 | flex-direction: column; 4 | 5 | .header { 6 | position: relative; 7 | flex-grow: 0; 8 | flex-shrink: 0; 9 | 10 | .items { 11 | display: flex; 12 | height: 32px; 13 | box-shadow: inset 0 -1px 0 0 rgba(0, 0, 0, 0.07); 14 | } 15 | 16 | .item { 17 | padding: 0 16px; 18 | flex-shrink: 0; 19 | flex-grow: 0; 20 | display: flex; 21 | height: 100%; 22 | align-items: center; 23 | justify-content: center; 24 | color: #666; 25 | font-size: 12px; 26 | cursor: pointer; 27 | 28 | &:hover { 29 | color: #000; 30 | } 31 | 32 | &.active { 33 | color: black; 34 | } 35 | } 36 | 37 | .indicator { 38 | content: ''; 39 | position: absolute; 40 | bottom: 0; 41 | left: 0; 42 | width: 72px; 43 | height: 1px; 44 | background-color: black; 45 | transition: left 0.3s ease; 46 | } 47 | } 48 | 49 | .tabs { 50 | flex: 1; 51 | overflow: hidden; 52 | 53 | .contents { 54 | height: 100%; 55 | display: flex; 56 | flex-direction: row; 57 | flex-wrap: nowrap; 58 | transform: translate3d(0, 0, 0); 59 | transition: transform 0.3s ease-in-out; 60 | } 61 | 62 | .content { 63 | width: 100%; 64 | height: 100%; 65 | // overflow-y: overlay; 66 | overflow-y: hidden; 67 | overflow-x: hidden; 68 | flex-shrink: 0; 69 | flex-grow: 0; 70 | opacity: 0; 71 | transition: opacity 0.3s ease-in-out; 72 | 73 | // &::-webkit-scrollbar-thumb { 74 | // background-color: #999; 75 | // } 76 | 77 | &.active { 78 | opacity: 1; 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /packages/plugin-materials-pane/src/pane/components/Tab/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import cls from 'classnames/bind'; 3 | import style from './index.module.scss'; 4 | 5 | const cx = cls.bind(style); 6 | 7 | interface Props { 8 | className?: string; 9 | children?: React.ReactNode; 10 | } 11 | 12 | interface State { 13 | active: number; 14 | offset: number; 15 | } 16 | 17 | export default class Tab extends React.Component<Props, State> { 18 | static Item; 19 | 20 | state: State = { 21 | active: 0, 22 | offset: 0, 23 | }; 24 | 25 | menus = React.createRef<HTMLDivElement>(); 26 | 27 | componentDidMount() { 28 | const offset = this.getIndicatorPos(); 29 | // eslint-disable-next-line 30 | this.setState({ offset }); 31 | } 32 | 33 | getIndicatorPos = (index?: number) => { 34 | const { active } = this.state; 35 | 36 | index = typeof index === 'undefined' ? active : index; 37 | 38 | const dom = this.menus.current; 39 | if (dom) { 40 | const children = dom.childNodes; 41 | 42 | const target = children[index] as HTMLDivElement; 43 | 44 | if (!target) { 45 | return 0; 46 | } 47 | 48 | return target.offsetLeft; 49 | } 50 | return 0; 51 | }; 52 | 53 | format = () => { 54 | const { children } = this.props; 55 | const childs = React.Children.toArray(children); 56 | const menus = []; 57 | for (let i = 0; i < childs.length; i += 1) { 58 | const { props } = childs[i] as React.ReactElement; 59 | menus.push({ 60 | title: props?.title, 61 | index: i, 62 | children: props.children, 63 | }); 64 | } 65 | 66 | return { 67 | menus, 68 | }; 69 | }; 70 | 71 | handleSelect = (active) => { 72 | this.setState({ 73 | active, 74 | offset: this.getIndicatorPos(active), 75 | }); 76 | }; 77 | 78 | render() { 79 | const { active, offset } = this.state; 80 | const { className } = this.props; 81 | 82 | const { menus } = this.format(); 83 | 84 | return ( 85 | <div className={cls(className, cx('tab'))}> 86 | <div className={cx('header')}> 87 | <div className={cx('indicator')} style={{ left: offset }} /> 88 | <div className={cx('items')} ref={this.menus}> 89 | {menus.map((menu) => { 90 | return ( 91 | <div 92 | key={menu.index} 93 | className={cx('item', { active: active === menu.index })} 94 | onClick={() => this.handleSelect(menu.index)} 95 | > 96 | {menu.title} 97 | </div> 98 | ); 99 | })} 100 | </div> 101 | </div> 102 | <div className={cx('tabs')}> 103 | <div 104 | className={cx('contents')} 105 | style={{ 106 | transform: `translate3d(-${active}00%, 0, 0)`, 107 | }} 108 | > 109 | {menus.map(({ children, index }) => { 110 | return ( 111 | <div className={cx('content', { active: active === index })} key={index}> 112 | {children} 113 | </div> 114 | ); 115 | })} 116 | </div> 117 | </div> 118 | </div> 119 | ); 120 | } 121 | } 122 | 123 | Tab.Item = function TabItem({ children }) { 124 | return children; 125 | }; 126 | -------------------------------------------------------------------------------- /packages/plugin-materials-pane/src/pane/index.module.scss: -------------------------------------------------------------------------------- 1 | #design-view { 2 | width: 100%; 3 | } 4 | .lowcode-component-panel { 5 | height: 100%; 6 | overflow: hidden; 7 | border-top: 1px solid transparent; 8 | display: flex; 9 | flex-direction: column; 10 | margin-bottom: 12px; 11 | 12 | ::-webkit-scrollbar { 13 | display: none !important; 14 | } 15 | 16 | > .header { 17 | // margin: 12px 16px; 18 | padding: 0 8px; 19 | flex-shrink: 0; 20 | flex-grow: 0; 21 | 22 | .search { 23 | width: 100%; 24 | } 25 | } 26 | 27 | > .tabs { 28 | flex: 1; 29 | overflow: hidden; 30 | } 31 | 32 | > .empty { 33 | display: flex; 34 | flex: auto; 35 | flex-flow: column nowrap; 36 | justify-content: center; 37 | align-items: center; 38 | img { 39 | width: 100px; 40 | height: 100px; 41 | } 42 | .content { 43 | line-height: 2; 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /packages/plugin-materials-pane/src/pane/store/index.ts: -------------------------------------------------------------------------------- 1 | import { SnippetMeta } from '../utils/transform'; 2 | 3 | export default class ComponentManager { 4 | snippets = new Map<string, SnippetMeta>(); 5 | 6 | setSnippets = (snippets: SnippetMeta[]) => { 7 | for (const snippet of snippets) { 8 | if (snippet.id) { 9 | this.snippets.set(snippet.id, snippet.schema); 10 | } 11 | } 12 | }; 13 | 14 | getSnippetById = (id: string) => { 15 | return this.snippets.get(id); 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /packages/plugin-materials-pane/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib" 5 | }, 6 | "include": [ 7 | "./src/" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/plugin-tools/README.md: -------------------------------------------------------------------------------- 1 | TODO 2 | --- 3 | -------------------------------------------------------------------------------- /packages/plugin-tools/build.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "build-plugin-component" 5 | ] 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /packages/plugin-tools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@alilc/lce-graph-tools", 3 | "version": "1.0.12", 4 | "description": "for Graph Editor Engine", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "files": [ 8 | "lib", 9 | "es" 10 | ], 11 | "scripts": { 12 | "test": "jest -c jest.config.js", 13 | "build": "build-scripts build --skip-demo", 14 | "build:watch": "build-scripts build --skip-demo --watch", 15 | "prepublish": "npm run build" 16 | }, 17 | "license": "MIT", 18 | "publishConfig": { 19 | "access": "public", 20 | "registry": "https://registry.npmjs.org/" 21 | }, 22 | "devDependencies": { 23 | "@alib/build-scripts": "^0.1.18", 24 | "build-plugin-component": "^1.10.0" 25 | }, 26 | "dependencies": { 27 | "@antv/g6": "^4.6.4", 28 | "@antv/x6": "^1.31.4" 29 | }, 30 | "gitHead": "7ed863fe077671622a9901f670a9311aa85c3615" 31 | } 32 | -------------------------------------------------------------------------------- /packages/plugin-tools/src/common/command.ts: -------------------------------------------------------------------------------- 1 | import { ILowCodePluginContext } from '@alilc/lowcode-engine'; 2 | import { Graph, TreeGraph } from '@antv/g6'; 3 | import { Graph as X6Graph } from '@antv/x6'; 4 | 5 | export type IGraph = typeof Graph | typeof TreeGraph | typeof X6Graph; 6 | export type ICommandCb = (ctx: ILowCodePluginContext, graph: IGraph, data: any) => void; 7 | 8 | export interface ICommands { 9 | [key: string]: ICommandCb 10 | } 11 | 12 | export class CommandManager { 13 | constructor(data: ICommands) { 14 | this.commands = data; 15 | } 16 | 17 | ctx: ILowCodePluginContext; 18 | 19 | graph: IGraph; 20 | 21 | commands: ICommands = {}; 22 | 23 | init(ctx: ILowCodePluginContext, graph: IGraph) { 24 | this.ctx = ctx; 25 | this.graph = graph; 26 | // TODO: 重复监听 27 | Object.keys(this.commands).forEach((key: string) => { 28 | ctx.event.on(`common:${key}`, (data: any) => { 29 | this.commands[key](ctx, graph, data); 30 | }); 31 | }); 32 | } 33 | 34 | register(key: string, listener: ICommandCb) { 35 | this.commands[key] = listener; 36 | if (this.ctx && this.graph) { 37 | this.ctx?.event.on(`common:${key}`, (data: any) => { 38 | listener(this.ctx, this.graph, data); 39 | }); 40 | } 41 | } 42 | 43 | get(key: string) { 44 | return this.commands[key]; 45 | } 46 | } -------------------------------------------------------------------------------- /packages/plugin-tools/src/common/index.ts: -------------------------------------------------------------------------------- 1 | // 2 | export * from './command'; 3 | export * from './constants'; 4 | export * from './utils'; -------------------------------------------------------------------------------- /packages/plugin-tools/src/index.scss: -------------------------------------------------------------------------------- 1 | .lc-toolbar { 2 | .lc-toolbar-left { 3 | display: flex; 4 | .tool-item { 5 | cursor: pointer; 6 | width: auto; 7 | margin-right: 10px; 8 | line-height: 20px; 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /packages/plugin-tools/src/index.ts: -------------------------------------------------------------------------------- 1 | import './index.scss'; 2 | export * from './tools/zoomIn'; 3 | export * from './tools/zoomOut'; 4 | export * from './tools/insertItem'; 5 | export * from './tools/removeItem'; 6 | export * from './tools/save'; 7 | export * from './tools/logo'; 8 | export * from './plugins'; 9 | export * from './common'; -------------------------------------------------------------------------------- /packages/plugin-tools/src/plugins/InsertItemPlugin.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import { ILowCodePluginContext, hotkey } from '@alilc/lowcode-engine' 3 | import { EditorCommand } from '../common' 4 | 5 | interface IInsertProps { 6 | ctx: ILowCodePluginContext // 上下文 7 | model: any // 节点配置 8 | nodeType: EditorCommand.insertChildNode | EditorCommand.insertSiblingNode 9 | } 10 | 11 | // 插入渲染 12 | class InsertView extends PureComponent<IInsertProps> { 13 | handleClick = () => { 14 | const { ctx, model, nodeType } = this.props 15 | const selectedNodes = ctx.project.currentDocument?.selection.getNodes() 16 | if (selectedNodes && selectedNodes.length > 0) { 17 | // 有选中节点触发 18 | ctx.event.emit(EditorCommand[nodeType], { selectedNodes, model }) 19 | } 20 | } 21 | 22 | render() { 23 | const { nodeType } = this.props 24 | return ( 25 | <div className="tool-item"> 26 | <span style={{ cursor: 'pointer' }} onClick={this.handleClick}> 27 | {nodeType === EditorCommand.insertChildNode ? '新增子节点' : '新增兄弟节点'} 28 | </span> 29 | </div> 30 | ) 31 | } 32 | } 33 | 34 | // 插入节点插件 35 | const InsertItemPlugin = (ctx: ILowCodePluginContext, options: { model: any }) => { 36 | const nodeType = [EditorCommand.insertChildNode, EditorCommand.insertSiblingNode] 37 | return { 38 | name: 'insertItem', 39 | async init() { 40 | const { skeleton } = ctx 41 | nodeType.forEach((item) => { 42 | console.log('alex', item) 43 | skeleton.add({ 44 | name: item, 45 | area: 'toolbar', 46 | type: 'Widget', 47 | props: { 48 | align: 'left', 49 | }, 50 | content: InsertView, 51 | contentProps: { 52 | ctx, 53 | model: options.model, 54 | nodeType: item, 55 | }, 56 | }) 57 | hotkey.bind(item === EditorCommand.insertChildNode ? 'tab' : 'enter', (e: any) => { 58 | e.preventDefault() 59 | const selectedNodes = ctx.project.currentDocument?.selection.getNodes() 60 | const { model } = options 61 | if (selectedNodes && selectedNodes.length > 0) { 62 | // 有选中节点触发 63 | ctx.event.emit(item, { selectedNodes, model }) 64 | } 65 | }) 66 | }) 67 | }, 68 | } 69 | } 70 | 71 | InsertItemPlugin.pluginName = 'insertItemPlugin' 72 | InsertItemPlugin.meta = { 73 | preferenceDeclaration: { 74 | title: '新增节点配置', 75 | properties: [ 76 | { 77 | key: 'model', 78 | type: 'any', 79 | description: '用户自定义新增节点配置', 80 | }, 81 | ], 82 | }, 83 | } 84 | 85 | export default InsertItemPlugin 86 | -------------------------------------------------------------------------------- /packages/plugin-tools/src/plugins/OperateButtonPlugin.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import { ILowCodePluginContext, hotkey } from '@alilc/lowcode-engine' 3 | import { Button } from '@alifd/next' 4 | 5 | interface IOperateButtonProps { 6 | ctx: ILowCodePluginContext // 上下文 7 | callback: (schema: any) => void // 回调函数 8 | text: string // 按钮显示文本 9 | btnProps?: any // 按钮属性 10 | } 11 | 12 | interface IOpratePluginOption { 13 | callback: (schema: any) => void 14 | name: string 15 | text: string 16 | btnProps?: any 17 | hotkey?: string 18 | } 19 | 20 | // 操作按钮 21 | class OperateButton extends PureComponent<IOperateButtonProps> { 22 | handleClick = () => { 23 | const { ctx, callback } = this.props 24 | const currentSchema = ctx.project.currentDocument?.exportSchema() 25 | callback?.(currentSchema) 26 | } 27 | 28 | render() { 29 | const { text, btnProps } = this.props 30 | return ( 31 | <Button type="primary" {...btnProps} onClick={this.handleClick}> 32 | {text} 33 | </Button> 34 | ) 35 | } 36 | } 37 | 38 | // 操作按钮插件 39 | const OperateButtonPlugin = (ctx: ILowCodePluginContext, options: IOpratePluginOption | IOpratePluginOption[]) => { 40 | const buttonsArray = Array.isArray(options) ? options : [options] 41 | return { 42 | name: 'operateButton', 43 | async init() { 44 | const { skeleton } = ctx 45 | buttonsArray.forEach((item: IOpratePluginOption) => { 46 | // 添加按钮 47 | skeleton.add({ 48 | name: item.name, 49 | area: 'topArea', 50 | type: 'Widget', 51 | props: { 52 | align: 'right', 53 | }, 54 | content: OperateButton, 55 | contentProps: { 56 | ctx, 57 | callback: item.callback, 58 | text: item.text, 59 | btnProps: item.btnProps, 60 | }, 61 | }) 62 | // 添加快捷键 63 | item.hotkey && 64 | hotkey.bind(item.hotkey, (e: any) => { 65 | e.preventDefault() 66 | const currentSchema = ctx.project.currentDocument?.exportSchema() 67 | item.callback(currentSchema) 68 | }) 69 | }) 70 | }, 71 | } 72 | } 73 | 74 | OperateButtonPlugin.pluginName = 'operateButtonPlugin' 75 | OperateButtonPlugin.meta = { 76 | preferenceDeclaration: { 77 | title: '自定义操作按钮', 78 | properties: [ 79 | { 80 | key: 'callback', 81 | type: 'Function', 82 | description: '用户自定义按钮处理', 83 | }, 84 | { 85 | key: 'name', 86 | type: 'Function', 87 | description: '用户自定义按钮名', 88 | }, 89 | { 90 | key: 'text', 91 | type: 'Function', 92 | description: '用户自定义按钮文本', 93 | }, 94 | { 95 | key: 'btnProps', 96 | type: 'Object', 97 | description: '自定义按钮属性设置', 98 | }, 99 | { 100 | key: 'hotKey', 101 | type: 'Function', 102 | description: '用户自定义按钮快捷键', 103 | }, 104 | ], 105 | }, 106 | } 107 | 108 | export default OperateButtonPlugin 109 | -------------------------------------------------------------------------------- /packages/plugin-tools/src/plugins/RemoveItemPlugin.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { ILowCodePluginContext, hotkey } from '@alilc/lowcode-engine'; 3 | import { isFormEvent } from '@alilc/lowcode-utils'; 4 | import { EditorCommand } from '../common'; 5 | 6 | interface IRemoveProps { 7 | ctx: ILowCodePluginContext // 上下文 8 | mode: string 9 | } 10 | 11 | // 删除渲染 12 | class RemoveView extends PureComponent<IRemoveProps> { 13 | handleClick = () => { 14 | const { ctx } = this.props 15 | const selectedNodes = ctx.project.currentDocument?.selection.getNodes(); 16 | if (selectedNodes && selectedNodes.length > 0) { 17 | // 有选中节点触发 18 | ctx.event.emit(EditorCommand.removeItem, selectedNodes) 19 | } 20 | } 21 | 22 | render() { 23 | return ( 24 | <div className="tool-item"> 25 | <span style={{ cursor: 'pointer' }} onClick={this.handleClick}> 26 | 删除 27 | </span> 28 | </div> 29 | ) 30 | } 31 | } 32 | 33 | // 删除节点插件 34 | const RemoveItemPlugin = (ctx: ILowCodePluginContext) => { 35 | const onRemove = (e: any) => { 36 | if (isFormEvent(e)) return; 37 | const selectedNodes = ctx.project.currentDocument?.selection.getNodes(); 38 | if (selectedNodes && selectedNodes.length > 0) { 39 | // 有选中节点触发 40 | ctx.event.emit(EditorCommand.removeItem, selectedNodes); 41 | } 42 | } 43 | return { 44 | name: 'removeItem', 45 | async init() { 46 | const { skeleton } = ctx 47 | skeleton.add({ 48 | name: 'removeItem', 49 | area: 'toolbar', 50 | type: 'Widget', 51 | props: { 52 | align: 'left', 53 | }, 54 | content: RemoveView, 55 | contentProps: { 56 | ctx, 57 | }, 58 | }); 59 | hotkey.bind(['backspace', 'del'], (e) => { 60 | onRemove(e); 61 | }); 62 | }, 63 | } 64 | } 65 | 66 | RemoveItemPlugin.pluginName = 'removeItemPlugin' 67 | 68 | export default RemoveItemPlugin 69 | -------------------------------------------------------------------------------- /packages/plugin-tools/src/plugins/UndoRedoPlugin.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { hotkey } from '@alilc/lowcode-engine'; 3 | import { IPublicModelPluginContext } from '@alilc/lowcode-types'; 4 | import { EditorCommand } from '../common'; 5 | 6 | interface IUndoRedoProps { 7 | ctx: IPublicModelPluginContext; // 上下文 8 | mode: string; 9 | } 10 | 11 | // 缩放渲染 12 | class UndoRedoView extends PureComponent<IUndoRedoProps> { 13 | handleClick = () => { 14 | const { ctx, mode } = this.props; 15 | ctx.event.emit(mode === 'undo' ? EditorCommand.Undo : EditorCommand.Redo); 16 | } 17 | 18 | render() { 19 | const { mode } = this.props; 20 | return ( 21 | <div className="tool-item"> 22 | <span style={{ cursor: 'pointer' }} onClick={this.handleClick}> 23 | {mode === 'undo' ? '撤销' : '恢复'} 24 | </span> 25 | </div> 26 | ); 27 | } 28 | } 29 | 30 | // 缩放插件 31 | const UndoRedoPlugin = (ctx: IPublicModelPluginContext) => { 32 | return { 33 | name: 'zoom', 34 | async init() { 35 | const { skeleton } = ctx; 36 | const modes = ['undo', 'redo']; 37 | modes.forEach((item) => { 38 | skeleton.add({ 39 | name: item, 40 | area: 'toolbar', 41 | type: 'Widget', 42 | props: { 43 | align: 'left', 44 | }, 45 | content: UndoRedoView, 46 | contentProps: { 47 | ctx, 48 | mode: item, 49 | }, 50 | }) 51 | // 绑定快捷键 52 | const key = item === 'undo' ? 'command+z' : 'command+y'; 53 | hotkey.bind(key, (e: any) => { 54 | e.preventDefault() 55 | ctx.event.emit(item === 'undo' ? EditorCommand.Undo : EditorCommand.Redo) 56 | }) 57 | }) 58 | }, 59 | } 60 | } 61 | 62 | UndoRedoPlugin.pluginName = 'undoRedoPlugin'; 63 | 64 | export default UndoRedoPlugin; 65 | -------------------------------------------------------------------------------- /packages/plugin-tools/src/plugins/ZoomPlugin.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { ILowCodePluginContext, hotkey } from '@alilc/lowcode-engine'; 3 | import { EditorCommand } from '../common'; 4 | 5 | interface IZoomProps { 6 | ctx: ILowCodePluginContext; // 上下文 7 | mode: string; 8 | } 9 | 10 | // 缩放渲染 11 | class ZoomView extends PureComponent<IZoomProps> { 12 | handleClick = () => { 13 | const { ctx, mode } = this.props; 14 | ctx.event.emit(mode === 'zoom-in' ? EditorCommand.ZoomIn : EditorCommand.ZoomOut); 15 | } 16 | 17 | render() { 18 | const { mode } = this.props; 19 | return ( 20 | <div className="tool-item"> 21 | <span style={{ cursor: 'pointer' }} onClick={this.handleClick}> 22 | {mode === 'zoom-in' ? '放大' : '缩小'} 23 | </span> 24 | {/* <Icon type="zoom-in" /> */} 25 | </div> 26 | ); 27 | } 28 | } 29 | 30 | // 缩放插件 31 | const ZoomPlugin = (ctx: ILowCodePluginContext) => { 32 | return { 33 | name: 'zoom', 34 | async init() { 35 | const { skeleton } = ctx 36 | const modes = ['zoom-in', 'zoom-out'] 37 | modes.forEach((item) => { 38 | skeleton.add({ 39 | name: item, 40 | area: 'toolbar', 41 | type: 'Widget', 42 | props: { 43 | align: 'left', 44 | }, 45 | content: ZoomView, 46 | contentProps: { 47 | ctx, 48 | mode: item, 49 | }, 50 | }) 51 | // 绑定快捷键 52 | const key = item === 'zoom-in' ? 'command+1' : 'command+2'; 53 | hotkey.bind(key, (e: any) => { 54 | e.preventDefault() 55 | ctx.event.emit(item === 'zoom-in' ? EditorCommand.ZoomIn : EditorCommand.ZoomOut) 56 | }) 57 | }) 58 | }, 59 | } 60 | } 61 | 62 | ZoomPlugin.pluginName = 'zoomPlugin'; 63 | 64 | export default ZoomPlugin; 65 | -------------------------------------------------------------------------------- /packages/plugin-tools/src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import ZoomPlugin from './ZoomPlugin'; 2 | import OperateButtonPlugin from './OperateButtonPlugin'; 3 | import RemoveItemPlugin from './RemoveItemPlugin'; 4 | import InsertItemPlugin from './InsertItemPlugin'; 5 | import UndoRedoPlugin from './UndoRedoPlugin'; 6 | 7 | export { ZoomPlugin, OperateButtonPlugin, RemoveItemPlugin, InsertItemPlugin, UndoRedoPlugin } 8 | -------------------------------------------------------------------------------- /packages/plugin-tools/src/tools/logo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ILowCodePluginContext } from '@alilc/lowcode-engine' 3 | 4 | class Logo extends React.Component<{ ctx: ILowCodePluginContext; imgUrl?: string; style?: any }, any> { 5 | render() { 6 | const { imgUrl, style } = this.props 7 | return ( 8 | <img 9 | alt="Logo" 10 | src={imgUrl || 'https://img.alicdn.com/imgextra/i2/O1CN01uv6vu822RBCSYLro2_!!6000000007116-55-tps-139-26.svg'} 11 | style={style} 12 | /> 13 | ) 14 | } 15 | } 16 | 17 | export const logo = (ctx: ILowCodePluginContext, options: { imgUrl: string; style?: any }) => { 18 | return { 19 | name: 'logo', 20 | async init() { 21 | const { skeleton } = ctx 22 | 23 | skeleton.add({ 24 | name: 'logo', 25 | area: 'topArea', 26 | type: 'Widget', 27 | props: { 28 | align: 'left', 29 | }, 30 | content: Logo, 31 | contentProps: { 32 | ctx, 33 | ...options, 34 | }, 35 | }) 36 | }, 37 | } 38 | } 39 | logo.pluginName = 'logo' 40 | logo.meta = { 41 | preferenceDeclaration: { 42 | title: 'logo配置', 43 | properties: [ 44 | { 45 | key: 'imgUrl', 46 | type: 'string', 47 | description: '用户自定义logo资源', 48 | }, 49 | { 50 | key: 'style', 51 | type: 'any', 52 | description: '用户自定义logo样式', 53 | }, 54 | ], 55 | }, 56 | } 57 | -------------------------------------------------------------------------------- /packages/plugin-tools/src/tools/removeItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ILowCodePluginContext, hotkey } from '@alilc/lowcode-engine'; 3 | import { isFormEvent } from '@alilc/lowcode-utils'; 4 | import { EditorCommand } from '../common'; 5 | 6 | class Tool extends React.Component<{ ctx: ILowCodePluginContext }, any> { 7 | handleClick() { 8 | const { ctx } = this.props; 9 | const selectedNodes = ctx.project.currentDocument?.selection.getNodes(); 10 | if (selectedNodes && selectedNodes.length > 0) { 11 | // 有选中节点触发 12 | ctx.event.emit(EditorCommand.removeItem, selectedNodes); 13 | } 14 | } 15 | render() { 16 | return ( 17 | <div onClick={this.handleClick.bind(this)} className="tool-item" style={{ width: 80 }}> 18 | 删除节点 19 | </div> 20 | ) 21 | } 22 | } 23 | 24 | export const removeItem = (ctx: ILowCodePluginContext) => { 25 | return { 26 | name: 'removeItem', 27 | async init() { 28 | const { skeleton } = ctx; 29 | 30 | skeleton.add({ 31 | name: 'removeItem', 32 | area: 'toolbar', 33 | type: 'Widget', 34 | props: { 35 | align: 'left' 36 | }, 37 | content: Tool, 38 | contentProps: { 39 | ctx 40 | } 41 | }); 42 | }, 43 | }; 44 | }; 45 | removeItem.pluginName = 'removeItem'; 46 | 47 | export const removeItemHotKey = (ctx: ILowCodePluginContext, options: any) => { 48 | const onRemove = (e: any) => { 49 | if (isFormEvent(e)) return; 50 | const selectedNodes = ctx.project.currentDocument?.selection.getNodes(); 51 | if (selectedNodes && selectedNodes.length > 0) { 52 | // 有选中节点触发 53 | ctx.event.emit(EditorCommand.removeItem, selectedNodes); 54 | } 55 | } 56 | return { 57 | name: 'removeItemHotKey', 58 | async init() { 59 | hotkey.bind(['backspace', 'del'], (e) => { 60 | onRemove(e); 61 | }); 62 | } 63 | }; 64 | }; 65 | removeItemHotKey.pluginName = 'removeItemHotKey'; -------------------------------------------------------------------------------- /packages/plugin-tools/src/tools/save.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ILowCodePluginContext, hotkey } from '@alilc/lowcode-engine'; 3 | import { Button } from '@alifd/next'; 4 | 5 | class Save extends React.Component<{ ctx: ILowCodePluginContext, cb: any, type: 'save' | 'publish' }, any> { 6 | handleSave() { 7 | const { ctx, cb } = this.props; 8 | const currentSchema = ctx.project.currentDocument?.exportSchema(); 9 | cb(currentSchema); 10 | } 11 | render() { 12 | const { type } = this.props; 13 | return ( 14 | <Button type={type === 'save' ? 'primary' : 'normal'} onClick={this.handleSave.bind(this)}>{type === 'save' ? '保存' : '发布'}</Button> 15 | ) 16 | } 17 | } 18 | 19 | export const save = (ctx: ILowCodePluginContext, options: any) => { 20 | return { 21 | name: 'save', 22 | async init() { 23 | const { skeleton } = ctx; 24 | 25 | skeleton.add({ 26 | name: 'save', 27 | area: 'topArea', 28 | type: 'Widget', 29 | props: { 30 | align: 'right' 31 | }, 32 | content: Save, 33 | contentProps: { 34 | ctx, 35 | cb: options.cb, 36 | type: 'save' 37 | } 38 | }); 39 | } 40 | }; 41 | }; 42 | save.pluginName = 'save'; 43 | save.meta = { 44 | preferenceDeclaration: { 45 | title: '保存回调', 46 | properties: [{ 47 | key: 'cb', 48 | type: 'Function', 49 | description: '用户自定义保存处理', 50 | }] 51 | } 52 | }; 53 | 54 | export const saveHotKey = (ctx: ILowCodePluginContext, options: any) => { 55 | return { 56 | name: 'saveHotKey', 57 | async init() { 58 | hotkey.bind('command+s', (e: any) => { 59 | e.preventDefault(); 60 | const currentSchema = ctx.project.currentDocument?.exportSchema(); 61 | options.cb(currentSchema); 62 | }); 63 | } 64 | }; 65 | }; 66 | saveHotKey.pluginName = 'saveHotKey'; 67 | saveHotKey.meta = { 68 | preferenceDeclaration: { 69 | title: '保存快捷键回调', 70 | properties: [{ 71 | key: 'cb', 72 | type: 'Function', 73 | description: '用户自定义保存快捷键回调', 74 | }] 75 | } 76 | }; 77 | 78 | export const publish = (ctx: ILowCodePluginContext, options: any) => { 79 | return { 80 | name: 'publish', 81 | async init() { 82 | const { skeleton } = ctx; 83 | 84 | skeleton.add({ 85 | name: 'publish', 86 | area: 'topArea', 87 | type: 'Widget', 88 | props: { 89 | align: 'right' 90 | }, 91 | content: Save, 92 | contentProps: { 93 | ctx, 94 | cb: options.cb, 95 | type: 'publish' 96 | } 97 | }); 98 | } 99 | }; 100 | }; 101 | publish.pluginName = 'publish'; 102 | publish.meta = { 103 | preferenceDeclaration: { 104 | title: '发布回调', 105 | properties: [{ 106 | key: 'cb', 107 | type: 'Function', 108 | description: '用户自定义发布处理', 109 | }] 110 | } 111 | }; -------------------------------------------------------------------------------- /packages/plugin-tools/src/tools/zoomIn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ILowCodePluginContext, hotkey } from '@alilc/lowcode-engine'; 3 | import { EditorCommand } from '../common'; 4 | class Tool extends React.Component<{ ctx: ILowCodePluginContext }, any> { 5 | handleClick() { 6 | const { ctx } = this.props; 7 | ctx.event.emit(EditorCommand.ZoomIn); 8 | } 9 | render() { 10 | return ( 11 | <div onClick={this.handleClick.bind(this)} className="tool-item"> 12 | 放大 13 | </div> 14 | ) 15 | } 16 | } 17 | 18 | export const zoomIn = (ctx: ILowCodePluginContext) => { 19 | return { 20 | name: 'zoomIn', 21 | async init() { 22 | const { skeleton } = ctx; 23 | 24 | skeleton.add({ 25 | name: 'zoomIn', 26 | area: 'toolbar', 27 | type: 'Widget', 28 | props: { 29 | align: 'left' 30 | }, 31 | content: Tool, 32 | contentProps: { 33 | ctx 34 | } 35 | }); 36 | }, 37 | }; 38 | }; 39 | zoomIn.pluginName = 'zoomIn'; 40 | 41 | export const zoomInHotKey = (ctx: ILowCodePluginContext, options: any) => { 42 | return { 43 | name: 'zoomInHotKey', 44 | async init() { 45 | hotkey.bind('command++', (e: any) => { 46 | e.preventDefault(); 47 | ctx.event.emit(EditorCommand.ZoomIn); 48 | }); 49 | } 50 | }; 51 | }; 52 | zoomInHotKey.pluginName = 'zoomInHotKey'; -------------------------------------------------------------------------------- /packages/plugin-tools/src/tools/zoomOut.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ILowCodePluginContext } from '@alilc/lowcode-engine'; 3 | import { EditorCommand } from '../common'; 4 | 5 | 6 | class Tool extends React.Component<{ ctx: ILowCodePluginContext }, any> { 7 | handleClick() { 8 | const { ctx } = this.props; 9 | ctx.event.emit(EditorCommand.ZoomOut); 10 | } 11 | render() { 12 | return ( 13 | <div onClick={this.handleClick.bind(this)} className="tool-item"> 14 | 缩小 15 | </div> 16 | ) 17 | } 18 | } 19 | 20 | export const zoomOut = (ctx: ILowCodePluginContext) => { 21 | return { 22 | name: 'zoomOut', 23 | async init() { 24 | const { skeleton } = ctx; 25 | 26 | skeleton.add({ 27 | name: 'zoomOut', 28 | area: 'toolbar', 29 | type: 'Widget', 30 | props: { 31 | align: 'left' 32 | }, 33 | content: Tool, 34 | contentProps: { 35 | ctx 36 | } 37 | }); 38 | }, 39 | }; 40 | }; 41 | zoomOut.pluginName = 'zoomOut'; -------------------------------------------------------------------------------- /packages/plugin-tools/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib" 5 | }, 6 | "include": [ 7 | "./src/" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/plugin-x6-designer/README.md: -------------------------------------------------------------------------------- 1 | ## x6 画布插件 2 | 3 | 设计要求: 4 | - 节点/线 组件统一使用 react 组件声明式封装 5 | - 节点/线 核心事件分发到组件,提供统一 x6Designer api -------------------------------------------------------------------------------- /packages/plugin-x6-designer/build.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "build-plugin-component" 5 | ] 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /packages/plugin-x6-designer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@alilc/lce-graph-x6-designer", 3 | "version": "1.0.12", 4 | "description": "for Graph Editor Engine", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "files": [ 8 | "lib", 9 | "es" 10 | ], 11 | "scripts": { 12 | "test": "jest -c jest.config.js", 13 | "build": "build-scripts build --skip-demo", 14 | "build:watch": "build-scripts build --skip-demo --watch", 15 | "prepublish": "npm run build" 16 | }, 17 | "license": "MIT", 18 | "publishConfig": { 19 | "access": "public", 20 | "registry": "https://registry.npmjs.org/" 21 | }, 22 | "dependencies": { 23 | "@alilc/lce-graph-tools": "1.0.12", 24 | "@alilc/lowcode-utils": "^1.0.0", 25 | "@antv/x6-react-shape": "^1.6.1" 26 | }, 27 | "devDependencies": { 28 | "@alib/build-scripts": "^0.1.18", 29 | "@antv/x6": "^1.31.4", 30 | "build-plugin-component": "^1.10.0" 31 | }, 32 | "gitHead": "7ed863fe077671622a9901f670a9311aa85c3615" 33 | } 34 | -------------------------------------------------------------------------------- /packages/plugin-x6-designer/src/DesignerView.tsx: -------------------------------------------------------------------------------- 1 | import { createElement, Fragment, PureComponent } from "react"; 2 | import { ILowCodePluginContext } from '@alilc/lowcode-engine'; 3 | import { Editor } from '@alilc/lowcode-editor-core'; 4 | import { initGraph } from "./graph/initGraph"; 5 | import Nodes from "./items"; 6 | import { render } from "react-dom"; 7 | import { registerShape } from "./graph/registerShape"; 8 | import { initEvents } from "./graph/initEvents"; 9 | import x6Designer from './designer'; 10 | 11 | interface IProps { 12 | editor: Editor; 13 | ctx: ILowCodePluginContext; 14 | } 15 | 16 | export default class DesignerView extends PureComponent<IProps> { 17 | private container: HTMLDivElement; 18 | private nodesContainer: HTMLDivElement; 19 | 20 | refContainer = (container: HTMLDivElement) => { 21 | this.container = container; 22 | } 23 | 24 | refNodesContainer = (container: HTMLDivElement) => { 25 | this.nodesContainer = container; 26 | } 27 | 28 | componentDidMount() { 29 | registerShape(); 30 | 31 | // @ts-ignore 32 | const graph = initGraph(this.container, []); 33 | if (graph) { 34 | x6Designer.init(this.props.ctx, graph); 35 | initEvents(graph); 36 | 37 | // add nodes & edges 38 | render( 39 | createElement(Nodes, { 40 | graph, 41 | ctx: this.props.ctx, 42 | }), 43 | this.nodesContainer 44 | ); 45 | 46 | } 47 | } 48 | 49 | render() { 50 | return ( 51 | <div id="design-view" className="design-view" ref={this.refContainer} style={{ width: '100%' }}> 52 | <div id="design-view-nodes" ref={this.refNodesContainer}></div> 53 | </div> 54 | ) 55 | } 56 | } -------------------------------------------------------------------------------- /packages/plugin-x6-designer/src/builtin-commands/index.ts: -------------------------------------------------------------------------------- 1 | export * from './remove-item'; 2 | export * from './zoom-in-out'; 3 | export * from './undo-redo'; -------------------------------------------------------------------------------- /packages/plugin-x6-designer/src/builtin-commands/remove-item.ts: -------------------------------------------------------------------------------- 1 | import { IPublicModelPluginContext } from '@alilc/lowcode-types'; 2 | export const removeItem = (ctx: IPublicModelPluginContext, graph: any, selectedNodes: any[]) => { 3 | if (selectedNodes?.length) { 4 | selectedNodes.forEach(node => { 5 | // 可被删除判断 6 | if (node?.componentMeta?.getMetadata().tags?.includes('node')) { 7 | const allNodes = Array.from(ctx.project.currentDocument?.nodesMap.values() || []); 8 | // 相关线 9 | const lines = allNodes.filter(n => (n?.componentMeta?.getMetadata().tags?.includes('edge') && (n.getPropValue('source') === node.id || n.getPropValue('target') === node.id))); 10 | lines.forEach(line => { 11 | ctx.project.currentDocument?.removeNode(line.id); 12 | }); 13 | } 14 | ctx.project.currentDocument?.removeNode(node.id); 15 | }); 16 | } 17 | } -------------------------------------------------------------------------------- /packages/plugin-x6-designer/src/builtin-commands/undo-redo.ts: -------------------------------------------------------------------------------- 1 | import { IPublicModelPluginContext } from '@alilc/lowcode-types'; 2 | export const undo = (ctx: IPublicModelPluginContext, graph: any) => { 3 | ctx.project.currentDocument?.history.back(); 4 | } 5 | export const redo = (ctx: IPublicModelPluginContext, graph: any) => { 6 | ctx.project.currentDocument?.history.forward(); 7 | } -------------------------------------------------------------------------------- /packages/plugin-x6-designer/src/builtin-commands/zoom-in-out.ts: -------------------------------------------------------------------------------- 1 | import { IPublicModelPluginContext } from '@alilc/lowcode-types'; 2 | export const zoomIn = (ctx: IPublicModelPluginContext, graph: any) => { 3 | graph.zoom(0.1); 4 | } 5 | export const zoomOut = (ctx: IPublicModelPluginContext, graph: any) => { 6 | graph.zoom(-0.1); 7 | } -------------------------------------------------------------------------------- /packages/plugin-x6-designer/src/designer.ts: -------------------------------------------------------------------------------- 1 | import { Node as NodeModel } from '@alilc/lowcode-shell'; 2 | import { Node, Edge, Graph } from '@antv/x6'; 3 | import { EditorCommand, CommandManager, ICommandCb } from '@alilc/lce-graph-tools'; 4 | import { zoomIn, zoomOut, removeItem, undo, redo } from './builtin-commands'; 5 | 6 | export interface IDesigner { 7 | onNodeRender: (cb: (model: NodeModel, node: Node) => void) => any[]; 8 | onEdgeRender: (cb: (model: NodeModel, edge: Edge) => void) => any[]; 9 | onEdgeLabelRender: (cb: (args: Graph.Hook.OnEdgeLabelRenderedArgs) => void) => any[]; 10 | getGraph: () => Graph; 11 | } 12 | 13 | /** 14 | * designer 统一导出 api 15 | */ 16 | class Designer implements IDesigner { 17 | constructor() { 18 | this.commandManager = new CommandManager({ 19 | [EditorCommand.ZoomIn]: zoomIn, 20 | [EditorCommand.ZoomOut]: zoomOut, 21 | [EditorCommand.removeItem]: removeItem, 22 | [EditorCommand.Undo]: undo, 23 | [EditorCommand.Redo]: redo 24 | }); 25 | } 26 | commandManager: CommandManager; 27 | public onNodeRenderCb: Array<(model: NodeModel, node: Node) => void> = []; 28 | public onEdgeRenderCb: Array<(model: NodeModel, edge: Edge) => void> = []; 29 | public onEdgeLabelRenderCb: Array<(args: Graph.Hook.OnEdgeLabelRenderedArgs) => void> = []; 30 | public graph: Graph; 31 | 32 | // node model data => graph node render 33 | public onNodeRender = (cb?: (model: NodeModel, node: Node) => void) => { 34 | if (cb) { 35 | this.onNodeRenderCb.push(cb); 36 | } 37 | return this.onNodeRenderCb; 38 | } 39 | 40 | // edge model data => graph edge render 41 | onEdgeRender = (cb?: (model: NodeModel, edge: Edge) => void) => { 42 | if (cb) { 43 | this.onEdgeRenderCb.push(cb); 44 | } 45 | return this.onEdgeRenderCb; 46 | } 47 | 48 | onEdgeLabelRender = (cb?: (args: Graph.Hook.OnEdgeLabelRenderedArgs) => void) => { 49 | if (cb) { 50 | this.onEdgeLabelRenderCb.push(cb); 51 | } 52 | return this.onEdgeLabelRenderCb; 53 | } 54 | 55 | init(ctx: any, graph: Graph) { 56 | this.graph = graph; 57 | this.commandManager.init(ctx, graph); 58 | } 59 | 60 | getGraph() { 61 | return this.graph; 62 | } 63 | 64 | public registerCommand = (key: string, listener: ICommandCb) => { 65 | this.commandManager.register(key, listener); 66 | } 67 | } 68 | 69 | const x6Designer = new Designer(); 70 | export default x6Designer; -------------------------------------------------------------------------------- /packages/plugin-x6-designer/src/graph/initGraph.tsx: -------------------------------------------------------------------------------- 1 | import { Graph, Shape } from '@antv/x6'; 2 | import { project } from '@alilc/lowcode-engine'; 3 | import x6Designer from '../designer'; 4 | 5 | export function initGraph(container: HTMLElement) { 6 | //@ts-ignore 7 | const graph = window._X6Graph = new Graph({ 8 | container, 9 | // async: true, // 异步加载画布 10 | grid: { 11 | // 网格 12 | size: 10, 13 | visible: true, 14 | type: 'dot', // 'dot' | 'fixedDot' | 'mesh' 15 | args: { 16 | color: '#919BAE', // 网格线/点颜色 17 | thickness: 1, // 网格线宽度/网格点大小 18 | }, 19 | }, 20 | panning: { 21 | enabled: true, 22 | eventTypes: ['mouseWheel'] 23 | }, 24 | clipboard: false, 25 | snapline: true, // 对齐线 26 | // https://github.com/antvis/X6/pull/2342 多选移动会和 sanpline 计算冲突,x6 bug 暂时不支持多选移动 27 | selecting: { 28 | enabled: true, 29 | rubberband: true, 30 | modifiers: ['shift'], 31 | }, 32 | connecting: { 33 | snap: { 34 | radius: 40, // 吸附阈值 35 | }, 36 | allowBlank: false, // 不允许连接到画布空白位置的点 37 | allowLoop: false, // 不允许创建循环连线 38 | allowMulti: false, // 不允许在相同的起始节点和终止之间创建多条边 39 | allowNode: false, 40 | allowEdge: true, 41 | allowPort: true, 42 | highlight: true, 43 | createEdge() { 44 | // 创建新边 45 | return new Shape.Edge({ 46 | attrs: { 47 | line: { 48 | strokeDasharray: '5 5', 49 | stroke: '#4C6079', 50 | strokeOpacity: 0.5, 51 | strokeWidth: 1, 52 | targetMarker: { 53 | // 箭头 54 | name: 'block', 55 | size: 8, 56 | }, 57 | }, 58 | }, 59 | zIndex: 0, 60 | }); 61 | }, 62 | validateEdge({ edge }) { 63 | const doc = project.currentDocument!; 64 | const contentEdge = doc.getNodeById(edge.id); 65 | console.log(edge.getSourceCellId(), edge.getTargetCellId()); 66 | if (!contentEdge) { 67 | const node = doc.createNode({ 68 | componentName: 'Line', 69 | title: '线', 70 | props: { 71 | name: '线', 72 | source: edge.getSourceCellId(), 73 | target: edge.getTargetCellId(), 74 | sourcePortId: edge.getSourcePortId(), 75 | targetPortId: edge.getTargetPortId() 76 | }, 77 | }); 78 | const rootNode = project.currentDocument?.root; 79 | project.currentDocument?.insertNode(rootNode!, node); 80 | } else { 81 | contentEdge.setPropValue('source', edge.getSourceCellId()); 82 | contentEdge.setPropValue('target', edge.getTargetCellId()); 83 | contentEdge.setPropValue('sourcePortId', edge.getSourcePortId()); 84 | contentEdge.setPropValue('targetPortId', edge.getTargetPortId()); 85 | } 86 | 87 | return false; 88 | }, 89 | }, 90 | onEdgeLabelRendered(args) { 91 | const onEdgeLabelRenderCb = x6Designer.onEdgeLabelRender(); 92 | for (const cb of onEdgeLabelRenderCb) { 93 | cb(args); 94 | } 95 | } 96 | }); 97 | 98 | // 适应画布 99 | const getContainerSize = () => { 100 | const leftPanel = document.querySelector('.lc-left-area')?.clientWidth || 0; 101 | const rightPanel = 102 | document.querySelector('.lc-right-area')?.clientWidth || 0; 103 | return { 104 | width: document.body.offsetWidth - leftPanel - rightPanel, 105 | height: document.querySelector('.lc-main-area')?.clientHeight || 0, 106 | }; 107 | }; 108 | const resizeFn = () => { 109 | const { width, height } = getContainerSize(); 110 | graph.resize(width, height); 111 | }; 112 | window.addEventListener('resize', resizeFn); 113 | 114 | // 画布内容居中 115 | requestAnimationFrame(() => { 116 | resizeFn(); 117 | graph.centerContent(); 118 | }); 119 | return graph; 120 | } 121 | -------------------------------------------------------------------------------- /packages/plugin-x6-designer/src/graph/register/shape/const.ts: -------------------------------------------------------------------------------- 1 | export const DeepBlueColor = '#4C6079'; 2 | export const BlueColor = '#027AFF'; 3 | export const portR = 5; 4 | export const strokeWidth = 1; -------------------------------------------------------------------------------- /packages/plugin-x6-designer/src/graph/register/shape/lce-circle.ts: -------------------------------------------------------------------------------- 1 | export const LceCircle = { 2 | inherit: 'circle', 3 | width: 50, 4 | height: 50, 5 | attrs: { 6 | circle: { 7 | stroke: '#4C6079', 8 | strokeWidth: 1, 9 | fill: '#fff', 10 | r: 50, 11 | }, 12 | text: { 13 | fontSize: 12, 14 | fill: '#4C6079', 15 | x: 10, 16 | }, 17 | }, 18 | ports: { 19 | groups: { 20 | top: { 21 | position: 'top', 22 | attrs: { 23 | circle: { 24 | r: 3, 25 | magnet: true, 26 | stroke: '#A5AFBC', 27 | strokeWidth: 1, 28 | fill: '#fff', 29 | style: { 30 | visibility: 'hidden', 31 | }, 32 | }, 33 | }, 34 | zIndex: 99, 35 | }, 36 | right: { 37 | position: 'right', 38 | attrs: { 39 | circle: { 40 | r: 3, 41 | magnet: true, 42 | stroke: '#A5AFBC', 43 | strokeWidth: 1, 44 | fill: '#fff', 45 | style: { 46 | visibility: 'hidden', 47 | }, 48 | }, 49 | }, 50 | zIndex: 99, 51 | }, 52 | bottom: { 53 | position: 'bottom', 54 | attrs: { 55 | circle: { 56 | r: 3, 57 | magnet: true, 58 | stroke: '#A5AFBC', 59 | strokeWidth: 1, 60 | fill: '#fff', 61 | style: { 62 | visibility: 'hidden', 63 | }, 64 | }, 65 | }, 66 | zIndex: 99, 67 | }, 68 | left: { 69 | position: 'left', 70 | attrs: { 71 | circle: { 72 | r: 3, 73 | magnet: true, 74 | stroke: '#A5AFBC', 75 | strokeWidth: 1, 76 | fill: '#fff', 77 | style: { 78 | visibility: 'hidden', 79 | }, 80 | }, 81 | }, 82 | zIndex: 99, 83 | }, 84 | }, 85 | items: [ 86 | { 87 | id: 't', 88 | group: 'top', 89 | }, 90 | { 91 | id: 'r', 92 | group: 'right', 93 | }, 94 | { 95 | id: 'b', 96 | group: 'bottom', 97 | }, 98 | { 99 | id: 'l', 100 | group: 'left', 101 | }, 102 | ], 103 | }, 104 | } -------------------------------------------------------------------------------- /packages/plugin-x6-designer/src/graph/register/shape/lce-diamond.ts: -------------------------------------------------------------------------------- 1 | import { BlueColor, DeepBlueColor, portR, strokeWidth } from "./const"; 2 | 3 | export const LceDiamond = { 4 | inherit: 'polygon', 5 | width: 124, 6 | height: 70, 7 | attrs: { 8 | body: { 9 | stroke: DeepBlueColor, 10 | strokeWidth, 11 | points: "0, 35, 62, 0, 124, 35, 62, 70", 12 | fill: '#fff', 13 | rx: 10, 14 | ry: 10 15 | }, 16 | text: { 17 | width: 60, 18 | height: 32, 19 | fontSize: 12, 20 | fill: DeepBlueColor, 21 | x: 13, 22 | lineHeight: 16, 23 | textWrap: { 24 | width: 50, 25 | height: 14, 26 | ellipsis: true, // 文本超出显示范围时,自动添加省略号 27 | breakWord: true, // 是否截断单词 28 | } 29 | }, 30 | image: { 31 | width: 20, 32 | height: 20, 33 | x: 30, 34 | y: 24, 35 | }, 36 | }, 37 | markup: [ 38 | { 39 | tagName: 'polygon', 40 | selector: 'body', 41 | }, 42 | { 43 | tagName: 'text', 44 | selector: 'text', 45 | }, 46 | { 47 | tagName: 'image', 48 | selector: 'image', 49 | }, 50 | ], 51 | ports: { 52 | groups: { 53 | top: { 54 | position: 'top', 55 | attrs: { 56 | circle: { 57 | r: portR, 58 | magnet: true, 59 | stroke: BlueColor, 60 | strokeWidth: 1, 61 | fill: '#fff', 62 | style: { 63 | visibility: 'hidden', 64 | opacity: '1' 65 | }, 66 | }, 67 | }, 68 | zIndex: 99, 69 | }, 70 | right: { 71 | position: 'right', 72 | attrs: { 73 | circle: { 74 | r: portR, 75 | magnet: true, 76 | stroke: BlueColor, 77 | strokeWidth: 1, 78 | fill: '#fff', 79 | style: { 80 | visibility: 'hidden', 81 | opacity: '1' 82 | }, 83 | }, 84 | }, 85 | zIndex: 99, 86 | }, 87 | bottom: { 88 | position: 'bottom', 89 | attrs: { 90 | circle: { 91 | r: portR, 92 | magnet: true, 93 | stroke: BlueColor, 94 | strokeWidth: 1, 95 | fill: '#fff', 96 | style: { 97 | visibility: 'hidden', 98 | opacity: '1' 99 | }, 100 | }, 101 | }, 102 | zIndex: 99, 103 | }, 104 | left: { 105 | position: 'left', 106 | attrs: { 107 | circle: { 108 | r: portR, 109 | magnet: true, 110 | stroke: BlueColor, 111 | strokeWidth: 1, 112 | fill: '#fff', 113 | style: { 114 | visibility: 'hidden', 115 | opacity: '1' 116 | }, 117 | }, 118 | }, 119 | zIndex: 99, 120 | }, 121 | }, 122 | items: [ 123 | { 124 | id: 't', 125 | group: 'top', 126 | }, 127 | { 128 | id: 'r', 129 | group: 'right', 130 | }, 131 | { 132 | id: 'b', 133 | group: 'bottom', 134 | }, 135 | { 136 | id: 'l', 137 | group: 'left', 138 | }, 139 | ], 140 | }, 141 | } -------------------------------------------------------------------------------- /packages/plugin-x6-designer/src/graph/register/shape/lce-rect.ts: -------------------------------------------------------------------------------- 1 | import { portR, BlueColor, strokeWidth } from "./const"; 2 | 3 | export const LceRect = { 4 | inherit: 'rect', 5 | width: 124, 6 | height: 42, 7 | attrs: { 8 | body: { 9 | stroke: '#4C6079', 10 | strokeWidth, 11 | fill: '#fff', 12 | rx: 21, 13 | ry: 21, 14 | }, 15 | text: { 16 | width: 60, 17 | height: 32, 18 | fontSize: 12, 19 | fill: '#4C6079', 20 | x: 10, 21 | lineHeight: 16, 22 | textWrap: { 23 | width: 60, 24 | height: '80%', 25 | ellipsis: true, // 文本超出显示范围时,自动添加省略号 26 | breakWord: true, // 是否截断单词 27 | } 28 | }, 29 | image: { 30 | width: 30, 31 | height: 30, 32 | x: 8, 33 | y: 6, 34 | }, 35 | }, 36 | markup: [ 37 | { 38 | tagName: 'rect', 39 | selector: 'body', 40 | }, 41 | { 42 | tagName: 'text', 43 | selector: 'text', 44 | }, 45 | { 46 | tagName: 'image', 47 | selector: 'image', 48 | }, 49 | ], 50 | ports: { 51 | groups: { 52 | top: { 53 | position: 'top', 54 | attrs: { 55 | circle: { 56 | r: portR, 57 | magnet: true, 58 | stroke: BlueColor, 59 | strokeWidth: 1, 60 | fill: '#fff', 61 | style: { 62 | visibility: 'hidden', 63 | opacity: '1' 64 | }, 65 | }, 66 | }, 67 | zIndex: 99, 68 | }, 69 | right: { 70 | position: 'right', 71 | attrs: { 72 | circle: { 73 | r: portR, 74 | magnet: true, 75 | stroke: BlueColor, 76 | strokeWidth: 1, 77 | fill: '#fff', 78 | style: { 79 | visibility: 'hidden', 80 | opacity: '1' 81 | }, 82 | }, 83 | }, 84 | zIndex: 99, 85 | }, 86 | bottom: { 87 | position: 'bottom', 88 | attrs: { 89 | circle: { 90 | r: portR, 91 | magnet: true, 92 | stroke: BlueColor, 93 | strokeWidth: 1, 94 | fill: '#fff', 95 | style: { 96 | visibility: 'hidden', 97 | opacity: '1' 98 | }, 99 | }, 100 | }, 101 | zIndex: 99, 102 | }, 103 | left: { 104 | position: 'left', 105 | attrs: { 106 | circle: { 107 | r: portR, 108 | magnet: true, 109 | stroke: BlueColor, 110 | strokeWidth: 1, 111 | fill: '#fff', 112 | style: { 113 | visibility: 'hidden', 114 | opacity: '1' 115 | }, 116 | }, 117 | }, 118 | zIndex: 99, 119 | }, 120 | }, 121 | items: [ 122 | { 123 | id: 't', 124 | group: 'top', 125 | }, 126 | { 127 | id: 'r', 128 | group: 'right', 129 | }, 130 | { 131 | id: 'b', 132 | group: 'bottom', 133 | }, 134 | { 135 | id: 'l', 136 | group: 'left', 137 | }, 138 | ], 139 | }, 140 | } -------------------------------------------------------------------------------- /packages/plugin-x6-designer/src/graph/registerShape.ts: -------------------------------------------------------------------------------- 1 | import { LceCircle } from './register/shape/lce-circle'; 2 | import { Graph } from '@antv/x6'; 3 | import { LceDiamond } from './register/shape/lce-diamond'; 4 | import { LceRect } from './register/shape/lce-rect'; 5 | 6 | function registerNode(nodeName: string, node: any) { 7 | // 在 registerNode 前进行 unregisterNode, 防止在多资源场景下由于注册重复的 node 导致报错。 8 | Graph.unregisterNode(nodeName); 9 | Graph.registerNode(nodeName, node); 10 | } 11 | 12 | /** 13 | * 注册 shape 14 | */ 15 | export function registerShape() { 16 | registerNode('lce-rect', LceRect); 17 | 18 | registerNode('lce-diamond', LceDiamond); 19 | 20 | registerNode('lce-circle', LceCircle); 21 | } 22 | -------------------------------------------------------------------------------- /packages/plugin-x6-designer/src/graph/util.ts: -------------------------------------------------------------------------------- 1 | import { FunctionExt, EdgeView, CellView, Edge, Point, Registry, Line } from '@antv/x6'; 2 | 3 | /** 4 | * 合并 points 5 | * 三个点在一条直线上的,删掉中间点 6 | * @param edgeView 7 | * @param routerPoints 8 | * @returns 9 | */ 10 | export function getVertices(edgeView: EdgeView, routerPoints: Array<any>) { 11 | const source = edgeView.sourceAnchor; 12 | const target = edgeView.targetAnchor; 13 | 14 | const points = [source, ...routerPoints, target] 15 | let indexArr: any[] = []; 16 | if (points.length > 2) { 17 | for (let i = 0; i <= points.length - 3; i++) { 18 | const isXEqual = Math.abs(points[i].x - points[i+1].x) < 1 && Math.abs(points[i+1].x - points[i+2].x) < 1; 19 | const isYEqual = Math.abs(points[i].y - points[i+1].y) < 1 && Math.abs(points[i+1].y - points[i+2].y) < 1; 20 | if (isXEqual || isYEqual) { 21 | indexArr.push(i+1); 22 | } 23 | } 24 | } 25 | 26 | const newPoints = points.filter((v, index) => !(indexArr as any).includes(index)); 27 | return newPoints; 28 | } 29 | 30 | /** 31 | * 节点 ports 显示隐藏 32 | * @param ports 33 | * @param show 34 | */ 35 | export function showPorts(ports: NodeListOf<SVGAElement>, show: boolean) { 36 | for (let i = 0, len = ports.length; i < len; i = i + 1) { 37 | ports[i].style.visibility = show ? 'visible' : 'hidden'; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/plugin-x6-designer/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { ILowCodePluginContext, project } from '@alilc/lowcode-engine'; 2 | import DesignerView from './DesignerView'; 3 | import { rootState } from './items/state'; 4 | import x6Designer, { IDesigner } from './designer'; 5 | import '@antv/x6-react-shape'; // 支持自定义 react 组件 6 | 7 | /** 8 | * plugin X6 designer 9 | * @param ctx 10 | * @returns 11 | */ 12 | const PluginX6Designer = (ctx: ILowCodePluginContext) => { 13 | return { 14 | exports() { 15 | return x6Designer; 16 | }, 17 | init() { 18 | const { skeleton, project } = ctx; 19 | skeleton.remove({ 20 | name: 'designer', 21 | area: 'mainArea', 22 | type: 'Widget' 23 | }); 24 | skeleton.add({ 25 | area: 'mainArea', 26 | name: 'designer', 27 | type: 'Widget', 28 | content: DesignerView, 29 | contentProps: { 30 | ctx, 31 | } 32 | }); 33 | 34 | // bind nodes state 35 | rootState.bindNodes(project.currentDocument); 36 | 37 | project.onChangeDocument((doc) => { 38 | rootState.disposeDocumentEvent(); 39 | rootState.bindNodes(project.currentDocument); 40 | }); 41 | } 42 | } 43 | } 44 | 45 | PluginX6Designer.pluginName = 'plugin-x6-designer'; 46 | export default PluginX6Designer; 47 | export { IDesigner }; 48 | -------------------------------------------------------------------------------- /packages/plugin-x6-designer/src/items/edge/index.tsx: -------------------------------------------------------------------------------- 1 | import { Graph, Edge } from '@antv/x6'; 2 | import React from 'react'; 3 | import { Node as NodeModel } from '@alilc/lowcode-shell'; 4 | import { getComponentView, updateNodeProps } from '../utils'; 5 | import designer from '../../designer'; 6 | 7 | interface Props { 8 | onMountEdge: (edge: Edge) => void; 9 | onUnMountEdge: (edge: Edge) => void; 10 | 11 | graph: Graph; 12 | model: NodeModel; 13 | ctx: any; 14 | } 15 | 16 | /** 17 | * edge component for x6 edge render 18 | */ 19 | class EdgeComponent extends React.PureComponent<Props> { 20 | private edge: Edge; 21 | 22 | componentDidMount() { 23 | const { model, graph, ctx } = this.props; 24 | const { project } = ctx; 25 | 26 | // 创建 edge 27 | const view = getComponentView(model); 28 | this.edge = graph.createEdge({ 29 | id: model.id, 30 | ...view 31 | }); 32 | 33 | // 收集 edge 统一添加到画布 34 | this.props.onMountEdge(this.edge); 35 | 36 | // set edge vertices 37 | // @ts-ignore 38 | const { source, target } = model.propsData; 39 | 40 | // set source & target 41 | this.edge.setSource({ cell: source }); 42 | this.edge.setTarget({ cell: target }); 43 | 44 | // 渲染逻辑切面 45 | const onEdgeRender = designer.onEdgeRender(); 46 | // 渲染逻辑切面 47 | for (const cb of onEdgeRender) { 48 | cb(model, this.edge); 49 | } 50 | 51 | // model 更新渲染 52 | project.currentDocument?.onChangeNodeProp(({ key, oldValue, newValue, node }) => { 53 | if (node.id !== model.id) { 54 | return; 55 | } 56 | 57 | if (key === 'source') { 58 | this.edge.setSource({ cell: newValue }); 59 | } 60 | 61 | if (key === 'target') { 62 | this.edge.setTarget({ cell: newValue }); 63 | } 64 | 65 | // 用户自定义渲染逻辑切面 66 | for (const cb of onEdgeRender) { 67 | cb(model, this.edge); 68 | } 69 | }); 70 | } 71 | 72 | componentWillUnmount() { 73 | // 删除节点 74 | this.props.onUnMountEdge(this.edge); 75 | } 76 | 77 | render() { 78 | return null; 79 | } 80 | } 81 | 82 | export default EdgeComponent; 83 | 84 | -------------------------------------------------------------------------------- /packages/plugin-x6-designer/src/items/index.less: -------------------------------------------------------------------------------- 1 | .x6-widget-selection-box { 2 | z-index: -10; 3 | } 4 | 5 | .x6-widget-stencil-content { 6 | border: 1px solid #EDEDED; 7 | box-shadow: 0 4px 15px 0 rgb(31 56 88 / 15%); 8 | border-radius: 4px; 9 | background-color: #FFFFFF; 10 | } 11 | 12 | .next-overlay-wrapper .next-balloon { 13 | padding: 10px 16px; 14 | } 15 | 16 | .x6-port-body { 17 | &:hover { 18 | color: #4C6079; 19 | transform: scale(1.3); 20 | } 21 | } -------------------------------------------------------------------------------- /packages/plugin-x6-designer/src/items/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import NodeComponent from './node'; 4 | import EdgeComponent from './edge'; 5 | import { Node, Edge, Graph } from '@antv/x6'; 6 | import { EdgeComponentName, rootState, RootState } from "./state"; 7 | import './index.less'; 8 | 9 | interface Props { 10 | graph: Graph; 11 | } 12 | 13 | /** 14 | * render node & edge model 15 | * observable nodes 16 | */ 17 | @observer 18 | class Nodes extends React.PureComponent<Props> { 19 | nodes: Node[] = []; 20 | edges: Edge[] = []; 21 | mounted: boolean = false; // 是否 didMounted 22 | 23 | componentDidMount() { 24 | const { graph } = this.props; 25 | graph.resetCells([...this.nodes, ...this.edges]); 26 | this.mounted = true; 27 | } 28 | 29 | onMountEdge = (edge: Edge) => { 30 | const { graph } = this.props; 31 | this.edges.push(edge); 32 | 33 | if (this.mounted) { 34 | graph.addEdge(edge); 35 | } 36 | } 37 | 38 | onMountNode = (node: Node) => { 39 | const { graph } = this.props; 40 | this.nodes.push(node); 41 | 42 | if (this.mounted) { 43 | graph.addNode(node); 44 | } 45 | } 46 | 47 | onUnMountNode = (node: Node) => { 48 | const { graph } = this.props; 49 | const index = this.nodes.indexOf(node); 50 | this.nodes.splice(index, 1); 51 | 52 | if (this.mounted) { 53 | graph.removeCell(node); 54 | } 55 | } 56 | 57 | onUnMountEdge = (edge: Edge) => { 58 | const { graph } = this.props; 59 | const index = this.edges.indexOf(edge); 60 | this.edges.splice(index, 1); 61 | 62 | if (this.mounted) { 63 | graph.removeCell(edge); 64 | } 65 | } 66 | 67 | render() { 68 | const { graph } = this.props; 69 | const nodes = rootState.getNodes(); 70 | const items = nodes.filter(v => typeof v.isPage === 'function' ? !v.isPage() : !v.isPage); 71 | return ( 72 | <div className="editor-graph"> 73 | { 74 | items.map(node => { 75 | if (node.componentMeta?.getMetadata().tags?.includes('edge')) { 76 | return ( 77 | <EdgeComponent 78 | key={node.id} 79 | onMountEdge={this.onMountEdge} 80 | onUnMountEdge={this.onUnMountEdge} 81 | model={node} 82 | graph={graph} 83 | ctx={this.props.ctx} 84 | /> 85 | ) 86 | } else { 87 | return ( 88 | <NodeComponent 89 | key={node.id} 90 | onMountNode={this.onMountNode} 91 | onUnMountNode={this.onUnMountNode} 92 | model={node} 93 | graph={graph} 94 | ctx={this.props.ctx} 95 | />) 96 | } 97 | }) 98 | } 99 | </div> 100 | ) 101 | } 102 | } 103 | 104 | export default Nodes; 105 | -------------------------------------------------------------------------------- /packages/plugin-x6-designer/src/items/node/index.tsx: -------------------------------------------------------------------------------- 1 | import { Graph, Node } from '@antv/x6'; 2 | import React from 'react'; 3 | import { Node as NodeModel } from '@alilc/lowcode-shell'; 4 | import designer from '../../designer'; 5 | import { getComponentView, updateNodeProps } from '../utils'; 6 | 7 | interface Props { 8 | onMountNode: (node: Node) => void; 9 | onUnMountNode: (node: Node) => void; 10 | 11 | graph: typeof Graph; 12 | model: NodeModel; 13 | ctx: any; 14 | } 15 | 16 | 17 | 18 | /** 19 | * node component for x6 node render 20 | */ 21 | class NodeComponent extends React.PureComponent<Props> { 22 | private node: Node; 23 | // 节点区分是否有自定义html渲染 24 | private nodeDefinedType: 'shape' | 'component'; 25 | 26 | componentDidMount() { 27 | // 添加节点 28 | const { model, graph, ctx } = this.props; 29 | const { project } = ctx; 30 | const view = getComponentView(model); 31 | this.nodeDefinedType = view?.component ? 'component' : 'shape'; 32 | // 基于 Schema 数据恢复节点,保持 id 和 ports 一致 33 | this.node = graph.createNode({ 34 | id: model.id, 35 | ports: model.propsData.ports, 36 | ...view 37 | }); 38 | 39 | // 收集 node 统一添加到画布 40 | this.props.onMountNode(this.node); 41 | 42 | // @ts-ignore 43 | const { position } = model.propsData; 44 | // 定位 45 | this.node.setPosition(position); 46 | // 加载自定义节点渲染逻辑 47 | const onNodeRenderCb = designer.onNodeRender(); 48 | 49 | // 用户自定义渲染逻辑切面 50 | if (this.nodeDefinedType === 'shape' && onNodeRenderCb.length > 0) { 51 | for (const cb of onNodeRenderCb) { 52 | cb(model, this.node); 53 | } 54 | } else { 55 | updateNodeProps(model, this.node); 56 | } 57 | 58 | // model 更新触发渲染 59 | project.currentDocument?.onChangeNodeProp(({ key, oldValue, newValue, node }) => { 60 | if (node.id !== model.id) { 61 | return; 62 | } 63 | 64 | if (key === 'position') { 65 | this.node.setPosition(newValue); 66 | return; 67 | } 68 | 69 | // 用户自定义渲染逻辑切面 70 | if (this.nodeDefinedType === 'shape' && onNodeRenderCb.length > 0) { 71 | for (const cb of onNodeRenderCb) { 72 | cb(model, this.node); 73 | } 74 | } else { 75 | this.node.prop(key, newValue); 76 | } 77 | }); 78 | } 79 | 80 | componentWillUnmount() { 81 | // 删除节点 82 | this.props.onUnMountNode(this.node); 83 | } 84 | 85 | render() { 86 | return null; 87 | } 88 | } 89 | 90 | export default NodeComponent; 91 | 92 | -------------------------------------------------------------------------------- /packages/plugin-x6-designer/src/items/state.ts: -------------------------------------------------------------------------------- 1 | import { observable, action, makeObservable } from "mobx"; 2 | import { IPublicModelDocumentModel } from '@alilc/lowcode-types'; 3 | 4 | export const EdgeComponentName = 'Line'; // 边 组件独一 componentName 5 | 6 | 7 | export class RootState { 8 | @observable.shallow private nodes: any[] = []; // 单独存一份 nodes 状态数据 9 | 10 | documentEvent: Function[] = []; 11 | 12 | constructor() { 13 | makeObservable(this); 14 | } 15 | 16 | @action 17 | setNodes(nodes: any[]) { 18 | this.nodes = nodes; 19 | } 20 | 21 | getNodes() { 22 | return this.nodes; 23 | } 24 | 25 | disposeDocumentEvent() { 26 | this.documentEvent.forEach(d => d && d()); 27 | this.documentEvent = []; 28 | } 29 | 30 | bindNodes(document: IPublicModelDocumentModel | null) { 31 | if (document) { 32 | this.nodes = Array.from(document.nodesMap.values() || []); 33 | this.documentEvent = [ 34 | document.onImportSchema(() => { 35 | this.setNodes(Array.from(document.nodesMap.values() || [])); 36 | }), 37 | document.onAddNode((node) => { 38 | this.nodes.push(node); 39 | this.setNodes(this.nodes); 40 | }), 41 | document.onRemoveNode((node) => { 42 | const _nodes = this.nodes.filter(v => v.id !== node.id); 43 | this.setNodes(_nodes); 44 | }), 45 | // 撤销恢复不会触发 onAddNode 和 onRemoveNode 事件 46 | document.history.onChangeState((...data: any) => { 47 | this.nodes = Array.from(document.nodesMap.values() || []); 48 | }), 49 | ]; 50 | } 51 | } 52 | } 53 | 54 | export const rootState = new RootState(); -------------------------------------------------------------------------------- /packages/plugin-x6-designer/src/items/utils.ts: -------------------------------------------------------------------------------- 1 | import { Node } from '@antv/x6'; 2 | import { Node as NodeModel } from '@alilc/lowcode-shell'; 3 | import { material } from '@alilc/lowcode-engine'; 4 | import { get } from 'lodash'; 5 | 6 | export const getComponentView = (nodeModel: NodeModel) => { 7 | const { componentName } = nodeModel; 8 | const global: any = window; 9 | const assets = material.getAssets(); 10 | const componentMeta = material.getComponentMeta(componentName); 11 | 12 | if (!componentMeta || !componentMeta.npm) { 13 | console.error(`${componentName} is not a npm component`); 14 | return null; 15 | } 16 | 17 | const { package: componentPackage, destructuring, exportName } = componentMeta.npm; 18 | 19 | const library = assets.packages.find((item: any) => item.package === componentPackage).library; 20 | 21 | if (!library) { 22 | console.error(`${componentName} library is not defined`); 23 | return null; 24 | } 25 | 26 | if (destructuring && exportName) { 27 | return typeof global[library][exportName] === 'function' ? global[library][exportName]() : global[library][exportName]; 28 | } 29 | 30 | return typeof global[library] === 'function' ? global[library]() : global[library]; 31 | } 32 | 33 | export function getPropList(model: NodeModel) { 34 | const configure = model.componentMeta?.configure; 35 | 36 | if (!Array.isArray(configure)) { 37 | return []; 38 | } 39 | 40 | const props = configure.find(item => item.name === '#props'); 41 | 42 | if (!Array.isArray(props?.items)) { 43 | return []; 44 | } 45 | 46 | const propsData = model.propsData || {}; 47 | 48 | return props?.items?.map(item => { 49 | const value = get(propsData, item.name) ?? item.defaultValue; 50 | return { 51 | name: item.name, 52 | value, 53 | }; 54 | }); 55 | } 56 | 57 | export function updateNodeProps(model: NodeModel, node: Node) { 58 | const propList = getPropList(model) || []; 59 | propList.forEach(item => { 60 | node.prop(item.name, item.value); 61 | }); 62 | } -------------------------------------------------------------------------------- /packages/plugin-x6-designer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib" 5 | }, 6 | "include": [ 7 | "./src/" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /scripts/create.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | DIR_NAME=$1 4 | 5 | if [ -z $DIR_NAME ];then 6 | echo 'Usage: ./create.sh <folder-name>' 7 | exit 0 8 | fi 9 | 10 | if [ -d packages/$DIR_NAME ];then 11 | echo 'Folder is existing!' 12 | exit 0 13 | fi 14 | 15 | mkdir packages/$DIR_NAME 16 | 17 | cp -r templates/* packages/$DIR_NAME 18 | 19 | mv packages/$DIR_NAME/_tsconfig.json packages/$DIR_NAME/tsconfig.json 20 | 21 | echo 'Add package successfully.' 22 | -------------------------------------------------------------------------------- /scripts/publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | WORK_DIR=$PWD 4 | BUILD_DEST=$1 5 | 6 | if [ ! -d "$BUILD_DEST" ]; then 7 | mkdir -p "$BUILD_DEST" 8 | fi 9 | 10 | npm i -g n 11 | # 使用官方源有较大概率会 block 12 | export N_NODE_MIRROR=https://npm.taobao.org/mirrors/node 13 | 14 | echo "Switch node version to 16" 15 | n 16.15.1 16 | echo "Node Version" 17 | node -v 18 | 19 | echo "Deploy ${WORK_DIR} -> ${BUILD_DEST} ..." 20 | 21 | echo "Setup" 22 | npm run setup 23 | npm run build 24 | 25 | mv ./packages/bootstrap/build/* $BUILD_DEST 26 | 27 | echo "Complete" 28 | -------------------------------------------------------------------------------- /scripts/setup.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const del = require('del'); 3 | const gulp = require('gulp'); 4 | const execa = require('execa'); 5 | 6 | async function deleteRootDirLockFile() { 7 | await del('package-lock.json'); 8 | await del('yarn.lock'); 9 | } 10 | 11 | async function clean() { 12 | await execa.command('lerna clean -y', { stdio: 'inherit', encoding: 'utf-8' }); 13 | } 14 | 15 | async function deletePackagesDirLockFile() { 16 | await del('packages/**/package-lock.json'); 17 | } 18 | 19 | async function bootstrap() { 20 | await execa.command('lerna bootstrap --force-local', { stdio: 'inherit', encoding: 'utf-8' }); 21 | } 22 | 23 | const setup = gulp.series(deleteRootDirLockFile, clean, deletePackagesDirLockFile, bootstrap); 24 | 25 | os.type() === 'Windows_NT' ? setup() : execa.command('scripts/setup.sh', { stdio: 'inherit', encoding: 'utf-8' }); -------------------------------------------------------------------------------- /scripts/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | rm -rf package-lock.json yarn.lock 4 | lerna clean -y 5 | find ./packages -type f -name "package-lock.json" -exec rm -f {} \; 6 | 7 | lerna bootstrap --force-local 8 | -------------------------------------------------------------------------------- /scripts/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | pkgName="@alilc/lce-graph" 4 | 5 | if [ "$1" ]; then 6 | pkgName="$1" 7 | fi 8 | 9 | lerna exec --scope $pkgName -- npm start 10 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | const app = require('express')(); 2 | const http = require('http').Server(app); 3 | const io = require('socket.io')(http, { 4 | cors: { 5 | origin: "http://localhost:6006", 6 | methods: ["GET", "POST"] 7 | } 8 | }); 9 | const port = process.env.PORT || 3000; 10 | 11 | io.on('connection', (socket) => { 12 | socket.on('updateTree', msg => { 13 | console.log(msg); 14 | io.emit('updateTree', msg); 15 | }); 16 | }); 17 | 18 | http.listen(port, () => { 19 | console.log(`Socket.IO server running at http://localhost:${port}/`); 20 | }); 21 | -------------------------------------------------------------------------------- /templates/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "import", 5 | { 6 | "libraryName": "@alife/next", 7 | "libraryDirectory": "lib", 8 | "style": true, 9 | "camel2DashComponentName": true 10 | } 11 | ] 12 | ] 13 | } -------------------------------------------------------------------------------- /templates/README.md: -------------------------------------------------------------------------------- 1 | TODO 2 | --- 3 | -------------------------------------------------------------------------------- /templates/_tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib" 5 | }, 6 | "include": [ 7 | "./src/" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /templates/build.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "build-plugin-component" 5 | ] 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /templates/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@alilc/lce-graph-", 3 | "version": "1.0.0", 4 | "description": "for Graph Editor Engine", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "files": [ 8 | "lib", 9 | "es" 10 | ], 11 | "scripts": { 12 | "test": "jest -c jest.config.js", 13 | "build": "build-scripts build --skip-demo", 14 | "build:watch": "build-scripts build --skip-demo --watch" 15 | }, 16 | "license": "MIT", 17 | "publishConfig": { 18 | "access": "public", 19 | "registry": "https://registry.npmjs.org/" 20 | }, 21 | "devDependencies": { 22 | "build-plugin-component": "^1.10.0", 23 | "@alib/build-scripts": "^0.1.18" 24 | } 25 | } -------------------------------------------------------------------------------- /templates/src/index.ts: -------------------------------------------------------------------------------- 1 | // 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "lib": [ 5 | "es2015", 6 | "dom" 7 | ], 8 | // Target latest version of ECMAScript. 9 | "target": "es6", 10 | // Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. 11 | "module": "esnext", 12 | // Search under node_modules for non-relative imports. 13 | "moduleResolution": "node", 14 | // Enable strictest settings like strictNullChecks & noImplicitAny. 15 | "strict": true, 16 | "strictPropertyInitialization": false, 17 | // Allow default imports from modules with no default export. This does not affect code emit, just typechecking. 18 | "allowSyntheticDefaultImports": true, 19 | // Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. 20 | "esModuleInterop": true, 21 | // Specify JSX code generation: 'preserve', 'react-native', or 'react'. 22 | "jsx": "preserve", 23 | // Import emit helpers (e.g. __extends, __rest, etc..) from tslib 24 | "importHelpers": true, 25 | // Enables experimental support for ES7 decorators. 26 | "experimentalDecorators": true, 27 | "emitDecoratorMetadata": true, 28 | // Generates corresponding .map file. 29 | "sourceMap": true, 30 | // Disallow inconsistently-cased references to the same file. 31 | "forceConsistentCasingInFileNames": true, 32 | // Allow json import 33 | "resolveJsonModule": true, 34 | // skip type checking of declaration files 35 | "skipLibCheck": true, 36 | "baseUrl": "./packages", 37 | "paths": { 38 | "@alilc/lce-graph-core": [ 39 | "plugin-core/src" 40 | ], 41 | "@alilc/lce-graph-g6-designer": [ 42 | "plugin-g6-designer/src" 43 | ], 44 | "@alilc/lce-graph-x6-designer": [ 45 | "plugin-x6-designer/src" 46 | ], 47 | "@alilc/lce-graph-tools": [ 48 | "plugin-tools/src" 49 | ], 50 | "@alilc/g-react-renderer": [ 51 | "g-react-renderer/src" 52 | ], 53 | "@alilc/lce-graph-materials-pane": [ 54 | "plugin-materials-pane/src" 55 | ], 56 | }, 57 | "outDir": "lib" 58 | }, 59 | "exclude": [ 60 | "**/test", 61 | "**/lib", 62 | "**/es", 63 | "node_modules" 64 | ] 65 | } --------------------------------------------------------------------------------