├── .gitignore ├── .prettierrc ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── img ├── alipay.jpg ├── qq-group.jpg ├── view1.jpg ├── view2.jpg └── wechatpay.jpg ├── index.html ├── package.json ├── public └── favicon.ico ├── src ├── App.vue ├── assets │ ├── icons │ │ └── svg │ │ │ ├── assignment.svg │ │ │ ├── decision.svg │ │ │ ├── deepLearning.svg │ │ │ ├── endParallel.svg │ │ │ ├── logo.svg │ │ │ ├── machineLearning.svg │ │ │ ├── start.svg │ │ │ └── startParallel.svg │ ├── images │ │ ├── logo-circle.jpg │ │ └── logo.jpg │ ├── styles │ │ ├── element-ui-theme.scss │ │ ├── element-ui.scss │ │ ├── index.scss │ │ ├── mixin.scss │ │ └── variables.module.scss │ └── vue.svg ├── components │ └── SvgIcon │ │ └── index.vue ├── main.ts ├── style.css ├── utils │ ├── elementIcons.ts │ ├── index.ts │ └── options.ts ├── views │ └── design │ │ ├── LFComponents │ │ ├── Control.vue │ │ ├── NodePanel.vue │ │ ├── top.vue │ │ └── view-json.vue │ │ ├── PropertySetting │ │ └── PropertyDialog.vue │ │ ├── index.vue │ │ ├── registerEdge │ │ ├── myBezier.vue │ │ └── registerBezier.ts │ │ ├── registerNode │ │ ├── assignment │ │ │ ├── assignment.ts │ │ │ ├── assignment.vue │ │ │ └── assignmentProperty.vue │ │ ├── decision │ │ │ ├── decision.ts │ │ │ ├── decision.vue │ │ │ └── decisionProperty.vue │ │ ├── deepLearning │ │ │ ├── deepLearning.ts │ │ │ ├── deepLearning.vue │ │ │ └── deepLearningProperty.vue │ │ ├── endParallel │ │ │ ├── endParallel.ts │ │ │ ├── endParallel.vue │ │ │ └── endParallelProperty.vue │ │ ├── machineLearning │ │ │ ├── machineLearning.ts │ │ │ ├── machineLearning.vue │ │ │ └── machineLearningProperty.vue │ │ ├── start │ │ │ ├── start.ts │ │ │ ├── start.vue │ │ │ └── startProperty.vue │ │ └── startParallel │ │ │ ├── startParallel.ts │ │ │ ├── startParallel.vue │ │ │ └── startParallelProperty.vue │ │ └── template.vue └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | package-lock.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "printWidth": 200 6 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 SHOW 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 |
2 | 3 |
4 | 5 | 6 | ## logicflow-vue3 7 | 8 | 🎉 logicflow+vue3+elementplus的流程编排。vue2版本在vue2分支。 9 | 10 |
11 | 12 |
13 |
14 | 15 |
16 | 17 | 18 | 19 | ## 功能亮点 20 | 21 | - 左侧树形,支持无数层 22 | 23 | - 自定义节点图标+文字,样式美观 24 | 25 | 26 | 27 | ## 运行步骤 28 | 29 | 使用node18 30 | 31 | ``` 32 | npm config set registry https://registry.npmmirror.com/ 33 | npm install 34 | npm run dev 35 | ``` 36 | 37 | 38 | 39 | ## 项目集成 40 | 41 | - 下载logicflow依赖 42 | 43 | "@logicflow/core": "^1.2.26", 44 | 45 | "@logicflow/extension": "^1.2.26" 46 | 47 | - 把src/views/design目录复制到自己项目里即可 48 | 49 | 50 | 51 | ## 鸣谢 52 | 53 | - https://github.com/didi/LogicFlow 54 | 55 | - https://site.logic-flow.cn/examples 56 | 57 | 58 | 59 | ## 项目链接 60 | 61 | - https://github.com/xoobom/logicflow-vue3 62 | 63 | 64 | 65 | 66 | ## 沟通交流 67 | 68 | ### QQ群 69 | 70 | 群号:692252235 71 | 72 |
73 | 74 |
75 | 76 | 77 | 78 | 79 | ## 捐赠 80 | 81 | 如果您觉得这个项目帮助到了您,您可以捐赠 :moneybag: 表示鼓励 :coffee: 82 |
83 | 84 | 85 |
-------------------------------------------------------------------------------- /img/alipay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xoobom/logicflow-vue3/ef8cfcac396672e1a8ff183077c8495a3e2a498d/img/alipay.jpg -------------------------------------------------------------------------------- /img/qq-group.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xoobom/logicflow-vue3/ef8cfcac396672e1a8ff183077c8495a3e2a498d/img/qq-group.jpg -------------------------------------------------------------------------------- /img/view1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xoobom/logicflow-vue3/ef8cfcac396672e1a8ff183077c8495a3e2a498d/img/view1.jpg -------------------------------------------------------------------------------- /img/view2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xoobom/logicflow-vue3/ef8cfcac396672e1a8ff183077c8495a3e2a498d/img/view2.jpg -------------------------------------------------------------------------------- /img/wechatpay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xoobom/logicflow-vue3/ef8cfcac396672e1a8ff183077c8495a3e2a498d/img/wechatpay.jpg -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | logicflow-vue3 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logicflow-vue3", 3 | "private": true, 4 | "version": "1.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@element-plus/icons-vue": "^2.3.1", 13 | "@logicflow/core": "^2.0.10", 14 | "@logicflow/extension": "^2.0.10", 15 | "element-plus": "^2.7.0", 16 | "vue": "^3.4.21", 17 | "vue-json-pretty": "^2.4.0" 18 | }, 19 | "devDependencies": { 20 | "@vitejs/plugin-vue": "^5.0.4", 21 | "fast-glob": "^3.3.2", 22 | "sass": "1.75.0", 23 | "typescript": "^5.2.2", 24 | "vite": "^5.2.0", 25 | "vite-plugin-svg-icons": "^2.0.1", 26 | "vue-tsc": "^2.0.6" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xoobom/logicflow-vue3/ef8cfcac396672e1a8ff183077c8495a3e2a498d/public/favicon.ico -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/icons/svg/assignment.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/svg/decision.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/svg/deepLearning.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/svg/endParallel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/svg/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/svg/machineLearning.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/svg/start.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/svg/startParallel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/logo-circle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xoobom/logicflow-vue3/ef8cfcac396672e1a8ff183077c8495a3e2a498d/src/assets/images/logo-circle.jpg -------------------------------------------------------------------------------- /src/assets/images/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xoobom/logicflow-vue3/ef8cfcac396672e1a8ff183077c8495a3e2a498d/src/assets/images/logo.jpg -------------------------------------------------------------------------------- /src/assets/styles/element-ui-theme.scss: -------------------------------------------------------------------------------- 1 | :root:root { 2 | --el-color-primary: #700efa; 3 | --el-color-primary-light-5: #7933e3; 4 | --el-color-primary-light-3: #4b21c4; 5 | --el-menu-item-height: 37px; 6 | --el-menu-sub-item-height: 37px; 7 | --el-border-radius-base: 6px; 8 | // --el-fill-color: #fff; //翻页背景 9 | } 10 | -------------------------------------------------------------------------------- /src/assets/styles/element-ui.scss: -------------------------------------------------------------------------------- 1 | .el-form--inline .el-form-item { 2 | margin-right: 15px !important; //表单右边间距 3 | } 4 | 5 | .el-table { 6 | // border-radius: $appRadius !important; 7 | .el-table__header-wrapper, 8 | .el-table__fixed-header-wrapper { 9 | th { 10 | background-color: $tableHeaderBgColor !important; 11 | text-align: center; //表头居中 12 | } 13 | } 14 | td { 15 | text-align: center !important; //表格内容居中 16 | } 17 | // 多选列的表头--去掉半选状态 18 | .el-checkbox__input.is-indeterminate .el-checkbox__inner { 19 | background-color: unset !important; 20 | border-color: #dcdfe6 !important; 21 | } 22 | } 23 | 24 | //表格高度 25 | .el-table__header { 26 | .el-table__cell { 27 | padding: 10px 0 !important; 28 | } 29 | } 30 | 31 | .el-table__body { 32 | .el-table__cell { 33 | padding: 12px 0 !important; 34 | } 35 | } 36 | 37 | // 表格中斑马纹行的颜色 38 | .el-table--striped .el-table__body tr.el-table__row--striped td.el-table__cell { 39 | background-color: $tableStripedColor !important; 40 | } 41 | 42 | .el-pagination { 43 | justify-content: center; //分页居中 44 | } 45 | .el-overlay-dialog { 46 | display: flex; 47 | justify-content: center; 48 | align-items: center; 49 | } 50 | .el-dialog { 51 | margin: auto !important; 52 | border-radius: $appRadiusLg !important; 53 | 54 | .el-dialog__close { 55 | font-size: 22px; 56 | position: relative; 57 | top: 5px; 58 | } 59 | } 60 | 61 | .el-dialog__footer { 62 | text-align: center !important; //弹窗底部居中 63 | } 64 | .el-dialog__header { 65 | border-bottom: 1px solid #e6e6e6; 66 | padding-bottom: 12px; 67 | } 68 | 69 | .el-dialog__body { 70 | padding: 15px 20px !important; 71 | } 72 | 73 | //表单间距 74 | .el-form-item { 75 | margin-bottom: 24px !important; 76 | } 77 | 78 | //表单错误提示 79 | .el-form-item--default .el-form-item__error { 80 | padding-top: 6px !important; 81 | } 82 | 83 | // danger按钮点击之后颜色不变淡 84 | .el-button--danger.is-link:focus { 85 | color: #f56c6c !important; 86 | } 87 | .el-button--danger.is-link:hover { 88 | color: #fab6b6 !important; 89 | } 90 | 91 | // primary按钮禁用时,颜色应变淡 92 | .el-button--primary.is-link.is-disabled { 93 | opacity: 0.6; 94 | } 95 | 96 | // 按钮active时的效果去掉 97 | .btn-has-bg .el-button--primary:active, 98 | .btn-has-bg, 99 | el-button--primary:focus { 100 | border-color: #700efa !important; 101 | background-color: #700efa !important; 102 | } 103 | 104 | // 解决:el-select+multiple+filterable、tag宽度+input宽度正好到达临界值、select的高度就会无限高低切换 105 | .el-select { 106 | .select-trigger { 107 | .el-select__input { 108 | min-width: 5px; 109 | margin-left: 10px; 110 | } 111 | } 112 | } 113 | 114 | .el-message-box { 115 | border-radius: $appRadiusLg; 116 | } 117 | 118 | //消息-标题、按钮居中 119 | .el-message-box__title, 120 | .el-message-box__btns { 121 | text-align: center !important; 122 | display: flex; 123 | justify-content: center; 124 | } 125 | 126 | .el-message-box__btns { 127 | padding-top: 15px; 128 | padding-bottom: 5px !important; 129 | } 130 | 131 | //消息-内容居中 132 | .el-message-box__content { 133 | display: flex; 134 | align-items: center; 135 | justify-content: center; 136 | } 137 | 138 | //页头 139 | .el-page-header__content { 140 | font-size: 15px !important; 141 | } -------------------------------------------------------------------------------- /src/assets/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import './variables.module.scss'; 2 | @import './element-ui.scss'; 3 | 4 | html, 5 | body, 6 | #app { 7 | height: 100%; 8 | margin: 0px; 9 | padding: 0px; 10 | } 11 | 12 | body { 13 | height: 100%; 14 | margin: 0; 15 | -moz-osx-font-smoothing: grayscale; 16 | -webkit-font-smoothing: antialiased; 17 | text-rendering: optimizeLegibility; 18 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; 19 | // 为全局的文字设置默认的字体大小 20 | // font-size: 12px; 21 | } 22 | 23 | label { 24 | font-weight: 700; 25 | } 26 | 27 | html { 28 | height: 100%; 29 | box-sizing: border-box; 30 | // 方便rem和px之间进行转化 此时1px = 0.1rem 31 | // font-size: 10px; 32 | } 33 | 34 | #app { 35 | height: 100%; 36 | } 37 | 38 | *, 39 | *:before, 40 | *:after { 41 | box-sizing: inherit; 42 | } 43 | 44 | .no-padding { 45 | padding: 0px !important; 46 | } 47 | 48 | .padding-content { 49 | padding: 4px 0; 50 | } 51 | 52 | a:focus, 53 | a:active { 54 | outline: none; 55 | } 56 | 57 | a, 58 | a:focus, 59 | a:hover { 60 | cursor: pointer; 61 | color: inherit; 62 | text-decoration: none; 63 | } 64 | 65 | div:focus { 66 | outline: none; 67 | } 68 | 69 | .fr { 70 | float: right; 71 | } 72 | 73 | .fl { 74 | float: left; 75 | } 76 | 77 | .pr-5 { 78 | padding-right: 5px; 79 | } 80 | 81 | .pl-5 { 82 | padding-left: 5px; 83 | } 84 | 85 | .block { 86 | display: block; 87 | } 88 | 89 | .pointer { 90 | cursor: pointer; 91 | } 92 | 93 | .inlineBlock { 94 | display: block; 95 | } 96 | 97 | .clearfix { 98 | &:after { 99 | visibility: hidden; 100 | display: block; 101 | font-size: 0; 102 | content: ' '; 103 | clear: both; 104 | height: 0; 105 | } 106 | } 107 | 108 | aside { 109 | background: #eef1f6; 110 | padding: 8px 24px; 111 | margin-bottom: 20px; 112 | border-radius: 2px; 113 | display: block; 114 | line-height: 32px; 115 | font-size: 16px; 116 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 117 | color: #2c3e50; 118 | -webkit-font-smoothing: antialiased; 119 | -moz-osx-font-smoothing: grayscale; 120 | 121 | a { 122 | color: #337ab7; 123 | cursor: pointer; 124 | 125 | &:hover { 126 | color: rgb(32, 160, 255); 127 | } 128 | } 129 | } 130 | 131 | //main-container全局样式 132 | .app-container { 133 | border: 1px solid #ebeef5; 134 | background-color: #fff; 135 | transition: 0.3s; 136 | padding: 15px 16px; 137 | border-radius: 12px; 138 | } 139 | 140 | .components-container { 141 | margin: 30px 50px; 142 | position: relative; 143 | } 144 | 145 | .text-center { 146 | text-align: center; 147 | } 148 | 149 | .sub-navbar { 150 | height: 50px; 151 | line-height: 50px; 152 | position: relative; 153 | width: 100%; 154 | text-align: right; 155 | padding-right: 20px; 156 | transition: 600ms ease position; 157 | background: linear-gradient(90deg, rgba(32, 182, 249, 1) 0%, rgba(32, 182, 249, 1) 0%, rgba(33, 120, 241, 1) 100%, rgba(33, 120, 241, 1) 100%); 158 | 159 | .subtitle { 160 | font-size: 20px; 161 | color: #fff; 162 | } 163 | 164 | &.draft { 165 | background: #d0d0d0; 166 | } 167 | 168 | &.deleted { 169 | background: #d0d0d0; 170 | } 171 | } 172 | 173 | .link-type, 174 | .link-type:focus { 175 | color: #337ab7; 176 | cursor: pointer; 177 | 178 | &:hover { 179 | color: rgb(32, 160, 255); 180 | } 181 | } 182 | 183 | .filter-container { 184 | padding-bottom: 10px; 185 | 186 | .filter-item { 187 | display: inline-block; 188 | vertical-align: middle; 189 | margin-bottom: 10px; 190 | } 191 | } 192 | 193 | //refine vue-multiselect plugin 194 | .multiselect { 195 | line-height: 16px; 196 | } 197 | 198 | .multiselect--active { 199 | z-index: 1000 !important; 200 | } 201 | 202 | //分页 203 | .el-pagination { 204 | margin-top: 15px; 205 | text-align: right; 206 | } 207 | 208 | // 微信二维码样式定制 209 | .weichat-container { 210 | height: 300px; 211 | iframe { 212 | width: 100%; 213 | height: 100%; 214 | } 215 | } 216 | 217 | .mt15 { 218 | margin-top: 15px; 219 | } 220 | 221 | .mb15 { 222 | margin-bottom: 15px; 223 | } 224 | 225 | .ml15 { 226 | margin-left: 15px; 227 | } 228 | 229 | .mr15 { 230 | margin-right: 15px; 231 | } 232 | 233 | .mt10 { 234 | margin-top: 10px; 235 | } 236 | 237 | .mb10 { 238 | margin-bottom: 10px; 239 | } 240 | 241 | .ml10 { 242 | margin-left: 10px; 243 | } 244 | 245 | .mr10 { 246 | margin-right: 10px; 247 | } 248 | 249 | .center { 250 | text-align: center; 251 | justify-content: center; 252 | display: flex; 253 | } 254 | 255 | .right { 256 | text-align: right; 257 | } 258 | 259 | //两边靠 260 | .flex-between { 261 | display: flex; 262 | justify-content: space-between; 263 | } 264 | 265 | .flex-center { 266 | display: flex; 267 | justify-content: space-between; 268 | align-items: center; 269 | } 270 | 271 | .color-primary { 272 | color: #700efa; 273 | } 274 | 275 | //json 276 | .json-container { 277 | position: relative; 278 | } 279 | 280 | //json右上角复制 281 | .json-copy { 282 | position: absolute; 283 | right: 20px; 284 | top: 0; 285 | font-size: 16px; 286 | cursor: pointer; 287 | } 288 | -------------------------------------------------------------------------------- /src/assets/styles/mixin.scss: -------------------------------------------------------------------------------- 1 | // 多行文本超出显示省略号 2 | @mixin n-ellipsis($n) { 3 | overflow: hidden; 4 | display: -webkit-box; 5 | -webkit-line-clamp: $n; 6 | -webkit-box-orient: vertical; 7 | text-overflow: ellipsis; 8 | word-break: break-all; 9 | } 10 | 11 | // 单行文本显示超出省略号 12 | @mixin ellipsis($w) { 13 | overflow: hidden; 14 | text-overflow: ellipsis; 15 | white-space: nowrap; 16 | width: $w; 17 | display: inline-block; 18 | } 19 | -------------------------------------------------------------------------------- /src/assets/styles/variables.module.scss: -------------------------------------------------------------------------------- 1 | $base-menu-color: #fff; 2 | $base-menu-color-active: #000; 3 | $base-menu-color-hover: #000; 4 | $base-menu-background: #4921c3; 5 | $base-logo-title-color: #fff; 6 | 7 | $base-menu-light-color: rgba(0, 0, 0, 0.7); 8 | $base-menu-light-background: #ffffff; 9 | $base-logo-light-title-color: #001529; 10 | 11 | $base-sub-menu-background: #fff; 12 | $base-sub-menu-hover: #fff; 13 | 14 | $top-bar-bg-color: #4921c3; 15 | $top-bar-height: 45px; 16 | 17 | $--color-primary: #4921c3; 18 | $--color-success: #67c23a; 19 | $--color-warning: #e6a23c; 20 | $--color-danger: #f56c6c; 21 | $--color-info: #939cb7; 22 | 23 | $base-sidebar-width: 180px; 24 | 25 | $tableStripedColor: #f7f8fa; //表格斑马条纹背景色 26 | $tableHeaderBgColor: #f1f4f7; //表格表头背景 27 | $appRadius: 6px; 28 | $appRadiusLg: 8px; 29 | 30 | $dragPanelBgColor: #f0f4fb; //拖拽面板背景色 31 | $dragNodeBorderColor: #e6f7ff; //拖拽节点边框色 32 | 33 | :export { 34 | menuColor: $base-menu-color; 35 | menuLightColor: $base-menu-light-color; 36 | menuColorActive: $base-menu-color-active; 37 | menuBackground: $base-menu-background; 38 | menuLightBackground: $base-menu-light-background; 39 | subMenuBackground: $base-sub-menu-background; 40 | subMenuHover: $base-sub-menu-hover; 41 | sideBarWidth: $base-sidebar-width; 42 | logoTitleColor: $base-logo-title-color; 43 | logoLightTitleColor: $base-logo-light-title-color; 44 | primaryColor: $--color-primary; 45 | successColor: $--color-success; 46 | dangerColor: $--color-danger; 47 | infoColor: $--color-info; 48 | warningColor: $--color-warning; 49 | dragPanelBgColor: $dragPanelBgColor; 50 | dragNodeBorderColor: $dragNodeBorderColor; 51 | } -------------------------------------------------------------------------------- /src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 44 | 45 | 62 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import './style.css'; 3 | import App from './App.vue'; 4 | import '@/assets/styles/index.scss'; // global css 5 | import ElementPlus from 'element-plus'; 6 | import 'element-plus/dist/index.css'; 7 | import '@/assets/styles/element-ui-theme.scss'; 8 | import 'virtual:svg-icons-register'; // svg图标 9 | import elementIcons from '@/utils/elementIcons'; //@element-plus/icons-vue 10 | 11 | const app = createApp(App); 12 | 13 | app.use(elementIcons); 14 | app.use(ElementPlus, { 15 | size: 'default', 16 | }); 17 | app.mount('#app'); 18 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xoobom/logicflow-vue3/ef8cfcac396672e1a8ff183077c8495a3e2a498d/src/style.css -------------------------------------------------------------------------------- /src/utils/elementIcons.ts: -------------------------------------------------------------------------------- 1 | import * as components from '@element-plus/icons-vue'; 2 | 3 | export default { 4 | install: (app: any) => { 5 | for (const key in components) { 6 | const componentConfig = components[key]; 7 | app.component(componentConfig.name, componentConfig); 8 | } 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { ElMessage } from "element-plus"; 2 | 3 | /** 4 | * @param {string} input value 5 | * @returns {number} output value 6 | */ 7 | export function byteLength(str) { 8 | // returns the byte length of an utf8 string 9 | let s = str.length; 10 | for (var i = str.length - 1; i >= 0; i--) { 11 | const code = str.charCodeAt(i); 12 | if (code > 0x7f && code <= 0x7ff) s++; 13 | else if (code > 0x7ff && code <= 0xffff) s += 2; 14 | if (code >= 0xdc00 && code <= 0xdfff) i--; 15 | } 16 | return s; 17 | } 18 | 19 | /** 20 | * @param {Array} actual 21 | * @returns {Array} 22 | */ 23 | export function cleanArray(actual) { 24 | const newArray = []; 25 | for (let i = 0; i < actual.length; i++) { 26 | if (actual[i]) { 27 | newArray.push(actual[i]); 28 | } 29 | } 30 | return newArray; 31 | } 32 | 33 | /** 34 | * @param {Object} json 35 | * @returns {Array} 36 | */ 37 | export function param(json) { 38 | if (!json) return ""; 39 | return cleanArray( 40 | Object.keys(json).map((key) => { 41 | if (json[key] === undefined) return ""; 42 | return encodeURIComponent(key) + "=" + encodeURIComponent(json[key]); 43 | }) 44 | ).join("&"); 45 | } 46 | 47 | /** 48 | * @param {string} url 49 | * @returns {Object} 50 | */ 51 | export function param2Obj(url) { 52 | const search = decodeURIComponent(url.split("?")[1]).replace(/\+/g, " "); 53 | if (!search) { 54 | return {}; 55 | } 56 | const obj = {}; 57 | const searchArr = search.split("&"); 58 | searchArr.forEach((v) => { 59 | const index = v.indexOf("="); 60 | if (index !== -1) { 61 | const name = v.substring(0, index); 62 | const val = v.substring(index + 1, v.length); 63 | obj[name] = val; 64 | } 65 | }); 66 | return obj; 67 | } 68 | 69 | /** 70 | * @param {string} val 71 | * @returns {string} 72 | */ 73 | export function html2Text(val) { 74 | const div = document.createElement("div"); 75 | div.innerHTML = val; 76 | return div.textContent || div.innerText; 77 | } 78 | 79 | /** 80 | * Merges two objects, giving the last one precedence 81 | * @param {Object} target 82 | * @param {(Object|Array)} source 83 | * @returns {Object} 84 | */ 85 | export function objectMerge(target, source) { 86 | if (typeof target !== "object") { 87 | target = {}; 88 | } 89 | if (Array.isArray(source)) { 90 | return source.slice(); 91 | } 92 | Object.keys(source).forEach((property) => { 93 | const sourceProperty = source[property]; 94 | if (typeof sourceProperty === "object") { 95 | target[property] = objectMerge(target[property], sourceProperty); 96 | } else { 97 | target[property] = sourceProperty; 98 | } 99 | }); 100 | return target; 101 | } 102 | 103 | /** 104 | * @param {HTMLElement} element 105 | * @param {string} className 106 | */ 107 | export function toggleClass(element, className) { 108 | if (!element || !className) { 109 | return; 110 | } 111 | let classString = element.className; 112 | const nameIndex = classString.indexOf(className); 113 | if (nameIndex === -1) { 114 | classString += "" + className; 115 | } else { 116 | classString = 117 | classString.substr(0, nameIndex) + 118 | classString.substr(nameIndex + className.length); 119 | } 120 | element.className = classString; 121 | } 122 | 123 | /** 124 | * @param {string} type 125 | * @returns {Date} 126 | */ 127 | export function getTime(type) { 128 | if (type === "start") { 129 | return new Date().getTime() - 3600 * 1000 * 24 * 90; 130 | } else { 131 | return new Date(new Date().toDateString()); 132 | } 133 | } 134 | 135 | /** 136 | * @param {Function} func 137 | * @param {number} wait 138 | * @param {boolean} immediate 139 | * @return {*} 140 | */ 141 | export function debounce(func, wait, immediate) { 142 | let timeout, args, context, timestamp, result; 143 | 144 | const later = function () { 145 | // 据上一次触发时间间隔 146 | const last = +new Date() - timestamp; 147 | 148 | // 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait 149 | if (last < wait && last > 0) { 150 | timeout = setTimeout(later, wait - last); 151 | } else { 152 | timeout = null; 153 | // 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用 154 | if (!immediate) { 155 | result = func.apply(context, args); 156 | if (!timeout) context = args = null; 157 | } 158 | } 159 | }; 160 | 161 | return function (...args) { 162 | context = this; 163 | timestamp = +new Date(); 164 | const callNow = immediate && !timeout; 165 | // 如果延时不存在,重新设定延时 166 | if (!timeout) timeout = setTimeout(later, wait); 167 | if (callNow) { 168 | result = func.apply(context, args); 169 | context = args = null; 170 | } 171 | 172 | return result; 173 | }; 174 | } 175 | 176 | /** 177 | * This is just a simple version of deep copy 178 | * Has a lot of edge cases bug 179 | * If you want to use a perfect deep copy, use lodash's _.cloneDeep 180 | * @param {Object} source 181 | * @returns {Object} 182 | */ 183 | export function deepClone(source) { 184 | if (!source && typeof source !== "object") { 185 | throw new Error("error arguments", "deepClone"); 186 | } 187 | const targetObj = source.constructor === Array ? [] : {}; 188 | Object.keys(source).forEach((keys) => { 189 | if (source[keys] && typeof source[keys] === "object") { 190 | targetObj[keys] = deepClone(source[keys]); 191 | } else { 192 | targetObj[keys] = source[keys]; 193 | } 194 | }); 195 | return targetObj; 196 | } 197 | 198 | /** 199 | * @param {Array} arr 200 | * @returns {Array} 201 | */ 202 | export function uniqueArr(arr) { 203 | return Array.from(new Set(arr)); 204 | } 205 | 206 | /** 207 | * @returns {string} 208 | */ 209 | export function createUniqueString() { 210 | const timestamp = +new Date() + ""; 211 | const randomNum = parseInt((1 + Math.random()) * 65536) + ""; 212 | return (+(randomNum + timestamp)).toString(32); 213 | } 214 | 215 | /** 216 | * Check if an element has a class 217 | * @param {HTMLElement} elm 218 | * @param {string} cls 219 | * @returns {boolean} 220 | */ 221 | export function hasClass(ele, cls) { 222 | return !!ele.className.match(new RegExp("(\\s|^)" + cls + "(\\s|$)")); 223 | } 224 | 225 | /** 226 | * Add class to element 227 | * @param {HTMLElement} elm 228 | * @param {string} cls 229 | */ 230 | export function addClass(ele, cls) { 231 | if (!hasClass(ele, cls)) ele.className += " " + cls; 232 | } 233 | 234 | /** 235 | * Remove class from element 236 | * @param {HTMLElement} elm 237 | * @param {string} cls 238 | */ 239 | export function removeClass(ele, cls) { 240 | if (hasClass(ele, cls)) { 241 | const reg = new RegExp("(\\s|^)" + cls + "(\\s|$)"); 242 | ele.className = ele.className.replace(reg, " "); 243 | } 244 | } 245 | 246 | export function makeMap(str, expectsLowerCase) { 247 | const map = Object.create(null); 248 | const list = str.split(","); 249 | for (let i = 0; i < list.length; i++) { 250 | map[list[i]] = true; 251 | } 252 | return expectsLowerCase ? (val) => map[val.toLowerCase()] : (val) => map[val]; 253 | } 254 | 255 | export const exportDefault = "export default "; 256 | 257 | export const beautifierConf = { 258 | html: { 259 | indent_size: "2", 260 | indent_char: " ", 261 | max_preserve_newlines: "-1", 262 | preserve_newlines: false, 263 | keep_array_indentation: false, 264 | break_chained_methods: false, 265 | indent_scripts: "separate", 266 | brace_style: "end-expand", 267 | space_before_conditional: true, 268 | unescape_strings: false, 269 | jslint_happy: false, 270 | end_with_newline: true, 271 | wrap_line_length: "110", 272 | indent_inner_html: true, 273 | comma_first: false, 274 | e4x: true, 275 | indent_empty_lines: true, 276 | }, 277 | js: { 278 | indent_size: "2", 279 | indent_char: " ", 280 | max_preserve_newlines: "-1", 281 | preserve_newlines: false, 282 | keep_array_indentation: false, 283 | break_chained_methods: false, 284 | indent_scripts: "normal", 285 | brace_style: "end-expand", 286 | space_before_conditional: true, 287 | unescape_strings: false, 288 | jslint_happy: true, 289 | end_with_newline: true, 290 | wrap_line_length: "110", 291 | indent_inner_html: true, 292 | comma_first: false, 293 | e4x: true, 294 | indent_empty_lines: true, 295 | }, 296 | }; 297 | 298 | // 首字母大小 299 | export function titleCase(str) { 300 | return str.replace(/( |^)[a-z]/g, (L) => L.toUpperCase()); 301 | } 302 | 303 | // 下划转驼峰 304 | export function camelCase(str) { 305 | return str.replace(/_[a-z]/g, (str1) => str1.substr(-1).toUpperCase()); 306 | } 307 | 308 | export function isNumberStr(str) { 309 | return /^[+-]?(0|([1-9]\d*))(\.\d+)?$/g.test(str); 310 | } 311 | 312 | export function obj2Param(obj) { 313 | return Object.keys(obj) 314 | .filter((i) => (obj[i] === false ? String(obj[i]) : obj[i])) 315 | .map((n) => 316 | Array.isArray(obj[n]) ? arrayParams(n, obj[n]) : [n, obj[n]].join("=") 317 | ) 318 | .join("&"); 319 | } 320 | 321 | // 生成19位唯一数字标识符,来自chatgpt 322 | export const randomNumber = () => { 323 | var timestamp = Date.now().toString(); // 获取当前时间戳 324 | var random = Math.floor(Math.random() * 9000000000000000) + 1000000000000000; // 生成15位随机数 325 | var identifier = timestamp + random.toString(); // 结合时间戳和随机数 326 | if (identifier.length > 19) { 327 | identifier = identifier.substr(0, 19); // 如果标识符超过19位,则截取前19位 328 | } 329 | if (identifier.charAt(0) === "0") { 330 | identifier = "1" + identifier.substr(1); // 如果开头是0,则将第一个字符替换为1 331 | } 332 | return identifier; 333 | }; 334 | 335 | //logicflow-获取节点输出几条线。graph是json数组,node是节点数据 336 | export const getNodeOutputCount = (graph, nodeData) => { 337 | // 找到所有节点 338 | const nodes = graph.nodes; 339 | // 找到所有边 340 | const edges = graph.edges; 341 | // 初始化节点输出线条数量 342 | let count = 0; 343 | // 遍历所有节点 344 | for (const node of nodes) { 345 | // 判断节点类型是否为 346 | if (node.id === nodeData.id) { 347 | // 遍历所有边,查找与该节点相关的边 348 | for (const edge of edges) { 349 | if (edge.sourceNodeId === node.id) { 350 | // 如果找到了与该节点相连的边,增加计数 351 | count++; 352 | } 353 | } 354 | } 355 | } 356 | return count; // 节点输出线条数量 357 | }; 358 | 359 | //logicflow-获取节点输入几条线。graph是json数组,node是节点数据 360 | export const getNodeInputCount = (graph, nodeData) => { 361 | // 找到所有节点 362 | const nodes = graph.nodes; 363 | // 找到所有边 364 | const edges = graph.edges; 365 | // 初始化节点输出线条数量 366 | let count = 0; 367 | // 遍历所有节点 368 | for (const node of nodes) { 369 | // 判断节点类型是否为 370 | if (node.id === nodeData.id) { 371 | // 遍历所有边,查找与该节点相关的边 372 | for (const edge of edges) { 373 | if (edge.targetNodeId === node.id) { 374 | // 如果找到了与该节点相连的边,增加计数 375 | count++; 376 | } 377 | } 378 | } 379 | } 380 | return count; // 节点输出线条数量 381 | }; 382 | 383 | //判断数组是否为空或者包含空。true为空 384 | export function arrHasEmpty(arr) { 385 | let arrTemp = arr.filter(Boolean); 386 | if (arr.length == 0) { 387 | return true; 388 | } else { 389 | if (arr.length != arrTemp.length) { 390 | return true; 391 | } else { 392 | return false; 393 | } 394 | } 395 | } 396 | 397 | //判断数组是否有值重复 398 | export const arrHasDuplicate = (arr) => { 399 | return new Set(arr).size !== arr.length; 400 | }; 401 | 402 | //服务编排-校验边 403 | export const validEdgeType = (lf) => { 404 | let nodeTypes = []; 405 | lf.getGraphData().nodes.forEach((i) => { 406 | nodeTypes.push(i.type); 407 | }); 408 | //校验决策条件是否为空。返回true校验通过false校验不通过 409 | if (nodeTypes.includes("decision")) { 410 | let arr = []; 411 | lf.getGraphData().edges.forEach((i) => { 412 | if (i.properties.type == "decision") { 413 | arr.push(i.properties.edgeType); 414 | } 415 | }); 416 | return !arrHasEmpty(arr); 417 | } else { 418 | return true; 419 | } 420 | }; 421 | 422 | //通过源节点获取所有输出的边 423 | export const getEdgesFormSourceNodeId = (graphData, sourceNodeId) => { 424 | let edges = []; 425 | graphData.edges.forEach((i) => { 426 | if (i.sourceNodeId == sourceNodeId) { 427 | edges.push(i); 428 | } 429 | }); 430 | return edges; 431 | }; 432 | 433 | //复制 434 | export const copyFunc = (data) => { 435 | if (data) { 436 | let oInput = document.createElement("input"); 437 | oInput.value = data; 438 | document.body.appendChild(oInput); 439 | oInput.select(); 440 | ElMessage.success("复制成功"); 441 | document.execCommand("Copy"); 442 | document.body.removeChild(oInput); 443 | } 444 | }; 445 | 446 | /** 447 | * @description:根据数组将value变成中文 448 | * @param {*} value value值 449 | * @param {*} arr 枚举数据 450 | * @param {*} typeValue 英文字段 451 | * @param {*} typeLabel 中文字段 452 | * @return {*} 453 | */ 454 | export const getLabelByValue = (value, arr, typeValue, typeLabel) => { 455 | let label = ""; 456 | arr.forEach((i) => { 457 | if (i[typeValue] == value) { 458 | label = i[typeLabel]; 459 | } 460 | }); 461 | return label; 462 | }; 463 | 464 | /** 465 | * @description:根据数组通过value拿type 466 | * @param {*} value value值 467 | * @param {*} arr 枚举数据 468 | * @param {*} typeValue 英文字段 469 | * @return {*} 470 | */ 471 | export const getTypeByValue = (value, arr, typeValue) => { 472 | let type = ""; 473 | arr.forEach((i) => { 474 | if (i[typeValue] == value) { 475 | type = i["type"]; 476 | } 477 | }); 478 | return type; 479 | }; 480 | -------------------------------------------------------------------------------- /src/utils/options.ts: -------------------------------------------------------------------------------- 1 | //所有枚举 2 | 3 | //服务编排-图元类型 4 | export const pixelOption = [ 5 | { 6 | value: 'start', 7 | label: '开始', 8 | }, 9 | { 10 | value: 'assignment', 11 | label: '节点1-1', 12 | }, 13 | { 14 | value: 'decision', 15 | label: '节点1-2', 16 | }, 17 | { 18 | value: 'startParallel', 19 | label: '节点1-3', 20 | }, 21 | { 22 | value: 'endParallel', 23 | label: '节点1-4', 24 | }, 25 | { 26 | value: 'machineLearning', 27 | label: '节点2-1', 28 | }, 29 | { 30 | value: 'deepLearning', 31 | label: '节点2-2', 32 | }, 33 | { 34 | value: 'myBezier', 35 | label: '连线', 36 | }, 37 | ]; -------------------------------------------------------------------------------- /src/views/design/LFComponents/Control.vue: -------------------------------------------------------------------------------- 1 | 32 | 90 | 112 | -------------------------------------------------------------------------------- /src/views/design/LFComponents/NodePanel.vue: -------------------------------------------------------------------------------- 1 | 33 | 190 | 276 | -------------------------------------------------------------------------------- /src/views/design/LFComponents/top.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 62 | 63 | 78 | -------------------------------------------------------------------------------- /src/views/design/LFComponents/view-json.vue: -------------------------------------------------------------------------------- 1 | 30 | 58 | 59 | -------------------------------------------------------------------------------- /src/views/design/PropertySetting/PropertyDialog.vue: -------------------------------------------------------------------------------- 1 | 28 | 68 | 69 | -------------------------------------------------------------------------------- /src/views/design/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 473 | 567 | -------------------------------------------------------------------------------- /src/views/design/registerEdge/myBezier.vue: -------------------------------------------------------------------------------- 1 | 16 | 64 | 65 | -------------------------------------------------------------------------------- /src/views/design/registerEdge/registerBezier.ts: -------------------------------------------------------------------------------- 1 | // 贝塞尔曲线 2 | import { BezierEdge, BezierEdgeModel } from '@logicflow/core'; 3 | import { randomNumber } from '@/utils/index'; 4 | class BezierModel extends BezierEdgeModel { 5 | createId() { 6 | return randomNumber(); 7 | } 8 | constructor(data, graphModel) { 9 | super(data, graphModel); 10 | this.menu = [ 11 | { 12 | text: '配置', 13 | callback: (res) => { 14 | graphModel.eventCenter.emit('edge:app-config', res); //发出事件 15 | }, 16 | }, 17 | { 18 | text: '删除', 19 | callback(res) { 20 | graphModel.deleteEdgeById(res.id); 21 | }, 22 | }, 23 | ]; 24 | } 25 | } 26 | 27 | export default { 28 | type: 'myBezier', 29 | view: BezierEdge, 30 | model: BezierModel, 31 | }; 32 | -------------------------------------------------------------------------------- /src/views/design/registerNode/assignment/assignment.ts: -------------------------------------------------------------------------------- 1 | import { createApp, h } from 'vue'; 2 | import assignment from './assignment.vue'; 3 | import { randomNumber } from '@/utils/index'; 4 | let nodeNameZh = ''; 5 | 6 | export default function registerConnect(lf) { 7 | lf.register('assignment', ({ HtmlNode, HtmlNodeModel }) => { 8 | class RuleNode extends HtmlNode { 9 | setHtml(rootEl) { 10 | const { model } = this.props; 11 | const el = document.createElement('div'); 12 | rootEl.innerHTML = ''; 13 | rootEl.appendChild(el); 14 | 15 | // Vue 3 使用 createApp 来创建应用实例 16 | const app = createApp({ 17 | render: () => 18 | h(assignment, { 19 | properties: model.properties, 20 | }), 21 | }); 22 | 23 | // 挂载 Vue 应用到元素上 24 | app.mount(el); 25 | } 26 | } 27 | 28 | class RuleModel extends HtmlNodeModel { 29 | createId() { 30 | return randomNumber(); //id用随机数数字 31 | } 32 | constructor(data, graphModel) { 33 | super(data, graphModel); 34 | // 右键菜单自由配置,也可以通过边的properties或者其他属性条件更换不同菜单 35 | this.menu = [ 36 | { 37 | text: '删除', 38 | callback(node) { 39 | lf.deleteNode(node.id); 40 | }, 41 | }, 42 | { 43 | text: '复制', 44 | callback(node) { 45 | lf.cloneNode(node.id); 46 | }, 47 | }, 48 | ]; 49 | } 50 | getDefaultAnchor() { 51 | const { id, x, y, width, height } = this; 52 | const anchors = []; 53 | anchors.push({ 54 | x, 55 | y: y - height / 2, 56 | id: `${id}_incomming`, 57 | type: 'incomming', 58 | }); 59 | anchors.push({ 60 | x, 61 | y: y + height / 2, 62 | id: `${id}_outgoing`, 63 | type: 'outgoing', 64 | }); 65 | return anchors; 66 | } 67 | initNodeData(data) { 68 | super.initNodeData(data); 69 | const width = 140; 70 | const height = 40; 71 | this.width = width; 72 | this.height = height; 73 | this.radius = 50; 74 | this.targetRules = [ 75 | // { 76 | // message: `【${nodeNameZh}】只允许一个输入`, 77 | // validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => { 78 | // const edges = this.graphModel.getNodeIncomingEdge(targetNode.id); 79 | // if (edges.length >= 1) { 80 | // return false; 81 | // } else { 82 | // return true; 83 | // } 84 | // }, 85 | // }, 86 | // { 87 | // message: '输入不允许连接输入', 88 | // validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => { 89 | // return sourceAnchor.type === 'outgoing'; 90 | // }, 91 | // }, 92 | ]; 93 | this.sourceRules = [ 94 | // { 95 | // message: `【${nodeNameZh}】只允许1个输出`, 96 | // validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => { 97 | // const edges = this.graphModel.getNodeOutgoingEdge(sourceNode.id); 98 | // if (edges.length >= 1) return false; 99 | // return true; 100 | // }, 101 | // }, 102 | // { 103 | // message: '输出不允许连接输出', 104 | // validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => { 105 | // return targetAnchor.type === 'incomming'; 106 | // }, 107 | // }, 108 | ]; 109 | } 110 | } 111 | return { 112 | view: RuleNode, 113 | model: RuleModel, 114 | }; 115 | }); 116 | } 117 | -------------------------------------------------------------------------------- /src/views/design/registerNode/assignment/assignment.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 30 | 31 | 38 | -------------------------------------------------------------------------------- /src/views/design/registerNode/assignment/assignmentProperty.vue: -------------------------------------------------------------------------------- 1 | 17 | 88 | 89 | -------------------------------------------------------------------------------- /src/views/design/registerNode/decision/decision.ts: -------------------------------------------------------------------------------- 1 | import { createApp, h } from 'vue'; 2 | import decision from './decision.vue'; 3 | import { randomNumber } from '@/utils/index'; 4 | let nodeNameZh = ''; 5 | 6 | export default function registerConnect(lf) { 7 | lf.register('decision', ({ HtmlNode, HtmlNodeModel }) => { 8 | class RuleNode extends HtmlNode { 9 | setHtml(rootEl) { 10 | const { model } = this.props; 11 | const el = document.createElement('div'); 12 | rootEl.innerHTML = ''; 13 | rootEl.appendChild(el); 14 | 15 | // Vue 3 使用 createApp 来创建应用实例 16 | const app = createApp({ 17 | render: () => 18 | h(decision, { 19 | properties: model.properties, 20 | }), 21 | }); 22 | 23 | // 挂载 Vue 应用到元素上 24 | app.mount(el); 25 | } 26 | } 27 | 28 | class RuleModel extends HtmlNodeModel { 29 | createId() { 30 | return randomNumber(); //id用随机数数字 31 | } 32 | constructor(data, graphModel) { 33 | super(data, graphModel); 34 | // 右键菜单自由配置,也可以通过边的properties或者其他属性条件更换不同菜单 35 | this.menu = [ 36 | { 37 | text: '删除', 38 | callback(node) { 39 | lf.deleteNode(node.id); 40 | }, 41 | }, 42 | { 43 | text: '复制', 44 | callback(node) { 45 | lf.cloneNode(node.id); 46 | }, 47 | }, 48 | ]; 49 | } 50 | getDefaultAnchor() { 51 | const { id, x, y, width, height } = this; 52 | const anchors = []; 53 | anchors.push({ 54 | x, 55 | y: y - height / 2, 56 | id: `${id}_incomming`, 57 | type: 'incomming', 58 | }); 59 | anchors.push({ 60 | x, 61 | y: y + height / 2, 62 | id: `${id}_outgoing`, 63 | type: 'outgoing', 64 | }); 65 | return anchors; 66 | } 67 | initNodeData(data) { 68 | super.initNodeData(data); 69 | const width = 140; 70 | const height = 40; 71 | this.width = width; 72 | this.height = height; 73 | this.radius = 50; 74 | this.targetRules = [ 75 | // { 76 | // message: `【${nodeNameZh}】只允许一个输入`, 77 | // validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => { 78 | // const edges = this.graphModel.getNodeIncomingEdge(targetNode.id); 79 | // if (edges.length >= 1) { 80 | // return false; 81 | // } else { 82 | // return true; 83 | // } 84 | // }, 85 | // }, 86 | // { 87 | // message: '输入不允许连接输入', 88 | // validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => { 89 | // return sourceAnchor.type === 'outgoing'; 90 | // }, 91 | // }, 92 | ]; 93 | this.sourceRules = [ 94 | // { 95 | // message: '输出不允许连接输出', 96 | // validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => { 97 | // return targetAnchor.type === 'incomming'; 98 | // }, 99 | // }, 100 | ]; 101 | } 102 | } 103 | return { 104 | view: RuleNode, 105 | model: RuleModel, 106 | }; 107 | }); 108 | } 109 | -------------------------------------------------------------------------------- /src/views/design/registerNode/decision/decision.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 30 | 31 | 37 | -------------------------------------------------------------------------------- /src/views/design/registerNode/decision/decisionProperty.vue: -------------------------------------------------------------------------------- 1 | 17 | 88 | 89 | -------------------------------------------------------------------------------- /src/views/design/registerNode/deepLearning/deepLearning.ts: -------------------------------------------------------------------------------- 1 | import { createApp, h } from 'vue'; 2 | import deepLearning from './deepLearning.vue'; 3 | import { randomNumber } from '@/utils/index'; 4 | let nodeNameZh = ''; 5 | 6 | export default function registerConnect(lf) { 7 | lf.register('deepLearning', ({ HtmlNode, HtmlNodeModel }) => { 8 | class RuleNode extends HtmlNode { 9 | setHtml(rootEl) { 10 | const { model } = this.props; 11 | const el = document.createElement('div'); 12 | rootEl.innerHTML = ''; 13 | rootEl.appendChild(el); 14 | 15 | // Vue 3 使用 createApp 来创建应用实例 16 | const app = createApp({ 17 | render: () => 18 | h(deepLearning, { 19 | properties: model.properties, 20 | }), 21 | }); 22 | 23 | // 挂载 Vue 应用到元素上 24 | app.mount(el); 25 | } 26 | } 27 | class RuleModel extends HtmlNodeModel { 28 | createId() { 29 | return randomNumber(); //id用随机数数字 30 | } 31 | constructor(data, graphModel) { 32 | super(data, graphModel); 33 | // 右键菜单自由配置,也可以通过边的properties或者其他属性条件更换不同菜单 34 | this.menu = [ 35 | { 36 | text: '删除', 37 | callback(node) { 38 | lf.deleteNode(node.id); 39 | }, 40 | }, 41 | { 42 | text: '复制', 43 | callback(node) { 44 | lf.cloneNode(node.id); 45 | }, 46 | }, 47 | ]; 48 | } 49 | getDefaultAnchor() { 50 | const { id, x, y, width, height } = this; 51 | const anchors = []; 52 | anchors.push({ 53 | x, 54 | y: y - height / 2, 55 | id: `${id}_incomming`, 56 | type: 'incomming', 57 | }); 58 | anchors.push({ 59 | x, 60 | y: y + height / 2, 61 | id: `${id}_outgoing`, 62 | type: 'outgoing', 63 | }); 64 | return anchors; 65 | } 66 | initNodeData(data) { 67 | super.initNodeData(data); 68 | const width = 140; 69 | const height = 40; 70 | this.width = width; 71 | this.height = height; 72 | this.radius = 50; 73 | this.targetRules = [ 74 | // { 75 | // message: `【${nodeNameZh}】只允许一个输入`, 76 | // validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => { 77 | // const edges = this.graphModel.getNodeIncomingEdge(targetNode.id); 78 | // if (edges.length >= 1) { 79 | // return false; 80 | // } else { 81 | // return true; 82 | // } 83 | // }, 84 | // }, 85 | // { 86 | // message: '输入不允许连接输入', 87 | // validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => { 88 | // return sourceAnchor.type === 'outgoing'; 89 | // }, 90 | // }, 91 | ]; 92 | 93 | this.sourceRules = [ 94 | // { 95 | // message: `【${nodeNameZh}】只允许1个输出`, 96 | // validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => { 97 | // const edges = this.graphModel.getNodeOutgoingEdge(sourceNode.id); 98 | // if (edges.length >= 1) return false; 99 | // return true; 100 | // }, 101 | // }, 102 | // { 103 | // message: '输出不允许连接输出', 104 | // validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => { 105 | // return targetAnchor.type === 'incomming'; 106 | // }, 107 | // }, 108 | ]; 109 | } 110 | } 111 | return { 112 | view: RuleNode, 113 | model: RuleModel, 114 | }; 115 | }); 116 | } 117 | -------------------------------------------------------------------------------- /src/views/design/registerNode/deepLearning/deepLearning.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 30 | 31 | 36 | -------------------------------------------------------------------------------- /src/views/design/registerNode/deepLearning/deepLearningProperty.vue: -------------------------------------------------------------------------------- 1 | 17 | 88 | 89 | -------------------------------------------------------------------------------- /src/views/design/registerNode/endParallel/endParallel.ts: -------------------------------------------------------------------------------- 1 | import { createApp, h } from 'vue'; 2 | import endParallel from './endParallel.vue'; 3 | import { randomNumber } from '@/utils/index'; 4 | let nodeNameZh = ''; 5 | 6 | export default function registerConnect(lf) { 7 | lf.register('endParallel', ({ HtmlNode, HtmlNodeModel }) => { 8 | class RuleNode extends HtmlNode { 9 | setHtml(rootEl) { 10 | const { model } = this.props; 11 | const el = document.createElement('div'); 12 | rootEl.innerHTML = ''; 13 | rootEl.appendChild(el); 14 | 15 | // Vue 3 使用 createApp 来创建应用实例 16 | const app = createApp({ 17 | render: () => 18 | h(endParallel, { 19 | properties: model.properties, 20 | }), 21 | }); 22 | 23 | // 挂载 Vue 应用到元素上 24 | app.mount(el); 25 | } 26 | } 27 | class RuleModel extends HtmlNodeModel { 28 | createId() { 29 | return randomNumber(); //id用随机数数字 30 | } 31 | constructor(data, graphModel) { 32 | super(data, graphModel); 33 | // 右键菜单自由配置,也可以通过边的properties或者其他属性条件更换不同菜单 34 | this.menu = [ 35 | { 36 | text: '删除', 37 | callback(node) { 38 | lf.deleteNode(node.id); 39 | }, 40 | }, 41 | { 42 | text: '复制', 43 | callback(node) { 44 | lf.cloneNode(node.id); 45 | }, 46 | }, 47 | ]; 48 | } 49 | getDefaultAnchor() { 50 | const { id, x, y, width, height } = this; 51 | const anchors = []; 52 | anchors.push({ 53 | x, 54 | y: y - height / 2, 55 | id: `${id}_incomming`, 56 | type: 'incomming', 57 | }); 58 | anchors.push({ 59 | x, 60 | y: y + height / 2, 61 | id: `${id}_outgoing`, 62 | type: 'outgoing', 63 | }); 64 | return anchors; 65 | } 66 | initNodeData(data) { 67 | super.initNodeData(data); 68 | const width = 140; 69 | const height = 40; 70 | this.width = width; 71 | this.height = height; 72 | this.radius = 50; 73 | this.targetRules = [ 74 | // { 75 | // message: '输入不允许连接输入', 76 | // validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => { 77 | // return sourceAnchor.type === 'outgoing'; 78 | // }, 79 | // }, 80 | ]; 81 | this.sourceRules = [ 82 | // { 83 | // message: `【${nodeNameZh}】只允许1个输出`, 84 | // validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => { 85 | // const edges = this.graphModel.getNodeOutgoingEdge(sourceNode.id); 86 | // if (edges.length >= 1) return false; 87 | // return true; 88 | // }, 89 | // }, 90 | // { 91 | // message: '输出不允许连接输出', 92 | // validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => { 93 | // return targetAnchor.type === 'incomming'; 94 | // }, 95 | // }, 96 | ]; 97 | } 98 | } 99 | return { 100 | view: RuleNode, 101 | model: RuleModel, 102 | }; 103 | }); 104 | } 105 | -------------------------------------------------------------------------------- /src/views/design/registerNode/endParallel/endParallel.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 30 | 31 | 36 | -------------------------------------------------------------------------------- /src/views/design/registerNode/endParallel/endParallelProperty.vue: -------------------------------------------------------------------------------- 1 | 17 | 88 | 89 | -------------------------------------------------------------------------------- /src/views/design/registerNode/machineLearning/machineLearning.ts: -------------------------------------------------------------------------------- 1 | import { createApp, h } from 'vue'; 2 | import machineLearning from './machineLearning.vue'; 3 | import { randomNumber } from '@/utils/index'; 4 | let nodeNameZh = ''; 5 | 6 | export default function registerConnect(lf) { 7 | lf.register('machineLearning', ({ HtmlNode, HtmlNodeModel }) => { 8 | class RuleNode extends HtmlNode { 9 | setHtml(rootEl) { 10 | const { model } = this.props; 11 | const el = document.createElement('div'); 12 | rootEl.innerHTML = ''; 13 | rootEl.appendChild(el); 14 | 15 | // Vue 3 使用 createApp 来创建应用实例 16 | const app = createApp({ 17 | render: () => 18 | h(machineLearning, { 19 | properties: model.properties, 20 | }), 21 | }); 22 | 23 | // 挂载 Vue 应用到元素上 24 | app.mount(el); 25 | } 26 | } 27 | class RuleModel extends HtmlNodeModel { 28 | createId() { 29 | return randomNumber(); //id用随机数数字 30 | } 31 | constructor(data, graphModel) { 32 | super(data, graphModel); 33 | // 右键菜单自由配置,也可以通过边的properties或者其他属性条件更换不同菜单 34 | this.menu = [ 35 | { 36 | text: '删除', 37 | callback(node) { 38 | lf.deleteNode(node.id); 39 | }, 40 | }, 41 | { 42 | text: '复制', 43 | callback(node) { 44 | lf.cloneNode(node.id); 45 | }, 46 | }, 47 | ]; 48 | } 49 | getDefaultAnchor() { 50 | const { id, x, y, width, height } = this; 51 | const anchors = []; 52 | anchors.push({ 53 | x, 54 | y: y - height / 2, 55 | id: `${id}_incomming`, 56 | type: 'incomming', 57 | }); 58 | anchors.push({ 59 | x, 60 | y: y + height / 2, 61 | id: `${id}_outgoing`, 62 | type: 'outgoing', 63 | }); 64 | return anchors; 65 | } 66 | initNodeData(data) { 67 | super.initNodeData(data); 68 | const width = 140; 69 | const height = 40; 70 | this.width = width; 71 | this.height = height; 72 | this.radius = 50; 73 | this.targetRules = [ 74 | // { 75 | // message: `【${nodeNameZh}】只允许一个输入`, 76 | // validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => { 77 | // const edges = this.graphModel.getNodeIncomingEdge(targetNode.id); 78 | // if (edges.length >= 1) { 79 | // return false; 80 | // } else { 81 | // return true; 82 | // } 83 | // }, 84 | // }, 85 | // { 86 | // message: '输入不允许连接输入', 87 | // validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => { 88 | // return sourceAnchor.type === 'outgoing'; 89 | // }, 90 | // }, 91 | ]; 92 | 93 | this.sourceRules = [ 94 | // { 95 | // message: `【${nodeNameZh}】只允许1个输出`, 96 | // validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => { 97 | // const edges = this.graphModel.getNodeOutgoingEdge(sourceNode.id); 98 | // if (edges.length >= 1) return false; 99 | // return true; 100 | // }, 101 | // }, 102 | // { 103 | // message: '输出不允许连接输出', 104 | // validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => { 105 | // return targetAnchor.type === 'incomming'; 106 | // }, 107 | // }, 108 | ]; 109 | } 110 | } 111 | return { 112 | view: RuleNode, 113 | model: RuleModel, 114 | }; 115 | }); 116 | } 117 | -------------------------------------------------------------------------------- /src/views/design/registerNode/machineLearning/machineLearning.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 30 | 31 | 36 | -------------------------------------------------------------------------------- /src/views/design/registerNode/machineLearning/machineLearningProperty.vue: -------------------------------------------------------------------------------- 1 | 17 | 88 | 89 | -------------------------------------------------------------------------------- /src/views/design/registerNode/start/start.ts: -------------------------------------------------------------------------------- 1 | import { createApp, h } from 'vue'; 2 | import start from './start.vue'; 3 | import { randomNumber } from '@/utils/index'; 4 | 5 | export default function registerConnect(lf) { 6 | lf.register('start', ({ HtmlNode, HtmlNodeModel }) => { 7 | class RuleNode extends HtmlNode { 8 | setHtml(rootEl) { 9 | const { model } = this.props; 10 | const el = document.createElement('div'); 11 | rootEl.innerHTML = ''; 12 | rootEl.appendChild(el); 13 | 14 | // Vue 3 使用 createApp 来创建应用实例 15 | const app = createApp({ 16 | render: () => 17 | h(start, { 18 | properties: model.properties, 19 | }), 20 | }); 21 | 22 | // 挂载 Vue 应用到元素上 23 | app.mount(el); 24 | } 25 | } 26 | class RuleModel extends HtmlNodeModel { 27 | createId() { 28 | return randomNumber(); 29 | } 30 | constructor(data, graphModel) { 31 | super(data, graphModel); 32 | this.menu = []; 33 | } 34 | getDefaultAnchor() { 35 | const { id, x, y, width, height } = this; 36 | const anchors = []; 37 | anchors.push({ 38 | x, 39 | y: y + height / 2, 40 | id: `${id}_outgoing`, 41 | type: 'outgoing', 42 | }); 43 | return anchors; 44 | } 45 | initNodeData(data) { 46 | super.initNodeData(data); 47 | const width = 140; 48 | const height = 40; 49 | this.width = width; 50 | this.height = height; 51 | this.radius = 50; 52 | } 53 | } 54 | return { 55 | view: RuleNode, 56 | model: RuleModel, 57 | }; 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /src/views/design/registerNode/start/start.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 30 | 31 | 37 | -------------------------------------------------------------------------------- /src/views/design/registerNode/start/startProperty.vue: -------------------------------------------------------------------------------- 1 | 17 | 88 | 89 | -------------------------------------------------------------------------------- /src/views/design/registerNode/startParallel/startParallel.ts: -------------------------------------------------------------------------------- 1 | import { createApp, h } from 'vue'; 2 | import startParallel from './startParallel.vue'; 3 | import { randomNumber } from '@/utils/index'; 4 | let nodeNameZh = ''; 5 | 6 | export default function registerConnect(lf) { 7 | lf.register('startParallel', ({ HtmlNode, HtmlNodeModel }) => { 8 | class myNode extends HtmlNode { 9 | setHtml(rootEl) { 10 | const { model } = this.props; 11 | const el = document.createElement('div'); 12 | rootEl.innerHTML = ''; 13 | rootEl.appendChild(el); 14 | 15 | // Vue 3 使用 createApp 来创建应用实例 16 | const app = createApp({ 17 | render: () => 18 | h(startParallel, { 19 | properties: model.properties, 20 | }), 21 | }); 22 | 23 | // 挂载 Vue 应用到元素上 24 | app.mount(el); 25 | } 26 | } 27 | class myModel extends HtmlNodeModel { 28 | createId() { 29 | return randomNumber(); //id用随机数数字 30 | } 31 | constructor(data, graphModel) { 32 | super(data, graphModel); 33 | this.menu = [ 34 | { 35 | text: '删除', 36 | callback(node) { 37 | lf.deleteNode(node.id); 38 | }, 39 | }, 40 | { 41 | text: '复制', 42 | callback(node) { 43 | lf.cloneNode(node.id); 44 | }, 45 | }, 46 | ]; 47 | } 48 | getDefaultAnchor() { 49 | const { id, x, y, width, height } = this; 50 | const anchors = []; 51 | anchors.push({ 52 | x, 53 | y: y - height / 2, 54 | id: `${id}_incomming`, 55 | type: 'incomming', 56 | }); 57 | anchors.push({ 58 | x, 59 | y: y + height / 2, 60 | id: `${id}_outgoing`, 61 | type: 'outgoing', 62 | }); 63 | return anchors; 64 | } 65 | initNodeData(data) { 66 | super.initNodeData(data); 67 | const width = 140; 68 | const height = 40; 69 | this.width = width; 70 | this.height = height; 71 | this.radius = 50; 72 | this.targetRules = [ 73 | // { 74 | // message: `【${nodeNameZh}】只允许一个输入`, 75 | // validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => { 76 | // const edges = this.graphModel.getNodeIncomingEdge(targetNode.id); 77 | // if (edges.length >= 1) { 78 | // return false; 79 | // } else { 80 | // return true; 81 | // } 82 | // }, 83 | // }, 84 | // { 85 | // message: '输入不允许连接输入', 86 | // validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => { 87 | // return sourceAnchor.type === 'outgoing'; 88 | // }, 89 | // }, 90 | ]; 91 | this.sourceRules = [ 92 | // { 93 | // message: '输出不允许连接输出', 94 | // validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => { 95 | // return targetAnchor.type === 'incomming'; 96 | // }, 97 | // }, 98 | ]; 99 | } 100 | } 101 | return { 102 | view: myNode, 103 | model: myModel, 104 | }; 105 | }); 106 | } 107 | -------------------------------------------------------------------------------- /src/views/design/registerNode/startParallel/startParallel.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 30 | 31 | 36 | -------------------------------------------------------------------------------- /src/views/design/registerNode/startParallel/startParallelProperty.vue: -------------------------------------------------------------------------------- 1 | 17 | 88 | 89 | -------------------------------------------------------------------------------- /src/views/design/template.vue: -------------------------------------------------------------------------------- 1 | 4 | 9 | 11 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | //修复找不到.vue类型声明 4 | declare module '*.vue' { 5 | import type { DefineComponent } from 'vue'; 6 | const vueComponent: DefineComponent<{}, {}, any>; 7 | export default vueComponent; 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "preserve", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true 9 | }, 10 | "include": ["vite.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import vue from '@vitejs/plugin-vue'; 3 | import path from 'path'; 4 | import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [ 9 | vue(), 10 | createSvgIconsPlugin({ 11 | iconDirs: [path.resolve(process.cwd(), 'src/assets/icons/svg')], 12 | symbolId: 'icon-[dir]-[name]', 13 | }), 14 | ], 15 | resolve: { 16 | alias: { 17 | '@': path.resolve(__dirname, './src'), 18 | }, 19 | extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'], 20 | }, 21 | // 设置scss的api类型为modern-compiler 22 | css: { 23 | preprocessorOptions: { 24 | scss: { 25 | api: 'modern-compiler' 26 | } 27 | } 28 | }, 29 | }); 30 | --------------------------------------------------------------------------------