From f9ea0366f6a8fe6e091fce63ddc5e1cb8b5fd1d6 Mon Sep 17 00:00:00 2001 From: boyongjiong Date: Fri, 31 May 2024 10:41:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A7=A3=E5=86=B3=20git=20config=20cor?= =?UTF-8?q?e.ignorecase=20=E4=B8=BA=20true=20=E5=AF=BC=E8=87=B4=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E6=96=87=E4=BB=B6=E5=A4=B9=E5=90=8D=E5=A4=A7=E5=B0=8F?= =?UTF-8?q?=E5=86=99=E4=B8=8D=E7=94=9F=E6=95=88=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重命名 BPMN -> bpmn; Control -> control - 命名规范:文件夹名小写(name or name1-name2-name3);.js or .ts 小驼峰;.tsx or .jsx 大驼峰 - 新增 Vue3-memory-leak demo 用于定位内存泄漏的问题,后续基于此 demo 增加 LogicFlow destroy 的方法 - 优化 properties 类型定义为 Record,避免 unknow 引起的问题,后续再优化 - 解决 graph demo 中发现 BezierEdge 初始化 path 为空的问题,可以看 -> BezierEdgeModel.ts 的改动 --- .eslintignore | 2 + DEVELOPER.md | 129 +++++++ .../src/pages/graph/index.tsx | 30 +- .../src/pages/graph/nodes/ReactNode.tsx | 63 ++++ .../src/pages/graph/nodes/index.ts | 4 +- .../src/pages/extensions/bpmn/bpmn.json | 255 +++++++++++++ .../src/pages/extensions/bpmn/diagram.xml | 92 +++++ .../src/pages/extensions/bpmn/index.less | 48 +++ .../src/pages/extensions/bpmn/index.tsx | 355 ++++++++++++++++++ .../src/pages/extensions/bpmn/svgIcons.ts | 23 ++ .../src/pages/extensions/bpmn/tips.ts | 85 +++++ .../src/pages/extensions/bpmn/util.ts | 14 + .../src/pages/extensions/control/index.less | 5 + .../src/pages/extensions/control/index.tsx | 132 +++++++ .../src/pages/extensions/menu/index.less | 5 + .../src/pages/extensions/menu/index.tsx | 125 ++++++ .../src/pages/graph/index.tsx | 5 +- .../src/pages/nodes/custom/ellipse/index.less | 5 + .../src/pages/nodes/custom/ellipse/index.tsx | 334 ++++++++++++++++ .../src/pages/nodes/custom/html/data.ts | 17 + .../src/pages/nodes/custom/html/index.less | 5 + .../src/pages/nodes/custom/html/index.tsx | 48 +++ .../src/pages/nodes/custom/html/style.css | 43 +++ .../src/pages/nodes/custom/icon/index.less | 5 + .../src/pages/nodes/custom/icon/index.tsx | 94 +++++ .../src/pages/nodes/custom/image/Cloud.tsx | 14 + .../src/pages/nodes/custom/image/index.less | 5 + .../src/pages/nodes/custom/image/index.tsx | 91 +++++ .../src/pages/nodes/custom/rect/index.less | 5 + .../src/pages/nodes/custom/rect/index.tsx | 325 ++++++++++++++++ .../src/pages/nodes/custom/theme/index.less | 5 + .../src/pages/nodes/custom/theme/index.tsx | 120 ++++++ .../src/pages/nodes/custom/theme/theme.ts | 91 +++++ examples/vue3-memory-leak/.gitignore | 24 ++ examples/vue3-memory-leak/README.md | 11 + examples/vue3-memory-leak/auto-imports.d.ts | 4 + examples/vue3-memory-leak/components.d.ts | 19 + examples/vue3-memory-leak/index.html | 13 + examples/vue3-memory-leak/package.json | 22 ++ examples/vue3-memory-leak/public/favicon.ico | Bin 0 -> 4286 bytes .../vue3-memory-leak/public/images/delay.svg | 1 + .../vue3-memory-leak/public/images/fetch.svg | 1 + .../public/images/function.svg | 1 + .../vue3-memory-leak/public/images/start.svg | 1 + .../vue3-memory-leak/public/images/swap.svg | 1 + .../vue3-memory-leak/public/images/switch.svg | 1 + examples/vue3-memory-leak/src/App.vue | 9 + examples/vue3-memory-leak/src/assets/logo.png | Bin 0 -> 6849 bytes .../src/components/FlowChart.vue | 316 ++++++++++++++++ .../src/components/engine/index.js | 4 + .../vue3-memory-leak/src/components/index.vue | 25 ++ .../src/components/node-red/FlowLink.js | 17 + .../src/components/node-red/index.js | 41 ++ .../src/components/node-red/nodes/BaseNode.js | 185 +++++++++ .../components/node-red/nodes/DelayNode.js | 28 ++ .../components/node-red/nodes/FetchNode.js | 28 ++ .../components/node-red/nodes/FunctionNode.js | 33 ++ .../components/node-red/nodes/StartNode.js | 49 +++ .../components/node-red/nodes/SwitchNode.js | 28 ++ .../components/node-red/nodes/VueHtmlNode.js | 73 ++++ .../src/components/node-red/nodes/VueNode.vue | 56 +++ .../src/components/node-red/readme.md | 4 + .../src/components/node-red/style.css | 26 ++ .../src/components/node-red/tools/Palette.vue | 155 ++++++++ .../src/components/node-red/tools/Setting.vue | 84 +++++ .../src/components/node-red/util.js | 18 + examples/vue3-memory-leak/src/main.js | 9 + examples/vue3-memory-leak/src/style.css | 11 + examples/vue3-memory-leak/vite.config.js | 24 ++ packages/core/src/LogicFlow.tsx | 15 +- packages/core/src/model/edge/BaseEdgeModel.ts | 4 +- .../core/src/model/edge/BezierEdgeModel.ts | 9 + 72 files changed, 3907 insertions(+), 22 deletions(-) create mode 100644 DEVELOPER.md create mode 100644 examples/engine-browser-examples/src/pages/graph/nodes/ReactNode.tsx create mode 100644 examples/feature-examples/src/pages/extensions/bpmn/bpmn.json create mode 100644 examples/feature-examples/src/pages/extensions/bpmn/diagram.xml create mode 100644 examples/feature-examples/src/pages/extensions/bpmn/index.less create mode 100644 examples/feature-examples/src/pages/extensions/bpmn/index.tsx create mode 100644 examples/feature-examples/src/pages/extensions/bpmn/svgIcons.ts create mode 100644 examples/feature-examples/src/pages/extensions/bpmn/tips.ts create mode 100644 examples/feature-examples/src/pages/extensions/bpmn/util.ts create mode 100644 examples/feature-examples/src/pages/extensions/control/index.less create mode 100644 examples/feature-examples/src/pages/extensions/control/index.tsx create mode 100644 examples/feature-examples/src/pages/extensions/menu/index.less create mode 100644 examples/feature-examples/src/pages/extensions/menu/index.tsx create mode 100644 examples/feature-examples/src/pages/nodes/custom/ellipse/index.less create mode 100644 examples/feature-examples/src/pages/nodes/custom/ellipse/index.tsx create mode 100644 examples/feature-examples/src/pages/nodes/custom/html/data.ts create mode 100644 examples/feature-examples/src/pages/nodes/custom/html/index.less create mode 100644 examples/feature-examples/src/pages/nodes/custom/html/index.tsx create mode 100644 examples/feature-examples/src/pages/nodes/custom/html/style.css create mode 100644 examples/feature-examples/src/pages/nodes/custom/icon/index.less create mode 100644 examples/feature-examples/src/pages/nodes/custom/icon/index.tsx create mode 100644 examples/feature-examples/src/pages/nodes/custom/image/Cloud.tsx create mode 100644 examples/feature-examples/src/pages/nodes/custom/image/index.less create mode 100644 examples/feature-examples/src/pages/nodes/custom/image/index.tsx create mode 100644 examples/feature-examples/src/pages/nodes/custom/rect/index.less create mode 100644 examples/feature-examples/src/pages/nodes/custom/rect/index.tsx create mode 100644 examples/feature-examples/src/pages/nodes/custom/theme/index.less create mode 100644 examples/feature-examples/src/pages/nodes/custom/theme/index.tsx create mode 100644 examples/feature-examples/src/pages/nodes/custom/theme/theme.ts create mode 100644 examples/vue3-memory-leak/.gitignore create mode 100644 examples/vue3-memory-leak/README.md create mode 100644 examples/vue3-memory-leak/auto-imports.d.ts create mode 100644 examples/vue3-memory-leak/components.d.ts create mode 100644 examples/vue3-memory-leak/index.html create mode 100644 examples/vue3-memory-leak/package.json create mode 100644 examples/vue3-memory-leak/public/favicon.ico create mode 100644 examples/vue3-memory-leak/public/images/delay.svg create mode 100644 examples/vue3-memory-leak/public/images/fetch.svg create mode 100644 examples/vue3-memory-leak/public/images/function.svg create mode 100644 examples/vue3-memory-leak/public/images/start.svg create mode 100644 examples/vue3-memory-leak/public/images/swap.svg create mode 100644 examples/vue3-memory-leak/public/images/switch.svg create mode 100644 examples/vue3-memory-leak/src/App.vue create mode 100644 examples/vue3-memory-leak/src/assets/logo.png create mode 100644 examples/vue3-memory-leak/src/components/FlowChart.vue create mode 100644 examples/vue3-memory-leak/src/components/engine/index.js create mode 100644 examples/vue3-memory-leak/src/components/index.vue create mode 100644 examples/vue3-memory-leak/src/components/node-red/FlowLink.js create mode 100644 examples/vue3-memory-leak/src/components/node-red/index.js create mode 100644 examples/vue3-memory-leak/src/components/node-red/nodes/BaseNode.js create mode 100644 examples/vue3-memory-leak/src/components/node-red/nodes/DelayNode.js create mode 100644 examples/vue3-memory-leak/src/components/node-red/nodes/FetchNode.js create mode 100644 examples/vue3-memory-leak/src/components/node-red/nodes/FunctionNode.js create mode 100644 examples/vue3-memory-leak/src/components/node-red/nodes/StartNode.js create mode 100644 examples/vue3-memory-leak/src/components/node-red/nodes/SwitchNode.js create mode 100644 examples/vue3-memory-leak/src/components/node-red/nodes/VueHtmlNode.js create mode 100644 examples/vue3-memory-leak/src/components/node-red/nodes/VueNode.vue create mode 100644 examples/vue3-memory-leak/src/components/node-red/readme.md create mode 100644 examples/vue3-memory-leak/src/components/node-red/style.css create mode 100644 examples/vue3-memory-leak/src/components/node-red/tools/Palette.vue create mode 100644 examples/vue3-memory-leak/src/components/node-red/tools/Setting.vue create mode 100644 examples/vue3-memory-leak/src/components/node-red/util.js create mode 100644 examples/vue3-memory-leak/src/main.js create mode 100644 examples/vue3-memory-leak/src/style.css create mode 100644 examples/vue3-memory-leak/vite.config.js diff --git a/.eslintignore b/.eslintignore index 92cc66301..b2bb466d8 100644 --- a/.eslintignore +++ b/.eslintignore @@ -15,4 +15,6 @@ packages/core/types packages/extension/src/bpmn-adapter/xml2json.ts packages/extension/src/bpmn-adapter/json2xml.ts +examples/vue3-memory-leak + **/*.d.ts diff --git a/DEVELOPER.md b/DEVELOPER.md new file mode 100644 index 000000000..cc78a0076 --- /dev/null +++ b/DEVELOPER.md @@ -0,0 +1,129 @@ +# developer tips + +## Start + +我们选用 fork 仓库,然后提交 PR 的形式进行开发。 + +### clone 自己 fork 后的仓库 + +```shell +git clone +``` + +### 安装项目依赖 + +> 需要提前安装 yarn + +```shell +npm run bootstrap +``` + +### 构建 types 并打包 + +LF 使用 monorepo 的形式进行管理,各个 package 之间存在依赖关系,所以需要先构建一次类型和源码才能进行开发。 + +```shell +npm run build:types + +npm run build +``` + +### 启动本地开发 + +开发 core 包 + +```shell +cd packages/core +npm run dev + +# 或跳过以上“构建部分”直接运行 +npm run dev:core +``` + +开发 extension + +```shell +cd packages/extension +npm run dev + +# 或跳过以上“构建部分”直接运行 +npm run dev:extension +``` + +### 项目配置修改 + +windows 和 mac 平台的换行不一致,windows 下是 CRLF,mac 下是 LF,因此 windows 系统下需要修改 eslint 规则: +(如果 widows 配置了转换为 LF,此条不适用) + +```js +{ + rules: { + 'linebreak-style': ['error', 'unix'], + // ... + } +} + +// 改为 +{ + rules: { + 'linebreak-style': ['error', process.env.NODE_ENV === 'production' ? 'unix' : 'windows'], + // ... + } +} +``` + +## Publish + +### clone 源码仓库 + +```shell +git clone git@github.com:didi/LogicFlow.git +``` + +### 源码打包 + +```shell +# 安装依赖 +npm run bootstrap + +# 构建 types +npm run build:types + +# 打包 +npm run build +``` + +### 更改 npm 官方源 + +```shell +npm config set registry https://registry.npmjs.org/ +``` + +### 本地登陆 npm + +```shell +npm login + +# 查看是否已经登陆 +npm whoami +``` + +### 为项目添加 tags + +```shell +lerna version patch +``` + +lerna version 的详细使用方式见[这里](https://github.com/lerna/lerna/tree/main/libs/commands/version#readme) + +### 发布版本 + +```shell +npm run lerna:publish +``` + +### 推 tag 到远端 + +```shell +git push origin --tags +``` diff --git a/examples/engine-browser-examples/src/pages/graph/index.tsx b/examples/engine-browser-examples/src/pages/graph/index.tsx index 4141a10d5..c5794b07d 100644 --- a/examples/engine-browser-examples/src/pages/graph/index.tsx +++ b/examples/engine-browser-examples/src/pages/graph/index.tsx @@ -1,10 +1,10 @@ -import { map } from 'lodash-es' +import { forEach, map } from 'lodash-es' import LogicFlow, { ElementState, LogicFlowUtil } from '@logicflow/core' import '@logicflow/core/es/index.css' import { Button, Card, Divider, Flex } from 'antd' import { useEffect, useRef } from 'react' -import { combine, square, star, uml, user } from './nodes' +import { combine, square, star, uml, user, reactNode } from './nodes' import { animation, connection } from './edges' import GraphConfigData = LogicFlow.GraphConfigData @@ -84,6 +84,17 @@ const data: GraphConfigData = { x: 600, y: 200, }, + { + id: 'custom-react-node-1', + text: { + x: 200, + y: 500, + value: 'custom-react-node', + }, + type: 'custom-react-node', + x: 200, + y: 500, + }, ], edges: [], } @@ -103,6 +114,7 @@ export default function BasicNode() { star, uml, user, + reactNode, ] map(elements, (customElement) => { @@ -112,7 +124,7 @@ export default function BasicNode() { const registerEvents = (lf: LogicFlow) => { lf.on('history:change', () => { const data = lf.getGraphData() - console.log(data) + console.log('history:change', data) }) } @@ -123,7 +135,7 @@ export default function BasicNode() { container: containerRef.current as HTMLElement, // hideAnchors: true, // width: 1200, - height: 400, + // height: 400, // adjustNodePosition: false, // isSilentMode: true, // overlapMode: 1, @@ -249,8 +261,9 @@ export default function BasicNode() { const lf = lfRef.current if (lf) { const data = lf.getGraphData() - console.log(data) + console.log('current graph data', data) const refreshData = LogicFlowUtil.refreshGraphId(data) + console.log('after refresh graphId', data) lf.render(refreshData) } } @@ -275,7 +288,7 @@ export default function BasicNode() { const handleTurnAnimationOn = () => { if (lfRef.current) { const { edges } = lfRef.current.getGraphData() as GraphConfigData - edges.forEach((edge) => { + forEach(edges, (edge) => { lfRef.current?.openEdgeAnimation(edge.id) }) } @@ -283,7 +296,7 @@ export default function BasicNode() { const handleTurnAnimationOff = () => { if (lfRef.current) { const { edges } = lfRef.current.getGraphData() as GraphConfigData - edges.forEach((edge) => { + forEach(edges, (edge) => { lfRef.current?.closeEdgeAnimation(edge.id) }) } @@ -444,9 +457,6 @@ export default function BasicNode() { 节点面板 - {/* */} - {/* */} - {/* */}
diff --git a/examples/engine-browser-examples/src/pages/graph/nodes/ReactNode.tsx b/examples/engine-browser-examples/src/pages/graph/nodes/ReactNode.tsx new file mode 100644 index 000000000..129232818 --- /dev/null +++ b/examples/engine-browser-examples/src/pages/graph/nodes/ReactNode.tsx @@ -0,0 +1,63 @@ +import ReactDOM from 'react-dom/client' +import { ColorPicker, Space, Tooltip } from 'antd' +import { CheckCircleTwoTone } from '@ant-design/icons' +import { HtmlNode, HtmlNodeModel } from '@logicflow/core' + +export class CustomReactNodeModel extends HtmlNodeModel { + createId() { + return Math.random() + '_react_node' + } + + setAttributes() { + const width = 200 + const height = 130 + this.width = width + this.height = height + this.textHeight = 60 + this.text = { + ...this.text, + y: this.y - this.height / 2, + } + + this.anchorsOffset = [ + { + x: width / 2, + y: 0, + isSourceAnchor: false, + isTargetAnchor: true, + }, + ] + // this.anchorsOffset = [ + // [width / 2, 0], + // [0, height / 2], + // [-width / 2, 0], + // [0, -height / 2], + // ] + } +} + +export class CustomReactNode extends HtmlNode { + setHtml(rootEl: SVGForeignObjectElement) { + const { properties } = this.props.model + console.log('properties', properties) + + const el = document.createElement('div') + el.className = 'custom-react-node-wrapper' + ReactDOM.createRoot(el).render( + + + + + Show Tooltip + + , + ) + rootEl.appendChild(el) + } +} + +export default { + type: 'custom-react-node', + view: CustomReactNode, + model: CustomReactNodeModel, +} diff --git a/examples/engine-browser-examples/src/pages/graph/nodes/index.ts b/examples/engine-browser-examples/src/pages/graph/nodes/index.ts index 8b2cb58af..45c32b6eb 100644 --- a/examples/engine-browser-examples/src/pages/graph/nodes/index.ts +++ b/examples/engine-browser-examples/src/pages/graph/nodes/index.ts @@ -3,11 +3,13 @@ import star from './star' import square from './square' import uml from './uml' import user from './user' +import reactNode from './ReactNode' export * from './combine' export * from './star' export * from './square' export * from './uml' export * from './user' +export * from './ReactNode' -export { combine, square, star, uml, user } +export { combine, square, star, uml, user, reactNode } diff --git a/examples/feature-examples/src/pages/extensions/bpmn/bpmn.json b/examples/feature-examples/src/pages/extensions/bpmn/bpmn.json new file mode 100644 index 000000000..ebc33ae50 --- /dev/null +++ b/examples/feature-examples/src/pages/extensions/bpmn/bpmn.json @@ -0,0 +1,255 @@ +{ + "bpmn:definitions": { + "-xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance", + "-xmlns:bpmn": "http://www.omg.org/spec/BPMN/20100524/MODEL", + "-xmlns:bpmndi": "http://www.omg.org/spec/BPMN/20100524/DI", + "-xmlns:dc": "http://www.omg.org/spec/DD/20100524/DC", + "-xmlns:di": "http://www.omg.org/spec/DD/20100524/DI", + "-id": "Definitions_06hsjbb", + "-targetNamespace": "http://bpmn.io/schema/bpmn", + "-exporter": "bpmn-js (https://demo.bpmn.io)", + "-exporterVersion": "7.3.0", + "bpmn:process": { + "-id": "Process_0zfyzey", + "-isExecutable": "false", + "bpmn:startEvent": { + "-id": "StartEvent_072hx11", + "bpmn:outgoing": "Flow_17mk55x" + }, + "bpmn:exclusiveGateway": { + "-id": "Gateway_11dyzcj", + "bpmn:incoming": "Flow_00um71v", + "bpmn:outgoing": ["Flow_1iy6wvy", "Flow_05w5m1i"] + }, + "bpmn:sequenceFlow": [ + { + "-id": "Flow_17mk55x", + "-sourceRef": "StartEvent_072hx11", + "-targetRef": "Activity_00hpzik" + }, + { + "-id": "Flow_00um71v", + "-sourceRef": "Activity_00hpzik", + "-targetRef": "Gateway_11dyzcj" + }, + { + "-id": "Flow_1iy6wvy", + "-sourceRef": "Gateway_11dyzcj", + "-targetRef": "Activity_1r8wi1d" + }, + { + "-id": "Flow_091o1xz", + "-sourceRef": "Activity_1r8wi1d", + "-targetRef": "Event_0e2j3bl" + }, + { + "-id": "Flow_05w5m1i", + "-sourceRef": "Gateway_11dyzcj", + "-targetRef": "Activity_0pbgbgt" + }, + { + "-id": "Flow_1hzglvd", + "-sourceRef": "Activity_0pbgbgt", + "-targetRef": "Event_1s9e2ln" + } + ], + "bpmn:endEvent": [ + { + "-id": "Event_0e2j3bl", + "bpmn:incoming": "Flow_091o1xz" + }, + { + "-id": "Event_1s9e2ln", + "bpmn:incoming": "Flow_1hzglvd" + } + ], + "bpmn:serviceTask": { + "-id": "Activity_1r8wi1d", + "bpmn:incoming": "Flow_1iy6wvy", + "bpmn:outgoing": "Flow_091o1xz" + }, + "bpmn:userTask": [ + { + "-id": "Activity_00hpzik", + "bpmn:incoming": "Flow_17mk55x", + "bpmn:outgoing": "Flow_00um71v" + }, + { + "-id": "Activity_0pbgbgt", + "bpmn:incoming": "Flow_05w5m1i", + "bpmn:outgoing": "Flow_1hzglvd" + } + ] + }, + "bpmndi:BPMNDiagram": { + "-id": "BPMNDiagram_1", + "bpmndi:BPMNPlane": { + "-id": "BPMNPlane_1", + "-bpmnElement": "Process_0zfyzey", + "bpmndi:BPMNEdge": [ + { + "-id": "Flow_091o1xz_di", + "-bpmnElement": "Flow_091o1xz", + "di:waypoint": [ + { + "-x": "580", + "-y": "130" + }, + { + "-x": "632", + "-y": "130" + } + ] + }, + { + "-id": "Flow_1iy6wvy_di", + "-bpmnElement": "Flow_1iy6wvy", + "di:waypoint": [ + { + "-x": "435", + "-y": "130" + }, + { + "-x": "480", + "-y": "130" + } + ] + }, + { + "-id": "Flow_00um71v_di", + "-bpmnElement": "Flow_00um71v", + "di:waypoint": [ + { + "-x": "340", + "-y": "130" + }, + { + "-x": "385", + "-y": "130" + } + ] + }, + { + "-id": "Flow_17mk55x_di", + "-bpmnElement": "Flow_17mk55x", + "di:waypoint": [ + { + "-x": "188", + "-y": "130" + }, + { + "-x": "240", + "-y": "130" + } + ] + }, + { + "-id": "Flow_05w5m1i_di", + "-bpmnElement": "Flow_05w5m1i", + "di:waypoint": [ + { + "-x": "410", + "-y": "155" + }, + { + "-x": "410", + "-y": "240" + }, + { + "-x": "480", + "-y": "240" + } + ] + }, + { + "-id": "Flow_1hzglvd_di", + "-bpmnElement": "Flow_1hzglvd", + "di:waypoint": [ + { + "-x": "580", + "-y": "240" + }, + { + "-x": "632", + "-y": "240" + } + ] + } + ], + "bpmndi:BPMNShape": [ + { + "-id": "Gateway_11dyzcj_di", + "-bpmnElement": "Gateway_11dyzcj", + "-isMarkerVisible": "true", + "dc:Bounds": { + "-x": "385", + "-y": "105", + "-width": "50", + "-height": "50" + } + }, + { + "-id": "Event_0e2j3bl_di", + "-bpmnElement": "Event_0e2j3bl", + "dc:Bounds": { + "-x": "632", + "-y": "112", + "-width": "36", + "-height": "36" + } + }, + { + "-id": "Activity_09ucrnv_di", + "-bpmnElement": "Activity_1r8wi1d", + "dc:Bounds": { + "-x": "480", + "-y": "90", + "-width": "100", + "-height": "80" + } + }, + { + "-id": "Activity_13bhmg8_di", + "-bpmnElement": "Activity_00hpzik", + "dc:Bounds": { + "-x": "240", + "-y": "90", + "-width": "100", + "-height": "80" + } + }, + { + "-id": "Activity_015rcjo_di", + "-bpmnElement": "Activity_0pbgbgt", + "dc:Bounds": { + "-x": "480", + "-y": "200", + "-width": "100", + "-height": "80" + } + }, + { + "-id": "Event_1s9e2ln_di", + "-bpmnElement": "Event_1s9e2ln", + "dc:Bounds": { + "-x": "632", + "-y": "222", + "-width": "36", + "-height": "36" + } + }, + { + "-id": "_BPMNShape_StartEvent_2", + "-bpmnElement": "StartEvent_072hx11", + "dc:Bounds": { + "-x": "152", + "-y": "112", + "-width": "36", + "-height": "36" + } + } + ] + } + } + } +} diff --git a/examples/feature-examples/src/pages/extensions/bpmn/diagram.xml b/examples/feature-examples/src/pages/extensions/bpmn/diagram.xml new file mode 100644 index 000000000..0fcf16999 --- /dev/null +++ b/examples/feature-examples/src/pages/extensions/bpmn/diagram.xml @@ -0,0 +1,92 @@ + + + + + Flow_17mk55x + + + Flow_00um71v + Flow_1iy6wvy + Flow_05w5m1i + + + + + + Flow_091o1xz + + + + Flow_1iy6wvy + Flow_091o1xz + + + Flow_17mk55x + Flow_00um71v + + + + Flow_05w5m1i + Flow_1hzglvd + + + Flow_1hzglvd + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/feature-examples/src/pages/extensions/bpmn/index.less b/examples/feature-examples/src/pages/extensions/bpmn/index.less new file mode 100644 index 000000000..4eeb595a0 --- /dev/null +++ b/examples/feature-examples/src/pages/extensions/bpmn/index.less @@ -0,0 +1,48 @@ +.viewport { + position: relative; + height: 70vh; + overflow: hidden; +} + +:global { + .lf-dnd-shape { + background-size: contain; + } + + .graph-io { + position: absolute; + right: 15px; + bottom: 10px; + z-index: 9999; + display: flex; + padding: 10px; + background: rgb(255 255 255 / 80%); + border-radius: 5px; + box-shadow: 0 1px 4px rgb(0 0 0 / 30%); + } + + .graph-io a { + margin: 0 5px; + } + + .graph-io a img { + height: 24px; + } + + .custom-minimap { + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAGeUlEQVRoQ+1afYhVRRQ/5y66RVBiH2RalCwWGltvZt7SUon+YaSCZpRaWln5AaVEUUErpRYVVBihBX1Y24f5sVEqlJJ/JH2w0btntpZWShYDy4ossSBslb0nzmXuMt7efe++52r+sQcuu2/mzG/mnDNz5pxzL4JHSqnFiNgKAEUAmOD3ZfzfQ0SX5+DLZNFaf5t3LgAoMXOntfaVBBCTf7TWIQDoWhfDzKustStrHSf8SqmViLiijrFEREbGxQJorTcDwM11AO0iosl1jBsYorX+BAAm1YHRQUSz0RhzGzO/5QF0RFG0squra3cdoCdsSKFQGB8EgVh6QNGIeDtqrd8FgFtk5uPZDids5Sng1LbbIAJ8DwDjhC8IgkKpVPr6ZC2mnnmKxeKVURR1ubF7RABOgIho4FDXA36yxvhrHhLgZGndn2fIAv+H1ocsUE3rWuvLmHluEASjmHk0AFzkHhm6Tx5E3B9F0S+IuJGIvquGmdU/qGfAGDODmecAgDwNORfVDwCbEHFTGIbbco7xw48B11+3G9VaT2fmFYgokWvdxMwlRFxFRB/mBUlbIAlnc4fGWuvHAeDRMhPuAoAdALCXmfcePXp0r/AMGzZsLCKOBQB5rs8I3p4gosfyCOGF4D3xzSvxRZ6QuFgsFqMoehIApngTHQCADkTsCMNQBIDm5uYzGhsbxzNzs/xGxO6+vr7d3d3df8tvY8wkZpagTJ5zPaydQRAsL5VKpWqCJGvOHTporW8AgDcB4MwEXPYwALSFYRhrWkhr/SAA3AcAY1KL+AkAXiCi55J2Y4xY5Cl3hpLmvwDgDiLaUk2IWDl5mIwxU5h5KwCc7vj7ZOFEtNofb4zZzsyyRTIJEXeEYTjVZ9BaPyCCAECjaz+MiDPDMNxZbX1VBTDGXM3MH3imluh1IRF97oMrpeaIe/TaPgYA4RW6FACuS/rE3VprxXoDpLW+BgBec7zSfgARZ4Vh+EVFhVTqdKHr+wBwieP70WkmCWfjZqVUMyJ+mViImedba9enBJyHiO8kGmbmq6y13SkLFpylL3TtPwRBcGOlEL+iBZRSX3lu8mAURbO6uro+TQttjFnKzGtc+0tEdG85xWitXwSAe6QPEZeFYbg2zVcoFCYGQSAWHyl94mattS1Zis4UwHkKyVeF/gGAm7J8tdZaTH+3m3CJXzVIWUGqHi+7tnVEtDBD0OkA8B4AnOaEnZx4uDR/pgBaawF/1S1qo7U2TjvLkVJqKyLOcJNNC8Nwezk+Y8xUZv7IYW6z1s6sgLkBEee6/kVEJEr6D2UK0NraOvLIkSPi+mLPw8yvW2tjLadJKdWGiHI/CD3ku0qf17nYZx3ecmuteJ5yeOsQ8S7XcXj48OFjOjs7D9YkgDCnb1xmXmutXZYGcneE7Fuhg0R0drnJtNZ/JHsbAGaV8/VKqTWIuNQbX/GGrupGlVKrEfF+D/BpImorI4Qc7mtdew8iPszMsbdCRPEuz3gVuM+IaGIZDLHII0k7Mz9vrZU7IpOqCuAsIaW8RR6KnA25yH5P2orF4vkSKleaLOmTkLtUKv2a/NZan+MusmPmIKLF1fByCeCEEB8+zwP8BhEljIgPpeMZBQBSZ8qqtEmsdCsRDQhqjJnGzKL5Kzzs9UQ0v9riY+vmYUp4yuzPuBjGzJv9Sp4EWkEQKGaOa62ISFEUWT9glEobIs5O10azzlnWOmsSQECUUnKjisYk4/KpQ6JSIpK/maS1TqLQdC12HzO3pW/wLCC5p+RuqCmc9izR5Nzm7DIT/JmkkJIXuH6JOpMU86wyYzYzs7jV3jw7Qmv9BgAsAIB2yciS6nDNlWal1IIgCOZUi0AzzY+4I4qiTdba9jwL987a8aeU/oQuYk3y4vOqLOY3Lx+uGGlm4QxqUu9P0tTU1DhixIjRzHyBVCYQUaoTctD3S0UCEX8+dOjQ/t7eXskn6qYTJkDdK6px4JAANSps0NmHLDDoKq0RcMgCNSps0NmzSosQRdGEU+31alp697q1x7X3SCiRxBXSFr88HnSVDSJg6qV8O7rvI5JKQRweNzQ0bDnVXrdKjaq/v/8GP/xm5iXJpwZ1fSfhFLuFiGYdr5JbWlrG9ff3S7XPL/ZWgo2/l/A/9qj3ewlJWDLrNnkFq/GbiYGtfkxC476bkMKrZFLx2/sc1E5Ed+bgq8jiyphS2bg4g3EPABAibg/D8O2E51+mVguWMJuBHAAAAABJRU5ErkJggg=='); + } + + .upload { + position: absolute; + top: 0; + left: 0; + z-index: 99; + cursor: pointer; + opacity: 0; + } + + .upload::-webkit-file-upload-button { + cursor: pointer; + } +} diff --git a/examples/feature-examples/src/pages/extensions/bpmn/index.tsx b/examples/feature-examples/src/pages/extensions/bpmn/index.tsx new file mode 100644 index 000000000..0891735ed --- /dev/null +++ b/examples/feature-examples/src/pages/extensions/bpmn/index.tsx @@ -0,0 +1,355 @@ +import LogicFlow from '@logicflow/core' +import { + AutoLayout, + BpmnElement, + BpmnXmlAdapter, + ContextMenu, + Control, + DndPanel, + ShapeItem, + FlowPath, + Menu, + Group, + MiniMap, + SelectionSelect, + Snapshot, +} from '@logicflow/extension' + +import { Button, Card, Divider, Flex } from 'antd' +import { useEffect, useRef } from 'react' +import { download } from './util' + +import '@logicflow/core/es/index.css' +import '@logicflow/extension/es/index.css' +import styles from './index.less' +import NodeData = LogicFlow.NodeData +import GraphConfigData = LogicFlow.GraphConfigData + +const config: Partial = { + edgeTextDraggable: true, + nodeTextDraggable: true, + // adjustNodePosition: false, + // stopMoveGraph: true, + // multipleSelectedKey: 'meta', // alt, shift + hideAnchors: false, + plugins: [ + BpmnElement, + MiniMap, + FlowPath, + AutoLayout, + DndPanel, + Menu, + ContextMenu, + Group, + Control, + BpmnXmlAdapter, + Snapshot, + SelectionSelect, + ], + // isSilentMode: true, + grid: { + type: 'dot', + size: 20, + }, + keyboard: { + enabled: true, + }, + snapline: true, +} + +const menuConfig: Record = { + nodeMenu: [ + { + text: '分享', + callback() { + console.log('分享成功!') + }, + }, + { + text: '复制', + callback() { + console.log('分享成功!') + }, + }, + { + text: '修改', + callback() { + console.log('分享成功!') + }, + }, + ], + graphMenu: [ + { + text: '分111享', + callback() { + console.log('分享成功22!') + }, + }, + ], +} + +const defaultIconConfig: ShapeItem[] = [ + { + type: 'bpmn:startEvent', + label: '开始', + text: '开始', + icon: require('@/assets/bpmn/start-event-none.png'), + }, + { + type: 'bpmn:userTask', + label: '用户任务', + icon: require('@/assets/bpmn/user-task.png'), + properties: { + actived: true, + }, + }, + { + type: 'bpmn:serviceTask', + label: '系统任务', + icon: require('@/assets/bpmn/service-task.png'), + cls: 'import_icon', + }, + { + type: 'bpmn:exclusiveGateway', + label: '条件判断', + icon: require('@/assets/bpmn/gateway-xor.png'), + }, + { + type: 'bpmn:endEvent', + label: '结束', + icon: require('@/assets/bpmn/end-event-none.png'), + }, + { + type: 'group', + label: '分组', + icon: require('@/assets/bpmn/task-none.png'), + }, +] + +const getDndPanelConfig = (lf: LogicFlow): ShapeItem[] => [ + { + label: '选区', + icon: require('@/assets/bpmn/select.png'), + callback: () => { + lf.openSelectionSelect() + lf.once('selection:selected', () => { + lf.closeSelectionSelect() + }) + }, + }, + ...defaultIconConfig, +] + +export default function BPMNExtension() { + const lfRef = useRef() + const containerRef = useRef(null) + + const renderXml = (xml: any) => { + const lf = lfRef.current + if (!lf) { + return + } + lf.renderByXml(xml) + } + + useEffect(() => { + if (!lfRef.current) { + const lf = new LogicFlow({ + ...config, + container: containerRef.current as HTMLElement, + }) + + lf.setMenuConfig(menuConfig) + + const commonMenuConfig = { + icon: require('@/assets/bpmn/delete.png'), + callback: (data: NodeData) => { + lf.deleteElement(data.id) + lf.hideContextMenu() + }, + } + lf.setContextMenuItems([commonMenuConfig]) + lf.setContextMenuByType('bpmn:userTask', defaultIconConfig) + // TODO: 待确认具体功能 + // lf.setContextMenuByType('bpmn:serviceTask', defaultIconConfig); + + const dndPanelConfig = getDndPanelConfig(lf) + lf.setPatternItems(dndPanelConfig) + + // TODO: 解决 lf extension 插件注册之后,类型仍然是 Extension 的问题,需要注册后定义对应类型,以方便访问其属性 or 方法 + const { control, miniMap } = lf.extension + + ;(control as Control).addItem({ + iconClass: 'custom-minimap', + title: '', + text: '导航', + onMouseEnter: (lf: LogicFlow, ev: MouseEvent) => { + const position = lf.getPointByClient(ev.x, ev.y) + ;(miniMap as MiniMap).show( + position.domOverlayPosition.x - 120, + position.domOverlayPosition.y + 35, + ) + }, + onClick: (lf: LogicFlow, ev: MouseEvent) => { + // console.log(MiniMap, ev); + const position = lf.getPointByClient(ev.x, ev.y) + // console.log(position); + ;(miniMap as MiniMap).show( + position.domOverlayPosition.x - 120, + position.domOverlayPosition.y + 35, + ) + }, + }) + + // 获取渲染数据 + let lfData: GraphConfigData + + const sessionStorageData = window.sessionStorage.getItem('lf-data') + if (sessionStorageData) { + lfData = JSON.parse(sessionStorageData) + renderXml(lfData) + } else { + const lfJsonData = window.sessionStorage.getItem('lf-json-data') + if (!lfJsonData) { + lfData = { + nodes: [], + edges: [], + } + } else { + lfData = JSON.parse(lfJsonData) + } + lf.render(lfData as GraphConfigData) + } + const pathes = window.sessionStorage.getItem('lf-pathes') + if (pathes) { + lf.setRawPathes(JSON.parse(pathes)) + } + + lfRef.current = lf + } + }, []) + + const handleDownloadData = () => { + const data = lfRef.current?.getGraphData() + const dataString = JSON.stringify(data) + download('logicflow.xml', dataString) + window.sessionStorage.setItem('lf-data', dataString) + } + + const handleUploadData = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + const reader = new FileReader() + reader.onload = (event) => { + const xml = event.target?.result + + renderXml(xml) + } + reader.onerror = (error) => console.log(error) + + file && reader.readAsText(file) // you could also read images and other binaries + } + + const getPath = () => { + const lf = lfRef.current + if (lf) { + lf.setStartNodeType('bpmn:startEvent') + const paths = lf.getPathes() + console.log('paths', paths) + window.sessionStorage.setItem('lf-paths', JSON.stringify(paths)) + // console.log(JSON.parse(window.sessionStorage.getItem('lf-pathes') ?? '')) + } + } + + const autoLayout = () => { + const nextData = lfRef.current?.layout('bpmn:startEvent') + console.log('after layout:', nextData) + } + + const getSelectElements = () => { + const data = lfRef.current?.getSelectElements(true) + console.log('selected elements: ', data) + } + + const showContextMenu = () => { + const lf = lfRef.current + if (lf) { + const { nodes } = lf.getSelectElements() + console.log(nodes[0]) + lf.showContextMenu(nodes[0]) + } + } + + return ( + +

兼容BPMN官方DEMO,此处仅实现了bpmn中的一部分节点

+

此页面绘制的图可以在BPMN官方DEMO中正常使用

+

+ 点击左下角下载xml,将文件上传到{' '} + + https://demo.bpmn.io/ + + 即可使用。 +

+ + + + + + + + + +
+ +
+ ) +} diff --git a/examples/feature-examples/src/pages/extensions/bpmn/svgIcons.ts b/examples/feature-examples/src/pages/extensions/bpmn/svgIcons.ts new file mode 100644 index 000000000..6982fb5b5 --- /dev/null +++ b/examples/feature-examples/src/pages/extensions/bpmn/svgIcons.ts @@ -0,0 +1,23 @@ +export const startEventIcon: string = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAABGdBTUEAALGPC/xhBQAAAnBJREFUOBGdVL1rU1EcPfdGBddmaZLiEhdx1MHZQXApraCzQ7GKLgoRBxMfcRELuihWKcXFRcEWF8HBf0DdDCKYRZpnl7p0svLe9Zzbd29eQhTbC8nv+9zf130AT63jvooOGS8Vf9Nt5zxba7sXQwODfkWpkbjTQfCGUd9gIp3uuPP8bZ946g56dYQvnBg+b1HB8VIQmMFrazKcKSvFW2dQTxJnJdQ77urmXWOMBCmXM2Rke4S7UAW+/8ywwFoewmBps2tu7mbTdp8VMOkIRAkKfrVawalJTtIliclFbaOBqa0M2xImHeVIfd/nKAfVq/LGnPss5Kh00VEdSzfwnBXPUpmykNss4lUI9C1ga+8PNrBD5YeqRY2Zz8PhjooIbfJXjowvQJBqkmEkVnktWhwu2SM7SMx7Cj0N9IC0oQXRo8xwAGzQms+xrB/nNSUWVveI48ayrFGyC2+E2C+aWrZHXvOuz+CiV6iycWe1Rd1Q6+QUG07nb5SbPrL4426d+9E1axKjY3AoRrlEeSQo2Eu0T6BWAAr6COhTcWjRaYfKG5csnvytvUr/WY4rrPMB53Uo7jZRjXaG6/CFfNMaXEu75nG47X+oepU7PKJvvzGDY1YLSKHJrK7vFUwXKkaxwhCW3u+sDFMVrIju54RYYbFKpALZAo7sB6wcKyyrd+aBMryMT2gPyD6GsQoRFkGHr14TthZni9ck0z+Pnmee460mHXbRAypKNy3nuMdrWgVKj8YVV8E7PSzp1BZ9SJnJAsXdryw/h5ctboUVi4AFiCd+lQaYMw5z3LGTBKjLQOeUF35k89f58Vv/tGh+l+PE/wG0rgfIUbZK5AAAAABJRU5ErkJggg==' + +export const endEventIcon: string = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAABGdBTUEAALGPC/xhBQAAA1BJREFUOBFtVE1IVUEYPXOf+tq40Y3vPcmFIdSjIorWoRG0ERWUgnb5FwVhYQSl72oUoZAboxKNFtWiwKRN0M+jpfSzqJAQclHo001tKkjl3emc8V69igP3znzfnO/M9zcDcKT67azmjYWTwl9Vn7Vumeqzj1DVb6cleQY4oAVnIOPb+mKAGxQmKI5CWNJ2aLPatxWa3aB9K7/fB+/Z0jUF6TmMlFLQqrkECWQzOZxYGjTlOl8eeKaIY5yHnFn486xBustDjWT6dG7pmjHOJd+33t0iitTPkK6tEvjxq4h2MozQ6WFSX/LkDUGfFwfhEZj1Auz/U4pyAi5Sznd7uKzznXeVHlI/Aywmk6j7fsUsEuCGADrWARXXwjxWQsUbIupDHJI7kF5dRktg0eN81IbiZXiTESic50iwS+t1oJgL83jAiBupLDCQqwziaWSoAFSeIR3P5Xv5az00wyIn35QRYTwdSYbz8pH8fxUUAtxnFvYmEmgI0wYXUXcCCSpeEVpXlsRhBnCEATxWylL9+EKCAYhe1NGstUa6356kS9NVvt3DU2fd+Wtbm/+lSbylJqsqkSm9CRhvoJVlvKPvF1RKY/FcPn5j4UfIMLn8D4UYb54BNsilTDXKnF4CfTobA0FpoW/LSp306wkXM+XaOJhZaFkcNM82ASNAWMrhrUbRfmyeI1FvRBTpN06WKxa9BK0o2E4Pd3zfBBEwPsv9sQBnmLVbLEIZ/Xe9LYwJu/Er17W6HYVBc7vmuk0xUQ+pqxdom5Fnp55SiytXLPYoMXNM4u4SNSCFWnrVIzKG3EGyMXo6n/BQOe+bX3FClY4PwydVhthOZ9NnS+ntiLh0fxtlUJHAuGaFoVmttpVMeum0p3WEXbcll94l1wM/gZ0Ccczop77VvN2I7TlsZCsuXf1WHvWEhjO8DPtyOVg2/mvK9QqboEth+7pD6NUQC1HN/TwvydGBARi9MZSzLE4b8Ru3XhX2PBxf8E1er2A6516o0w4sIA+lwURhAON82Kwe2iDAC1Watq4XHaGQ7skLcFOtI5lDxuM2gZe6WFIotPAhbaeYlU4to5cuarF1QrcZ/lwrLaCJl66JBocYZnrNlvm2+MBCTmUymPrYZVbjdlr/BxlMjmNmNI3SAAAAAElFTkSuQmCC' + +export const userTaskIcon: string = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAEFVwZaAAAABGdBTUEAALGPC/xhBQAAAqlJREFUOBF9VM9rE0EUfrMJNUKLihGbpLGtaCOIR8VjQMGDePCgCCIiCNqzCAp2MyYUCXhUtF5E0D+g1t48qAd7CCLqQUQKEWkStcEfVGlLdp/fm3aW2QQdyLzf33zz5m2IsAZ9XhDpyaaIZkTS4ASzK41TFao88GuJ3hsr2pAbipHxuSYyKRugagICGANkfFnNh3HeE2N0b3nN2cgnpcictw5veJIzxmDamSlxxQZicq/mflxhbaH8BLRbuRwNtZp0JAhoplVRUdzmCe/vO27wFuuA3S5qXruGdboy5/PRGFsbFGKo/haRtQHIrM83bVeTrOgNhZReWaYGnE4aUQgTJNvijJFF4jQ8BxJE5xfKatZWmZcTQ+BVgh7s8SgPlCkcec4mGTmieTP4xd7PcpIEg1TX6gdeLW8rTVMVLVvb7ctXoH0Cydl2QOPJBG21STE5OsnbweVYzAnD3A7PVILuY0yiiyDwSm2g441r6rMSgp6iK42yqroI2QoXeJVeA+YeZSa47gZdXaZWQKTrG93rukk/l2Al6Kzh5AZEl7dDQy+JjgFahQjRopSxPbrbvK7GRe9ePWBo1wcU7sYrFZtavXALwGw/7Dnc50urrHJuTPSoO2IMV3gUQGNg87IbSOIY9BpiT9HV7FCZ94nPXb3MSnwHn/FFFE1vG6DTby+r31KAkUktB3Qf6ikUPWxW1BkXSPQeMHHiW0+HAd2GelJsZz1OJegCxqzl+CLVHa/IibuHeJ1HAKzhuDR+ymNaRFM+4jU6UWKXorRmbyqkq/D76FffevwdCp+jN3UAN/C9JRVTDuOxC/oh+EdMnqIOrlYteKSfadVRGLJFJPSB/ti/6K8f0CNymg/iH2gO/f0DwE0yjAFO6l8JaR5j0VPwPwfaYHqOqrCI319WzwhwzNW/aQAAAABJRU5ErkJggg==' + +export const serviceTaskIcon: string = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAEFVwZaAAAABGdBTUEAALGPC/xhBQAAAqlJREFUOBF9VM9rE0EUfrMJNUKLihGbpLGtaCOIR8VjQMGDePCgCCIiCNqzCAp2MyYUCXhUtF5E0D+g1t48qAd7CCLqQUQKEWkStcEfVGlLdp/fm3aW2QQdyLzf33zz5m2IsAZ9XhDpyaaIZkTS4ASzK41TFao88GuJ3hsr2pAbipHxuSYyKRugagICGANkfFnNh3HeE2N0b3nN2cgnpcictw5veJIzxmDamSlxxQZicq/mflxhbaH8BLRbuRwNtZp0JAhoplVRUdzmCe/vO27wFuuA3S5qXruGdboy5/PRGFsbFGKo/haRtQHIrM83bVeTrOgNhZReWaYGnE4aUQgTJNvijJFF4jQ8BxJE5xfKatZWmZcTQ+BVgh7s8SgPlCkcec4mGTmieTP4xd7PcpIEg1TX6gdeLW8rTVMVLVvb7ctXoH0Cydl2QOPJBG21STE5OsnbweVYzAnD3A7PVILuY0yiiyDwSm2g441r6rMSgp6iK42yqroI2QoXeJVeA+YeZSa47gZdXaZWQKTrG93rukk/l2Al6Kzh5AZEl7dDQy+JjgFahQjRopSxPbrbvK7GRe9ePWBo1wcU7sYrFZtavXALwGw/7Dnc50urrHJuTPSoO2IMV3gUQGNg87IbSOIY9BpiT9HV7FCZ94nPXb3MSnwHn/FFFE1vG6DTby+r31KAkUktB3Qf6ikUPWxW1BkXSPQeMHHiW0+HAd2GelJsZz1OJegCxqzl+CLVHa/IibuHeJ1HAKzhuDR+ymNaRFM+4jU6UWKXorRmbyqkq/D76FffevwdCp+jN3UAN/C9JRVTDuOxC/oh+EdMnqIOrlYteKSfadVRGLJFJPSB/ti/6K8f0CNymg/iH2gO/f0DwE0yjAFO6l8JaR5j0VPwPwfaYHqOqrCI319WzwhwzNW/aQAAAABJRU5ErkJggg==' + +export const exclusiveGatewayIcon: string = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAVCAYAAAHeEJUAAAAABGdBTUEAALGPC/xhBQAAAvVJREFUOBGNVEFrE0EU/mY3bQoiFlOkaUJrQUQoWMGePLX24EH0IIoHKQiCV0G8iE1covgLiqA/QTzVm1JPogc9tIJYFaQtlhQxqYjSpunu+L7JvmUTU3AgmTfvffPNN++9WSA1DO182f6xwILzD5btfAoQmwL5KJEwiQyVbSVZ0IgRyV6PTpIJ81E5ZvqfHQR0HUOBHW4L5Et2kQ6Zf7iAOhTFAA8s0pEP7AXO1uAA52SbqGk6h/6J45LaLhO64ByfcUzM39V7ZiAdS2yCePPEIQYvTUHqM/n7dgQNfBKWPjpF4ISk8q3J4nB11qw6X8l+FsF3EhlkEMfrjIer3wJTLwS2aCNcj4DbGxXTw00JmAuO+Ni6bBxVUCvS5d9aa04+so4pHW5jLTywuXAL7jJ+D06sl82Sgl2JuVBQn498zkc2bGKxULHjCnSMadBKYDYYHAtsby1EQ5lNGrQd4Y3v4Zo0XdGEmDno46yCM9Tk+RiJmUYHS/aXHPNTcjxcbTFna000PFJHIVZ5lFRqRpJWk9/+QtlOUYJj9HG5pVFEU7zqIYDVsw2s+AJaD8wTd2umgSCCyUxgGsS1Y6TBwXQQTFuZaHcd8gAGioE90hlsY+wMcs30RduYtxanjMGal8H5dMW67dmT1JFtYUEe8LiQLRsPZ6IIc7A4J5tqco3T0pnv/4u0kyzrYUq7gASuEyI8VXKvB9Odytv6jS/PNaZBln0nioJG/AVQRZvApOdhjj3Jt8QC8Im09SafwdBdvIpztpxWxpeKCC+EsFdS8DCyuCn2munFpL7ctHKp+Xc5cMybeIyMAN33SPL3ZR9QV1XVwLyzHm6Iv0/yeUuUb7PPlZC4D4HZkeu6dpF4v9j9MreGtMbxMMRLIcjJic9yHi7WQ3yVKzZVWUr5UrViJvn1FfUlwe/KYVfYyWRLSGNu16hR01U9IacajXPei0wx/5BqgInvJN+MMNtNme7ReU9SBbgntovn0kKHpFg7UogZvaZiOue/q1SBo9ktHzQAAAAASUVORK5CYII=' + +export const groupIcon: string = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAABGdBTUEAALGPC/xhBQAAA1BJREFUOBFtVE1IVUEYPXOf+tq40Y3vPcmFIdSjIorWoRG0ERWUgnb5FwVhYQSl72oUoZAboxKNFtWiwKRN0M+jpfSzqJAQclHo001tKkjl3emc8V69igP3znzfnO/M9zcDcKT67azmjYWTwl9Vn7Vumeqzj1DVb6cleQY4oAVnIOPb+mKAGxQmKI5CWNJ2aLPatxWa3aB9K7/fB+/Z0jUF6TmMlFLQqrkECWQzOZxYGjTlOl8eeKaIY5yHnFn486xBustDjWT6dG7pmjHOJd+33t0iitTPkK6tEvjxq4h2MozQ6WFSX/LkDUGfFwfhEZj1Auz/U4pyAi5Sznd7uKzznXeVHlI/Aywmk6j7fsUsEuCGADrWARXXwjxWQsUbIupDHJI7kF5dRktg0eN81IbiZXiTESic50iwS+t1oJgL83jAiBupLDCQqwziaWSoAFSeIR3P5Xv5az00wyIn35QRYTwdSYbz8pH8fxUUAtxnFvYmEmgI0wYXUXcCCSpeEVpXlsRhBnCEATxWylL9+EKCAYhe1NGstUa6356kS9NVvt3DU2fd+Wtbm/+lSbylJqsqkSm9CRhvoJVlvKPvF1RKY/FcPn5j4UfIMLn8D4UYb54BNsilTDXKnF4CfTobA0FpoW/LSp306wkXM+XaOJhZaFkcNM82ASNAWMrhrUbRfmyeI1FvRBTpN06WKxa9BK0o2E4Pd3zfBBEwPsv9sQBnmLVbLEIZ/Xe9LYwJu/Er17W6HYVBc7vmuk0xUQ+pqxdom5Fnp55SiytXLPYoMXNM4u4SNSCFWnrVIzKG3EGyMXo6n/BQOe+bX3FClY4PwydVhthOZ9NnS+ntiLh0fxtlUJHAuGaFoVmttpVMeum0p3WEXbcll94l1wM/gZ0Ccczop77VvN2I7TlsZCsuXf1WHvWEhjO8DPtyOVg2/mvK9QqboEth+7pD6NUQC1HN/TwvydGBARi9MZSzLE4b8Ru3XhX2PBxf8E1er2A6516o0w4sIA+lwURhAON82Kwe2iDAC1Watq4XHaGQ7skLcFOtI5lDxuM2gZe6WFIotPAhbaeYlU4to5cuarF1QrcZ/lwrLaCJl66JBocYZnrNlvm2+MBCTmUymPrYZVbjdlr/BxlMjmNmNI3SAAAAAElFTkSuQmCC' + +export const selectionIcon: string = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAABGdBTUEAALGPC/xhBQAAAOVJREFUOBGtVMENwzAIjKP++2026ETdpv10iy7WFbqFyyW6GBywLCv5gI+Dw2Bluj1znuSjhb99Gkn6QILDY2imo60p8nsnc9bEo3+QJ+AKHfMdZHnl78wyTnyHZD53Zzx73MRSgYvnqgCUHj6gwdck7Zsp1VOrz0Uz8NbKunzAW+Gu4fYW28bUYutYlzSa7B84Fh7d1kjLwhcSdYAYrdkMQVpsBr5XgDGuXwQfQr0y9zwLda+DUYXLaGKdd2ZTtvbolaO87pdo24hP7ov16N0zArH1ur3iwJpXxm+v7oAJNR4JEP8DoAuSFEkYH7cAAAAASUVORK5CYII=' + +export const deleteMenuIcon = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAVhJREFUOE/VlLFKA0EQhmewEazshVjGJlgKWkSLNAGdveMqEayt4hMY9AXyCIKVhuy/TSwCFnZ2Jp1vIRaWZmTCBc6LF86ooNvcz+38383tzw7TnCUi68xcyZao6ksIYVhk46KNFPZIRKNcjX2k473vfOadB6wzcxtAPWsUEWHmVv79tIadc4GIDub9etk9Zj4t7LAsJF/3ARhFUcsKis4nbxaRTWY+BjDx2coD2ylw8ozjuNrr9Z5Mm3mabhRFW977BxGZOedCoHMuIaITALvOuT0i6gCoxXFcU9Vb7/3aIsAbABacAe9MG3A8Ho9MLwI8BCC/BTwHsJN2aJ1Xv9vh/wL+nVCuACz/WMqqehRC2Degql6EELbTlK8BbHwpZbtuSZIsdbvdN9ONRmNlMBi8mm42m6v9fv+5LLCiqpdlpo2q2nBw2dk4c5dV9SyF3ZeBEtEwO23eAUh+SIoWcATkAAAAAElFTkSuQmCC' diff --git a/examples/feature-examples/src/pages/extensions/bpmn/tips.ts b/examples/feature-examples/src/pages/extensions/bpmn/tips.ts new file mode 100644 index 000000000..d48555258 --- /dev/null +++ b/examples/feature-examples/src/pages/extensions/bpmn/tips.ts @@ -0,0 +1,85 @@ +import LogicFlow from '@logicflow/core' +import NodeData = LogicFlow.NodeData + +const nodeWidth = 100 +const nodeHeight = 80 + +export interface ITipsProps { + lf: LogicFlow +} + +export class Tips { + static pluginName = 'tips' + + private lf: LogicFlow + tipsWrap: HTMLDivElement + container?: HTMLElement + isDragging: boolean = false + currentData: any + isCurrentLeaveId?: string + + constructor({ lf }: ITipsProps) { + this.lf = lf + const tipsWrap = document.createElement('div') + tipsWrap.className = 'custom-tips' + this.tipsWrap = tipsWrap + } + + render(lf: LogicFlow, container: HTMLElement) { + this.container = container + this.container.appendChild(this.tipsWrap) + + // TODO: 解决 lf 事件监听 callback 函数的类型定义问题(是否需要统一,比如 {data: NodeData | EdgeData | undefined, ev: MouseEvent | xxxDOMEvent}) + this.lf.on('node:mouseenter', ({ data }: any) => { + const model = this.lf.graphModel.getNodeModelById(data.id) + // 没有model可以认为是fakernode, 也就是正在外部拖入的节点。 + if (!model) return + this.showTip(data) + }) + this.lf.on('node:mouseleave', ({ data }: any) => { + const model = this.lf.graphModel.getNodeModelById(data.id) + // 没有model可以认为是fakernode, 也就是正在外部拖入的节点。 + if (!model) return + this.isCurrentLeaveId = data.id + setTimeout(() => { + if (this.isCurrentLeaveId === data.id) { + this.hideTips() + } + }, 200) + }) + this.lf.on('node:dragstart', () => { + this.isDragging = true + this.hideTips() + }) + this.lf.on('node:drop', ({ data }: any) => { + this.isDragging = false + this.showTip(data) + }) + this.tipsWrap.addEventListener('click', () => { + this.currentData && lf.graphModel.deleteNode(this.currentData.id) + this.hideTips() + }) + this.tipsWrap.addEventListener('mouseenter', () => { + this.isCurrentLeaveId = undefined + }) + this.tipsWrap.addEventListener('mouseleave', () => { + this.hideTips() + }) + } + + showTip(data: NodeData) { + if (this.isDragging) return + this.currentData = data + const [x, y] = this.lf.graphModel.transformModel.CanvasPointToHtmlPoint([ + data.x + nodeWidth / 2 + 4, + data.y - nodeHeight / 2, + ]) + this.tipsWrap.style.display = 'block' + this.tipsWrap.style.top = `${y}px` + this.tipsWrap.style.left = `${x}px` + } + + hideTips() { + this.tipsWrap.style.display = 'none' + } +} diff --git a/examples/feature-examples/src/pages/extensions/bpmn/util.ts b/examples/feature-examples/src/pages/extensions/bpmn/util.ts new file mode 100644 index 000000000..9221b4f16 --- /dev/null +++ b/examples/feature-examples/src/pages/extensions/bpmn/util.ts @@ -0,0 +1,14 @@ +export function download(filename: string, text: string) { + const element = document.createElement('a') + element.setAttribute( + 'href', + 'data:text/plain;charset=utf-8,' + encodeURIComponent(text), + ) + element.setAttribute('download', filename) + + element.style.display = 'none' + document.body.appendChild(element) + + element.click() + document.body.removeChild(element) +} diff --git a/examples/feature-examples/src/pages/extensions/control/index.less b/examples/feature-examples/src/pages/extensions/control/index.less new file mode 100644 index 000000000..8eb833cff --- /dev/null +++ b/examples/feature-examples/src/pages/extensions/control/index.less @@ -0,0 +1,5 @@ +.viewport { + position: relative; + height: 80vh; + overflow: hidden; +} diff --git a/examples/feature-examples/src/pages/extensions/control/index.tsx b/examples/feature-examples/src/pages/extensions/control/index.tsx new file mode 100644 index 000000000..1a317dd69 --- /dev/null +++ b/examples/feature-examples/src/pages/extensions/control/index.tsx @@ -0,0 +1,132 @@ +import LogicFlow from '@logicflow/core' +import { Control } from '@logicflow/extension' + +import { Button, Card } from 'antd' +import { useEffect, useRef } from 'react' +import styles from './index.less' + +import '@logicflow/core/es/index.css' +import '@logicflow/extension/es/index.css' + +const config: Partial = { + isSilentMode: false, + stopScrollGraph: true, + stopZoomGraph: true, + style: { + rect: { + rx: 5, + ry: 5, + strokeWidth: 2, + }, + circle: { + fill: '#f5f5f5', + stroke: '#666', + }, + ellipse: { + fill: '#dae8fc', + stroke: '#6c8ebf', + }, + polygon: { + fill: '#d5e8d4', + stroke: '#82b366', + }, + diamond: { + fill: '#ffe6cc', + stroke: '#d79b00', + }, + text: { + color: '#b85450', + fontSize: 12, + }, + }, +} + +const data = { + nodes: [ + { + id: '1', + type: 'rect', + x: 150, + y: 100, + text: '矩形', + }, + { + id: '2', + type: 'circle', + x: 350, + y: 100, + text: '圆形', + }, + { + id: '3', + type: 'ellipse', + x: 550, + y: 100, + text: '椭圆', + }, + { + id: '4', + type: 'polygon', + x: 150, + y: 250, + text: '多边形', + }, + { + id: '5', + type: 'diamond', + x: 350, + y: 250, + text: '菱形', + }, + { + id: '6', + type: 'text', + x: 550, + y: 250, + text: '纯文本节点', + }, + { + id: '7', + type: 'html', + x: 150, + y: 400, + text: 'html节点', + }, + ], +} + +export default function ControlExtension() { + const lfRef = useRef() + const containerRef = useRef(null) + useEffect(() => { + if (!lfRef.current) { + const lf = new LogicFlow({ + ...config, + container: containerRef.current as HTMLElement, + // container: document.querySelector('#graph') as HTMLElement, + grid: { + size: 10, + }, + plugins: [Control], + }) + + lf.render(data) + lfRef.current = lf + } + }, []) + + return ( + + +
+
+ ) +} diff --git a/examples/feature-examples/src/pages/extensions/menu/index.less b/examples/feature-examples/src/pages/extensions/menu/index.less new file mode 100644 index 000000000..8eb833cff --- /dev/null +++ b/examples/feature-examples/src/pages/extensions/menu/index.less @@ -0,0 +1,5 @@ +.viewport { + position: relative; + height: 80vh; + overflow: hidden; +} diff --git a/examples/feature-examples/src/pages/extensions/menu/index.tsx b/examples/feature-examples/src/pages/extensions/menu/index.tsx new file mode 100644 index 000000000..5e8f3b6f8 --- /dev/null +++ b/examples/feature-examples/src/pages/extensions/menu/index.tsx @@ -0,0 +1,125 @@ +import LogicFlow from '@logicflow/core' +import { Menu } from '@logicflow/extension' + +import { Card } from 'antd' +import { useEffect, useRef } from 'react' +import styles from './index.less' + +import '@logicflow/core/es/index.css' +import '@logicflow/extension/es/index.css' + +const config: Partial = { + isSilentMode: false, + stopScrollGraph: true, + stopZoomGraph: true, + style: { + rect: { + rx: 5, + ry: 5, + strokeWidth: 2, + }, + circle: { + fill: '#f5f5f5', + stroke: '#666', + }, + ellipse: { + fill: '#dae8fc', + stroke: '#6c8ebf', + }, + polygon: { + fill: '#d5e8d4', + stroke: '#82b366', + }, + diamond: { + fill: '#ffe6cc', + stroke: '#d79b00', + }, + text: { + color: '#b85450', + fontSize: 12, + }, + }, +} + +const data = { + nodes: [ + { + id: '1', + type: 'rect', + x: 150, + y: 100, + text: '矩形', + }, + { + id: '2', + type: 'circle', + x: 350, + y: 100, + text: '圆形', + }, + { + id: '3', + type: 'ellipse', + x: 550, + y: 100, + text: '椭圆', + }, + { + id: '4', + type: 'polygon', + x: 150, + y: 250, + text: '多边形', + }, + { + id: '5', + type: 'diamond', + x: 350, + y: 250, + text: '菱形', + }, + { + id: '6', + type: 'text', + x: 550, + y: 250, + text: '纯文本节点', + }, + { + id: '7', + type: 'html', + x: 150, + y: 400, + text: 'html节点', + }, + ], +} + +export default function MenuExtension() { + const lfRef = useRef() + const containerRef = useRef(null) + useEffect(() => { + console.log('Menu --->>>', Menu) + + if (!lfRef.current) { + const lf = new LogicFlow({ + ...config, + container: containerRef.current as HTMLElement, + // container: document.querySelector('#graph') as HTMLElement, + grid: { + size: 10, + }, + plugins: [Menu], + }) + + lf.render(data) + lfRef.current = lf + } + }, []) + + return ( + +
+
+ ) +} diff --git a/examples/feature-examples/src/pages/graph/index.tsx b/examples/feature-examples/src/pages/graph/index.tsx index 8b622cb06..d9bd5f2cd 100644 --- a/examples/feature-examples/src/pages/graph/index.tsx +++ b/examples/feature-examples/src/pages/graph/index.tsx @@ -111,7 +111,7 @@ export default function BasicNode() { const registerEvents = (lf: LogicFlow) => { lf.on('history:change', () => { const data = lf.getGraphData() - console.log(data) + console.log('history:change', data) }) } @@ -248,8 +248,9 @@ export default function BasicNode() { const lf = lfRef.current if (lf) { const data = lf.getGraphData() - console.log(data) + console.log('current graph data', data) const refreshData = LogicFlowUtil.refreshGraphId(data) + console.log('after refresh graphId', data) lf.render(refreshData) } } diff --git a/examples/feature-examples/src/pages/nodes/custom/ellipse/index.less b/examples/feature-examples/src/pages/nodes/custom/ellipse/index.less new file mode 100644 index 000000000..47d207082 --- /dev/null +++ b/examples/feature-examples/src/pages/nodes/custom/ellipse/index.less @@ -0,0 +1,5 @@ +.viewport { + position: relative; + height: 90vh; + overflow: hidden; +} diff --git a/examples/feature-examples/src/pages/nodes/custom/ellipse/index.tsx b/examples/feature-examples/src/pages/nodes/custom/ellipse/index.tsx new file mode 100644 index 000000000..e15ede6ee --- /dev/null +++ b/examples/feature-examples/src/pages/nodes/custom/ellipse/index.tsx @@ -0,0 +1,334 @@ +import CustomEllipse from '@/components/nodes/custom-ellipse' +import LogicFlow from '@logicflow/core' +import { Button, Card } from 'antd' +import { useEffect, useRef } from 'react' +import styles from './index.less' + +import '@logicflow/core/es/index.css' + +const config: Partial = { + isSilentMode: false, + stopScrollGraph: true, + stopZoomGraph: true, + style: { + rect: { + width: 100, + height: 50, + rx: 2, + ry: 2, + }, + }, +} + +export default function RectNode() { + const lfRef = useRef() + const containerRef = useRef(null) + useEffect(() => { + if (!lfRef.current && containerRef.current) { + const lf = new LogicFlow({ + ...config, + container: containerRef.current as HTMLElement, + // container: document.querySelector('#graph') as HTMLElement, + grid: { + size: 10, + }, + }) + + lf.register(CustomEllipse) + lf.render({}) + + // row 1 + lf.addNode({ + id: '10', + type: 'customEllipse', + x: 150, + y: 70, + text: 'ellipse', + properties: { + rx: 60, + ry: 30, + }, + }) + + lf.addNode({ + id: '11', + type: 'customEllipse', + x: 350, + y: 70, + text: 'Ellipse', + properties: { + rx: 60, + ry: 30, + style: { + fill: '#efdbff', + stroke: '#9254de', + }, + }, + }) + + lf.addNode({ + id: '12', + type: 'customEllipse', + x: 550, + y: 70, + text: 'Ellipse', + properties: { + rx: 60, + ry: 30, + style: { + fill: '#f8cecc', + stroke: '#b85450', + }, + }, + }) + + lf.addNode({ + id: '13', + type: 'customEllipse', + x: 730, + y: 70, + text: 'Ellipse', + properties: { + rx: 30, + ry: 30, + style: { + fill: '#ffe6cc', + stroke: '#d79b00', + }, + textStyle: { + textAnchor: 'middle', + dominantBaseline: 'middle', + }, + }, + }) + + // row 2 + lf.addNode({ + id: '20', + type: 'customEllipse', + x: 150, + y: 200, + text: 'Ellipse', + properties: { + rx: 60, + ry: 30, + refX: -25, + refY: -10, + }, + }) + + lf.addNode({ + id: '21', + type: 'customEllipse', + x: 350, + y: 200, + text: 'Ellipse', + properties: { + rx: 60, + ry: 30, + refX: -25, + style: { + fill: '#efdbff', + stroke: '#9254de', + }, + }, + }) + + lf.addNode({ + id: '22', + type: 'customEllipse', + x: 550, + y: 200, + text: 'Ellipse', + properties: { + rx: 60, + ry: 30, + refX: -25, + refY: 10, + style: { + fill: '#f8cecc', + stroke: '#b85450', + }, + }, + }) + + lf.addNode({ + id: '23', + type: 'customEllipse', + x: 730, + y: 200, + text: 'Ellipse', + properties: { + rx: 30, + ry: 30, + refY: -40, + style: { + fill: '#ffe6cc', + stroke: '#d79b00', + }, + textStyle: { + textAnchor: 'middle', + dominantBaseline: 'middle', + }, + }, + }) + + // row 3 + lf.addNode({ + id: '30', + type: 'customEllipse', + x: 150, + y: 330, + text: 'Ellipse', + properties: { + rx: 60, + ry: 30, + refY: -10, + }, + }) + + lf.addNode({ + id: '31', + type: 'customEllipse', + x: 350, + y: 330, + text: 'Ellipse', + properties: { + rx: 60, + ry: 30, + style: { + fill: '#efdbff', + stroke: '#9254de', + }, + }, + }) + + lf.addNode({ + id: '32', + type: 'customEllipse', + x: 550, + y: 330, + text: 'Ellipse', + properties: { + rx: 60, + ry: 30, + refY: 10, + style: { + fill: '#f8cecc', + stroke: '#b85450', + }, + }, + }) + + lf.addNode({ + id: '33', + type: 'customEllipse', + x: 730, + y: 330, + text: 'Ellipse', + properties: { + rx: 30, + ry: 30, + refX: 50, + style: { + fill: '#ffe6cc', + stroke: '#d79b00', + }, + textStyle: { + textAnchor: 'middle', + dominantBaseline: 'middle', + }, + }, + }) + + // row 4 + lf.addNode({ + id: '40', + type: 'customEllipse', + x: 150, + y: 460, + text: 'Ellipse', + properties: { + rx: 60, + ry: 30, + refX: 25, + refY: -10, + }, + }) + + lf.addNode({ + id: '41', + type: 'customEllipse', + x: 350, + y: 460, + text: 'Ellipse', + properties: { + rx: 60, + ry: 30, + refX: 25, + style: { + fill: '#efdbff', + stroke: '#9254de', + }, + }, + }) + + lf.addNode({ + id: '42', + type: 'customEllipse', + x: 550, + y: 460, + text: 'Ellipse', + properties: { + rx: 60, + ry: 30, + refX: 25, + refY: 10, + style: { + fill: '#f8cecc', + stroke: '#b85450', + }, + }, + }) + + lf.addNode({ + id: '43', + type: 'customEllipse', + x: 730, + y: 460, + text: 'Ellipse', + properties: { + rx: 30, + ry: 30, + refY: 40, + style: { + fill: '#ffe6cc', + stroke: '#d79b00', + }, + textStyle: { + textAnchor: 'middle', + dominantBaseline: 'middle', + }, + }, + }) + + lfRef.current = lf + } + }, []) + + return ( + + +
+
+ ) +} diff --git a/examples/feature-examples/src/pages/nodes/custom/html/data.ts b/examples/feature-examples/src/pages/nodes/custom/html/data.ts new file mode 100644 index 000000000..e2f3b0f4d --- /dev/null +++ b/examples/feature-examples/src/pages/nodes/custom/html/data.ts @@ -0,0 +1,17 @@ +const data = { + nodes: [ + { + id: '1', + type: 'customHtml', + x: 300, + y: 150, + properties: { + name: 'hello', + body: 'world', + }, + }, + ], + edges: [], +} + +export default data diff --git a/examples/feature-examples/src/pages/nodes/custom/html/index.less b/examples/feature-examples/src/pages/nodes/custom/html/index.less new file mode 100644 index 000000000..47d207082 --- /dev/null +++ b/examples/feature-examples/src/pages/nodes/custom/html/index.less @@ -0,0 +1,5 @@ +.viewport { + position: relative; + height: 90vh; + overflow: hidden; +} diff --git a/examples/feature-examples/src/pages/nodes/custom/html/index.tsx b/examples/feature-examples/src/pages/nodes/custom/html/index.tsx new file mode 100644 index 000000000..3cb8ffe60 --- /dev/null +++ b/examples/feature-examples/src/pages/nodes/custom/html/index.tsx @@ -0,0 +1,48 @@ +import { CustomHtml } from '@/components/nodes/custom-html' +import LogicFlow from '@logicflow/core' +import '@logicflow/core/es/index.css' + +import { Card } from 'antd' +import { useEffect, useRef } from 'react' +import data from './data' +import styles from './index.less' +import './style.css' + +const config: Partial = { + isSilentMode: false, + stopScrollGraph: true, + stopZoomGraph: true, +} + +export default function BasicNode() { + const lfRef = useRef() + const containerRef = useRef(null) + useEffect(() => { + if (!lfRef.current) { + const lf = new LogicFlow({ + ...config, + container: containerRef.current as HTMLElement, + // container: document.querySelector('#graph') as HTMLElement, + grid: { + size: 10, + }, + }) + + lf.register(CustomHtml) + + lf.render(data) + lf.on('custom:button-click', (model: any) => { + lf.setProperties(model.id, { + body: 'LogicFlow', + }) + }) + lfRef.current = lf + } + }, []) + + return ( + +
+
+ ) +} diff --git a/examples/feature-examples/src/pages/nodes/custom/html/style.css b/examples/feature-examples/src/pages/nodes/custom/html/style.css new file mode 100644 index 000000000..68690fff7 --- /dev/null +++ b/examples/feature-examples/src/pages/nodes/custom/html/style.css @@ -0,0 +1,43 @@ +.uml-wrapper { + background: #efdbff; + width: 100%; + height: 100%; + border-radius: 10px; + border: 2px solid #9254de; + box-sizing: border-box; +} + +.uml-btn { + width: 32px; + min-width: 32px; + color: #fff; + background-color: #9254de; + border: 1px solid #1a223f; + border-radius: 4px; + outline: none; + cursor: pointer; +} + +.uml-btn:hover { + color: #fff; + background-color: #a780d7; +} + +.uml-head { + text-align: center; + line-height: 30px; + font-size: 16px; + font-weight: bold; +} + +.uml-body { + border-top: 1px solid #9254de; + border-bottom: 1px solid #9254de; + padding: 5px 10px; + font-size: 12px; +} + +.uml-footer { + padding: 5px 10px; + font-size: 14px; +} diff --git a/examples/feature-examples/src/pages/nodes/custom/icon/index.less b/examples/feature-examples/src/pages/nodes/custom/icon/index.less new file mode 100644 index 000000000..47d207082 --- /dev/null +++ b/examples/feature-examples/src/pages/nodes/custom/icon/index.less @@ -0,0 +1,5 @@ +.viewport { + position: relative; + height: 90vh; + overflow: hidden; +} diff --git a/examples/feature-examples/src/pages/nodes/custom/icon/index.tsx b/examples/feature-examples/src/pages/nodes/custom/icon/index.tsx new file mode 100644 index 000000000..ef57f8acd --- /dev/null +++ b/examples/feature-examples/src/pages/nodes/custom/icon/index.tsx @@ -0,0 +1,94 @@ +import { CustomIcon } from '@/components/nodes/custom-html' +import LogicFlow from '@logicflow/core' +import '@logicflow/core/es/index.css' + +import { Card } from 'antd' +import { useEffect, useRef } from 'react' +import styles from './index.less' + +const config: Partial = { + isSilentMode: false, + stopScrollGraph: true, + stopZoomGraph: true, +} + +const data = { + nodes: [ + { + id: '1', + type: 'customIcon', + x: 150, + y: 100, + text: '心形❤', + properties: { + width: 80, + height: 80, + style: { + fill: '#d75a4a', + stroke: '#9254de', + path: 'M 39.7599939046071 14.139130434782608 C 42.99207598923147 5.808695652173912 50.36826332097323 0 58.944481129679474 0 C 70.49677452125768 0 78.83298623457102 10.74782608695652 79.87301264794026 23.54782608695652 C 79.87301264794026 23.54782608695652 80.43302687052369 26.730434782608697 79.20099558084013 32.45217391304348 C 77.5049525067303 40.243478260869566 73.5208513232082 47.18260869565217 68.16071519276679 52.469565217391306 L 39.7599939046071 80 L 11.839284807233199 52.452173913043474 C 6.47914867679179 47.18260869565217 2.49504749326967 40.243478260869566 0.7990044191598517 32.45217391304348 C -0.43302687052369576 26.730434782608697 0.12698735205973488 23.54782608695652 0.12698735205973488 23.54782608695652 C 1.167013765428963 10.74782608695652 9.503225478742316 0 21.055518870320512 0 C 29.631736679026766 0 36.52791181998273 5.808695652173912 39.7599939046071 14.139130434782608 Z', + }, + }, + }, + { + id: '2', + type: 'customIcon', + x: 350, + y: 100, + text: '星星✨', + properties: { + width: 80, + height: 80, + style: { + fill: '#ed8a19', + stroke: '#9254de', + path: 'M 43.86933445808668 2.512856514404972 L 52.8801968616723 21.621036259359443 C 53.51429458636906 22.94726608640651 54.732429689076 23.872136886847233 56.134119396300434 24.08154159638098 L 76.3084391109949 27.15281066954261 C 79.84603694351371 27.69377283583813 81.24772665073813 32.23087487573599 78.69464896972221 34.830983352446694 L 64.09371451946772 49.71616812180392 C 63.07582080350713 50.76319166947266 62.608590901099 52.24647502867004 62.85889263453193 53.71230799540627 L 66.29636977367754 74.72258051862562 C 66.8970939339166 78.40461332792735 63.20931506133804 81.21412651417181 60.05551322008307 79.46908726805725 L 42.00041484845411 69.55726435012652 C 40.74890618128944 68.85924865168069 39.247095780691836 68.85924865168069 37.995587113527165 69.55726435012652 L 19.940488741898204 79.46908726805725 C 16.786686900643236 81.21412651417181 13.098908028064676 78.40461332792735 13.69963218830372 74.72258051862562 L 17.137109327449345 53.71230799540627 C 17.38741106088228 52.24647502867004 16.920181158474136 50.74574127701151 15.902287442513536 49.71616812180392 L 1.3013529922590603 34.830983352446694 C -1.2517246887568652 32.23087487573599 0.16665180069642677 27.69377283583813 3.6875628509863634 27.15281066954261 L 23.861882565680837 24.08154159638098 C 25.280259055134128 23.872136886847233 26.481707375612206 22.94726608640651 27.115805100308975 21.621036259359443 L 36.143354286123454 2.512856514404972 C 37.71191181563651 -0.8376188381349908 42.28409014634477 -0.8376188381349908 43.86933445808668 2.512856514404972 Z', + }, + }, + }, + { + id: '3', + type: 'customIcon', + x: 550, + y: 100, + text: '星星✨', + properties: { + width: 80, + height: 80, + style: { + fill: '#eb2f96', + stroke: '#9254de', + path: 'M 79.45600000000002 0.36341192933293687 C 79.12 0.08649114358208143 78.656 -0.044681860194639565 78.19200000000001 0.013617252595014213 L 28.592 5.858103309757805 C 27.808 5.945551978942286 27.200000000000003 6.572267441431064 27.200000000000003 7.301006351301736 L 27.200000000000003 16.075022826144632 L 27.200000000000003 22.211004447255693 L 27.200000000000003 60.14915209512289 C 24.432000000000002 57.248771233837616 20.096000000000004 55.38319962456869 15.200000000000001 55.38319962456869 C 6.816000000000001 55.38319962456869 0 60.89246578319098 0 67.68431242318563 C 0 74.49073384137772 6.816000000000001 80 15.200000000000001 80 C 23.600000000000005 80 30.400000000000002 74.49073384137772 30.400000000000002 67.68431242318563 C 30.400000000000002 67.20334474267099 30.368 66.73695184035377 30.304 66.25598415983913 C 30.368 66.110236377865 30.400000000000002 65.93533903949603 30.400000000000002 65.76044170112706 L 30.400000000000002 23.493584928628078 L 76.80000000000001 18.01346832640062 L 76.80000000000001 45.57437389770944 C 74.03200000000001 42.68856781462158 69.69600000000001 40.808421427155245 64.8 40.808421427155245 C 56.41600000000001 40.808421427155245 49.599999999999994 46.317687585777534 49.599999999999994 53.109534225772194 C 49.599999999999994 59.915955643964274 56.41600000000001 65.42522180258656 64.8 65.42522180258656 C 73.2 65.42522180258656 80 59.915955643964274 80 53.109534225772194 C 80 52.97836122199548 79.98400000000001 52.84718821821875 79.98400000000001 52.71601521444204 C 79.98400000000001 52.6868656580472 80 52.64314132345496 80 52.61399176706014 L 80 16.366518390092903 L 80 10.230536768981839 L 80 1.4565202941389452 C 80 1.0338517264139553 79.808 0.6403327150837924 79.45600000000002 0.36341192933293687 Z', + }, + }, + }, + ], +} + +export default function BasicNode() { + const lfRef = useRef() + const containerRef = useRef(null) + useEffect(() => { + if (!lfRef.current) { + const lf = new LogicFlow({ + ...config, + container: containerRef.current as HTMLElement, + // container: document.querySelector('#graph') as HTMLElement, + grid: { + size: 10, + }, + }) + + lf.register(CustomIcon) + + lf.render(data) + lfRef.current = lf + } + }, []) + + return ( + +
+
+ ) +} diff --git a/examples/feature-examples/src/pages/nodes/custom/image/Cloud.tsx b/examples/feature-examples/src/pages/nodes/custom/image/Cloud.tsx new file mode 100644 index 000000000..15e7d2ffd --- /dev/null +++ b/examples/feature-examples/src/pages/nodes/custom/image/Cloud.tsx @@ -0,0 +1,14 @@ +import { CustomImage } from '@/components/nodes/custom-html' + +// 云形状的图片节点 +class CloudImageNode extends CustomImage.view { + getImageHref = () => { + return 'https://dpubstatic.udache.com/static/dpubimg/0oqFX1nvbD/cloud.png' + } +} + +export default { + type: 'imageCloud', + view: CloudImageNode, + model: CustomImage.model, +} diff --git a/examples/feature-examples/src/pages/nodes/custom/image/index.less b/examples/feature-examples/src/pages/nodes/custom/image/index.less new file mode 100644 index 000000000..47d207082 --- /dev/null +++ b/examples/feature-examples/src/pages/nodes/custom/image/index.less @@ -0,0 +1,5 @@ +.viewport { + position: relative; + height: 90vh; + overflow: hidden; +} diff --git a/examples/feature-examples/src/pages/nodes/custom/image/index.tsx b/examples/feature-examples/src/pages/nodes/custom/image/index.tsx new file mode 100644 index 000000000..c28893dae --- /dev/null +++ b/examples/feature-examples/src/pages/nodes/custom/image/index.tsx @@ -0,0 +1,91 @@ +import LogicFlow from '@logicflow/core' +import '@logicflow/core/es/index.css' +import CloudImageNode from './Cloud' + +import { Card } from 'antd' +import { useEffect, useRef } from 'react' +import styles from './index.less' + +const config: Partial = { + isSilentMode: false, + stopScrollGraph: true, + stopZoomGraph: true, +} + +const data = { + nodes: [ + { + id: '1', + type: 'imageCloud', + x: 150, + y: 100, + text: '心形 ❤️', + properties: { + width: 80, + height: 60, + style: { + fill: '#d75a4a', + stroke: '#9254de', + }, + }, + }, + { + id: '2', + type: 'imageCloud', + x: 350, + y: 100, + text: '星星 ✨', + properties: { + width: 80, + height: 60, + style: { + fill: '#ed8a19', + stroke: '#9254de', + }, + }, + }, + { + id: '3', + type: 'imageCloud', + x: 550, + y: 100, + text: '音符 🎵', + properties: { + width: 80, + height: 60, + style: { + fill: '#eb2f96', + stroke: '#9254de', + }, + }, + }, + ], +} + +export default function BasicNode() { + const lfRef = useRef() + const containerRef = useRef(null) + useEffect(() => { + if (!lfRef.current) { + const lf = new LogicFlow({ + ...config, + container: containerRef.current as HTMLElement, + // container: document.querySelector('#graph') as HTMLElement, + grid: { + size: 10, + }, + }) + + lf.register(CloudImageNode) + + lf.render(data) + lfRef.current = lf + } + }, []) + + return ( + +
+
+ ) +} diff --git a/examples/feature-examples/src/pages/nodes/custom/rect/index.less b/examples/feature-examples/src/pages/nodes/custom/rect/index.less new file mode 100644 index 000000000..47d207082 --- /dev/null +++ b/examples/feature-examples/src/pages/nodes/custom/rect/index.less @@ -0,0 +1,5 @@ +.viewport { + position: relative; + height: 90vh; + overflow: hidden; +} diff --git a/examples/feature-examples/src/pages/nodes/custom/rect/index.tsx b/examples/feature-examples/src/pages/nodes/custom/rect/index.tsx new file mode 100644 index 000000000..f4e5d2c34 --- /dev/null +++ b/examples/feature-examples/src/pages/nodes/custom/rect/index.tsx @@ -0,0 +1,325 @@ +import CustomRect from '@/components/nodes/custom-rect' +import LogicFlow from '@logicflow/core' +import { Card } from 'antd' +import { useEffect, useRef } from 'react' +import styles from './index.less' + +import '@logicflow/core/es/index.css' + +const config: Partial = { + isSilentMode: false, + stopScrollGraph: true, + stopZoomGraph: true, + style: { + rect: { + width: 100, + height: 50, + rx: 2, + ry: 2, + }, + }, +} + +// const data = { +// nodes: [ +// { +// id: '10', +// type: 'rect', +// x: 150, +// y: 70, +// text: '矩形', +// }, +// { +// id: '20', +// type: 'rect', +// x: 350, +// y: 70, +// text: '矩形', +// }, +// ], +// } + +export default function RectNode() { + const lfRef = useRef() + const containerRef = useRef(null) + useEffect(() => { + if (!lfRef.current && containerRef.current) { + const lf = new LogicFlow({ + ...config, + container: containerRef.current as HTMLElement, + // container: document.querySelector('#graph') as HTMLElement, + grid: { + size: 10, + }, + }) + + lf.register(CustomRect) + lf.render({}) + + // row 1 + lf.addNode({ + id: '10', + type: 'customRect', + x: 150, + y: 70, + text: '矩形', + }) + + lf.addNode({ + id: '11', + type: 'customRect', + x: 350, + y: 70, + text: '矩形', + properties: { + style: { + fill: '#efdbff', + stroke: '#9254de', + }, + }, + }) + + lf.addNode({ + id: '12', + type: 'customRect', + x: 550, + y: 70, + text: '矩形', + properties: { + radius: 8, + style: { + fill: '#f8cecc', + stroke: '#b85450', + }, + }, + }) + + lf.addNode({ + id: '13', + type: 'customRect', + x: 730, + y: 70, + text: '矩形', + properties: { + width: 60, + height: 60, + radius: 20, + style: { + fill: '#ffe6cc', + stroke: '#d79b00', + }, + textStyle: { + textAnchor: 'middle', + dominantBaseline: 'middle', + }, + }, + }) + + // row 2 + lf.addNode({ + id: '20', + type: 'customRect', + x: 150, + y: 200, + text: '矩形', + properties: { + refX: -25, + refY: -20, + }, + }) + + lf.addNode({ + id: '21', + type: 'customRect', + x: 350, + y: 200, + text: '矩形', + properties: { + refX: -25, + style: { + fill: '#efdbff', + stroke: '#9254de', + }, + }, + }) + + lf.addNode({ + id: '22', + type: 'customRect', + x: 550, + y: 200, + text: '矩形', + properties: { + radius: 8, + refX: -25, + refY: 20, + style: { + fill: '#f8cecc', + stroke: '#b85450', + }, + }, + }) + + lf.addNode({ + id: '23', + type: 'customRect', + x: 730, + y: 200, + text: '矩形', + properties: { + width: 60, + height: 60, + radius: 20, + refY: -40, + style: { + fill: '#ffe6cc', + stroke: '#d79b00', + }, + textStyle: { + textAnchor: 'middle', + dominantBaseline: 'middle', + }, + }, + }) + + // row 3 + lf.addNode({ + id: '30', + type: 'customRect', + x: 150, + y: 330, + text: '矩形', + properties: { + refY: -20, + }, + }) + + lf.addNode({ + id: '31', + type: 'customRect', + x: 350, + y: 330, + text: '矩形', + properties: { + style: { + fill: '#efdbff', + stroke: '#9254de', + }, + }, + }) + + lf.addNode({ + id: '32', + type: 'customRect', + x: 550, + y: 330, + text: '矩形', + properties: { + radius: 8, + refY: 20, + style: { + fill: '#f8cecc', + stroke: '#b85450', + }, + }, + }) + + lf.addNode({ + id: '33', + type: 'customRect', + x: 730, + y: 330, + text: '矩形', + properties: { + width: 60, + height: 60, + radius: 20, + refX: 48, + style: { + fill: '#ffe6cc', + stroke: '#d79b00', + }, + textStyle: { + textAnchor: 'middle', + dominantBaseline: 'middle', + }, + }, + }) + + // row 4 + lf.addNode({ + id: '40', + type: 'customRect', + x: 150, + y: 460, + text: '矩形', + properties: { + refX: 20, + refY: -20, + }, + }) + + lf.addNode({ + id: '41', + type: 'customRect', + x: 350, + y: 460, + text: '矩形', + properties: { + refX: 20, + style: { + fill: '#efdbff', + stroke: '#9254de', + }, + }, + }) + + lf.addNode({ + id: '42', + type: 'customRect', + x: 550, + y: 460, + text: '矩形', + properties: { + radius: 8, + refX: 20, + refY: 20, + style: { + fill: '#f8cecc', + stroke: '#b85450', + }, + }, + }) + + lf.addNode({ + id: '43', + type: 'customRect', + x: 730, + y: 460, + text: '矩形', + properties: { + width: 60, + height: 60, + radius: 20, + refY: 40, + style: { + fill: '#ffe6cc', + stroke: '#d79b00', + }, + textStyle: { + textAnchor: 'middle', + dominantBaseline: 'middle', + }, + }, + }) + + lfRef.current = lf + } + }, []) + + return ( + +
+
+ ) +} diff --git a/examples/feature-examples/src/pages/nodes/custom/theme/index.less b/examples/feature-examples/src/pages/nodes/custom/theme/index.less new file mode 100644 index 000000000..8eb833cff --- /dev/null +++ b/examples/feature-examples/src/pages/nodes/custom/theme/index.less @@ -0,0 +1,5 @@ +.viewport { + position: relative; + height: 80vh; + overflow: hidden; +} diff --git a/examples/feature-examples/src/pages/nodes/custom/theme/index.tsx b/examples/feature-examples/src/pages/nodes/custom/theme/index.tsx new file mode 100644 index 000000000..07bc5a6c7 --- /dev/null +++ b/examples/feature-examples/src/pages/nodes/custom/theme/index.tsx @@ -0,0 +1,120 @@ +import LogicFlow from '@logicflow/core' +import '@logicflow/core/es/index.css' + +import { Card } from 'antd' +import { useEffect, useRef } from 'react' +import styles from './index.less' +import theme from './theme' + +const config: Partial = { + isSilentMode: false, + stopScrollGraph: true, + stopZoomGraph: true, +} + +const data = { + nodes: [ + { + id: '1', + type: 'rect', + x: 150, + y: 100, + text: '矩形', + }, + { + id: '2', + type: 'circle', + x: 350, + y: 100, + text: '圆形', + }, + { + id: '3', + type: 'ellipse', + x: 550, + y: 100, + text: '椭圆', + }, + { + id: '4', + type: 'polygon', + x: 150, + y: 250, + text: '多边形', + }, + { + id: '5', + type: 'diamond', + x: 350, + y: 250, + text: '菱形', + }, + { + id: '6', + type: 'text', + x: 550, + y: 250, + text: '纯文本节点', + }, + { + id: '7', + type: 'html', + x: 150, + y: 400, + text: 'html节点', + }, + ], + edges: [ + // TODO: 解决线段中间弯折问题 + { + sourceNodeId: '1', + targetNodeId: '3', + startPoint: { + x: 150, + y: 60, + }, + endPoint: { + x: 550, + y: 50, + }, + text: '333', + type: 'polyline', + }, + { + sourceNodeId: '3', + targetNodeId: '4', + type: 'line', + }, + { + sourceNodeId: '3', + targetNodeId: '5', + type: 'bezier', + }, + ], +} + +export default function BasicNode() { + const lfRef = useRef() + const containerRef = useRef(null) + useEffect(() => { + if (!lfRef.current) { + const lf = new LogicFlow({ + ...config, + container: containerRef.current as HTMLElement, + // container: document.querySelector('#graph') as HTMLElement, + grid: { + size: 10, + }, + }) + lf.setTheme(theme) + lf.render(data) + lfRef.current = lf + } + }, []) + + return ( + +
+
+ ) +} diff --git a/examples/feature-examples/src/pages/nodes/custom/theme/theme.ts b/examples/feature-examples/src/pages/nodes/custom/theme/theme.ts new file mode 100644 index 000000000..aa0bac9ff --- /dev/null +++ b/examples/feature-examples/src/pages/nodes/custom/theme/theme.ts @@ -0,0 +1,91 @@ +import LogicFlow from '@logicflow/core' + +const theme: Partial = { + baseNode: { + fill: 'rgb(255, 230, 204)', + stroke: '#00796b', + strokeDasharray: '3,3', + }, + rect: { + fill: '#FFFFFF', + strokeDasharray: '10, 1', + className: 'custom-cls', + radius: 30, + }, + circle: { + r: 10, + fill: '#9a9b9c', + }, + diamond: { + fill: '#238899', + }, + ellipse: { + strokeWidth: 3, + }, + polygon: { + strokeDasharray: 'none', + }, + anchor: { + r: 3, + fill: '#9a9312', + hover: { + fill: '#d84315', + }, + }, + nodeText: { + fontSize: 16, + color: '#d84315', + overflowMode: 'autoWrap', + }, + baseEdge: { + strokeWidth: 1, + strokeDasharray: '3,3', + }, + edgeText: { + fontSize: 12, + textWidth: 60, + overflowMode: 'autoWrap', + background: { + fill: '#919810', + }, + }, + polyline: { + offset: 20, + strokeDasharray: 'none', + strokeWidth: 2, + }, + bezier: { + stroke: '#d84315', + adjustLine: { + strokeWidth: 2, + stroke: '#d84315', + }, + adjustAnchor: { + stroke: 'blue', + fill: '#00796b', + }, + }, + arrow: { + offset: 10, // 箭头长度 + verticalLength: 3, // 箭头垂直于边的距离 + fill: 'none', + stroke: '#00796b', + }, + anchorLine: { + stroke: '#d84315', + }, + snapline: { + stroke: '#d84315', + }, + edgeAdjust: { + r: 10, + }, + outline: { + stroke: '#d84315', + hover: { + stroke: '#00796b', + }, + }, +} + +export default theme diff --git a/examples/vue3-memory-leak/.gitignore b/examples/vue3-memory-leak/.gitignore new file mode 100644 index 000000000..4108b33e7 --- /dev/null +++ b/examples/vue3-memory-leak/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/vue3-memory-leak/README.md b/examples/vue3-memory-leak/README.md new file mode 100644 index 000000000..74e213ed1 --- /dev/null +++ b/examples/vue3-memory-leak/README.md @@ -0,0 +1,11 @@ +# LogicFlow-NodeRed + +LogicFlow 仿 NodeRed 风格流程图。 + +## 效果 + +![node-red](https://cdn.jsdelivr.net/gh/Logic-Flow/static@latest/core/node-red.png) + +## codesandbox地址 + +[https://codesandbox.io/s/logicflow-node-red-vue3-u2c3zk?file=/src/components/FlowChart.vue](https://codesandbox.io/s/logicflow-node-red-vue3-u2c3zk?file=/src/components/FlowChart.vue) diff --git a/examples/vue3-memory-leak/auto-imports.d.ts b/examples/vue3-memory-leak/auto-imports.d.ts new file mode 100644 index 000000000..7d31d5ce0 --- /dev/null +++ b/examples/vue3-memory-leak/auto-imports.d.ts @@ -0,0 +1,4 @@ +// Generated by 'unplugin-auto-import' +// We suggest you to commit this file into source control +declare global {} +export {} diff --git a/examples/vue3-memory-leak/components.d.ts b/examples/vue3-memory-leak/components.d.ts new file mode 100644 index 000000000..394120b5d --- /dev/null +++ b/examples/vue3-memory-leak/components.d.ts @@ -0,0 +1,19 @@ +// generated by unplugin-vue-components +// We suggest you to commit this file into source control +// Read more: https://github.com/vuejs/vue-next/pull/3399 + +declare module 'vue' { + export interface GlobalComponents { + ElButton: (typeof import('element-plus/es'))['ElButton'] + ElCollapse: (typeof import('element-plus/es'))['ElCollapse'] + ElCollapseItem: (typeof import('element-plus/es'))['ElCollapseItem'] + ElInput: (typeof import('element-plus/es'))['ElInput'] + FlowChart: (typeof import('./src/components/FlowChart.vue'))['default'] + Palette: (typeof import('./src/components/node-red/tools/Palette.vue'))['default'] + Setting: (typeof import('./src/components/node-red/tools/Setting.vue'))['default'] + Undefined: (typeof import('./src/components/index.vue'))['default'] + VueNode: (typeof import('./src/components/node-red/nodes/VueNode.vue'))['default'] + } +} + +export {} diff --git a/examples/vue3-memory-leak/index.html b/examples/vue3-memory-leak/index.html new file mode 100644 index 000000000..0938c4504 --- /dev/null +++ b/examples/vue3-memory-leak/index.html @@ -0,0 +1,13 @@ + + + + + + + LogicFlow + + +
+ + + diff --git a/examples/vue3-memory-leak/package.json b/examples/vue3-memory-leak/package.json new file mode 100644 index 000000000..c5972fa7e --- /dev/null +++ b/examples/vue3-memory-leak/package.json @@ -0,0 +1,22 @@ +{ + "name": "vue3-memory-leak", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@logicflow/core": "workspace:latest", + "@logicflow/extension": "workspace:latest", + "element-plus": "^2.0.4", + "vue": "^3.2.25" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^2.2.0", + "unplugin-auto-import": "^0.6.1", + "unplugin-vue-components": "^0.17.21", + "vite": "^2.8.0" + } +} diff --git a/examples/vue3-memory-leak/public/favicon.ico b/examples/vue3-memory-leak/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..df36fcfb72584e00488330b560ebcf34a41c64c2 GIT binary patch literal 4286 zcmds*O-Phc6o&64GDVCEQHxsW(p4>LW*W<827=Unuo8sGpRux(DN@jWP-e29Wl%wj zY84_aq9}^Am9-cWTD5GGEo#+5Fi2wX_P*bo+xO!)p*7B;iKlbFd(U~_d(U?#hLj56 zPhFkj-|A6~Qk#@g^#D^U0XT1cu=c-vu1+SElX9NR;kzAUV(q0|dl0|%h|dI$%VICy zJnu2^L*Te9JrJMGh%-P79CL0}dq92RGU6gI{v2~|)p}sG5x0U*z<8U;Ij*hB9z?ei z@g6Xq-pDoPl=MANPiR7%172VA%r)kevtV-_5H*QJKFmd;8yA$98zCxBZYXTNZ#QFk2(TX0;Y2dt&WitL#$96|gJY=3xX zpCoi|YNzgO3R`f@IiEeSmKrPSf#h#Qd<$%Ej^RIeeYfsxhPMOG`S`Pz8q``=511zm zAm)MX5AV^5xIWPyEu7u>qYs?pn$I4nL9J!=K=SGlKLXpE<5x+2cDTXq?brj?n6sp= zphe9;_JHf40^9~}9i08r{XM$7HB!`{Ys~TK0kx<}ZQng`UPvH*11|q7&l9?@FQz;8 zx!=3<4seY*%=OlbCbcae?5^V_}*K>Uo6ZWV8mTyE^B=DKy7-sdLYkR5Z?paTgK-zyIkKjIcpyO z{+uIt&YSa_$QnN_@t~L014dyK(fOOo+W*MIxbA6Ndgr=Y!f#Tokqv}n<7-9qfHkc3 z=>a|HWqcX8fzQCT=dqVbogRq!-S>H%yA{1w#2Pn;=e>JiEj7Hl;zdt-2f+j2%DeVD zsW0Ab)ZK@0cIW%W7z}H{&~yGhn~D;aiP4=;m-HCo`BEI+Kd6 z={Xwx{TKxD#iCLfl2vQGDitKtN>z|-AdCN|$jTFDg0m3O`WLD4_s#$S literal 0 HcmV?d00001 diff --git a/examples/vue3-memory-leak/public/images/delay.svg b/examples/vue3-memory-leak/public/images/delay.svg new file mode 100644 index 000000000..75dd9cbd3 --- /dev/null +++ b/examples/vue3-memory-leak/public/images/delay.svg @@ -0,0 +1 @@ + diff --git a/examples/vue3-memory-leak/public/images/fetch.svg b/examples/vue3-memory-leak/public/images/fetch.svg new file mode 100644 index 000000000..63679c8ad --- /dev/null +++ b/examples/vue3-memory-leak/public/images/fetch.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/vue3-memory-leak/public/images/function.svg b/examples/vue3-memory-leak/public/images/function.svg new file mode 100644 index 000000000..8103ffce9 --- /dev/null +++ b/examples/vue3-memory-leak/public/images/function.svg @@ -0,0 +1 @@ + diff --git a/examples/vue3-memory-leak/public/images/start.svg b/examples/vue3-memory-leak/public/images/start.svg new file mode 100644 index 000000000..efb26891b --- /dev/null +++ b/examples/vue3-memory-leak/public/images/start.svg @@ -0,0 +1 @@ + diff --git a/examples/vue3-memory-leak/public/images/swap.svg b/examples/vue3-memory-leak/public/images/swap.svg new file mode 100644 index 000000000..e004e8b97 --- /dev/null +++ b/examples/vue3-memory-leak/public/images/swap.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/vue3-memory-leak/public/images/switch.svg b/examples/vue3-memory-leak/public/images/switch.svg new file mode 100644 index 000000000..79afabf28 --- /dev/null +++ b/examples/vue3-memory-leak/public/images/switch.svg @@ -0,0 +1 @@ + diff --git a/examples/vue3-memory-leak/src/App.vue b/examples/vue3-memory-leak/src/App.vue new file mode 100644 index 000000000..6c461306c --- /dev/null +++ b/examples/vue3-memory-leak/src/App.vue @@ -0,0 +1,9 @@ + + + diff --git a/examples/vue3-memory-leak/src/assets/logo.png b/examples/vue3-memory-leak/src/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f3d2503fc2a44b5053b0837ebea6e87a2d339a43 GIT binary patch literal 6849 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 +import { ref } from 'vue' +import LogicFlow from '@logicflow/core' +import '@logicflow/core/es/index.css' +import NodeRedExtension from './node-red/index' +// import Setting from './node-red/tools/Setting' + +import './node-red/style.css' + +export default { + setup() { + const count = ref(0) + const currentNode = ref(null) + return { + count, + currentNode, + } + }, + + mounted() { + this.lf = new LogicFlow({ + container: this.$refs.container, + grid: { + visible: true, + type: 'mesh', + size: 10, + config: { + color: '#eeeeee', + }, + }, + // adjustEdge: false, + hoverOutline: false, + edgeSelectedOutline: false, + keyboard: { + enabled: true, + }, + // keyboard: true, + plugins: [NodeRedExtension], + }) + this.lf.render({ + // nodes: [ + // { + // id: 'node_1', + // type: 'rect', + // x: 220, + // y: 200, + // text: 'start' + // }, + // // { + // // id: 'node_123_1', + // // type: 'vue-html', + // // x: 720, + // // y: 400, + // // text: '2', + // // properties: { + // // t: 3 + // // } + // // }, + // { + // id: 'node_2', + // type: 'rect', + // x: 420, + // y: 200, + // text: 'fetch data' + // }, + // { + // id: 'node_3', + // type: 'rect', + // x: 620, + // y: 200, + // text: 'function' + // }, + // { + // id: 'node_4', + // type: 'rect', + // x: 420, + // y: 300, + // text: 'function' + // }, + // { + // id: 'node_5', + // type: 'rect', + // x: 820, + // y: 200, + // text: 'switch' + // }, + // { + // id: 'node_6', + // type: 'rect', + // x: 1020, + // y: 200, + // text: 'function' + // }, + // { + // id: 'node_7', + // type: 'rect', + // x: 1020, + // y: 300, + // text: 'function' + // } + // ], + nodes: [ + { + id: 'node_1', + type: 'start-node', + x: 220, + y: 200, + text: 'start', + }, + { + id: 'node_byj_111', + type: 'rect', + x: 500, + y: 0, + text: 'start', + }, + { + id: 'node_123_1', + type: 'vue-html', + x: 720, + y: 400, + text: '2', + properties: { + t: 3, + }, + }, + // { + // id: "node_123_1", + // type: "html", + // x: 720, + // y: 400, + // text: "2", + // properties: { + // t: 3, + // }, + // }, + { + id: 'node_2', + type: 'fetch-node', + x: 420, + y: 200, + text: 'fetch data', + }, + { + id: 'node_3', + type: 'function-node', + x: 620, + y: 200, + text: 'function', + }, + { + id: 'node_4', + type: 'delay-node', + x: 420, + y: 300, + text: 'function', + }, + { + id: 'node_5', + type: 'switch-node', + x: 820, + y: 200, + text: 'switch', + }, + { + id: 'node_6', + type: 'function-node', + x: 1020, + y: 200, + text: 'function', + }, + { + id: 'node_7', + type: 'function-node', + x: 1020, + y: 300, + text: 'function', + }, + ], + edges: [ + { + type: 'flow-link', + sourceNodeId: 'node_1', + targetNodeId: 'node_2', + }, + { + type: 'flow-link', + sourceNodeId: 'node_2', + targetNodeId: 'node_3', + }, + { + type: 'flow-link', + sourceNodeId: 'node_3', + startPoint: { + x: 680, + y: 200, + }, + targetNodeId: 'node_4', + }, + { + type: 'flow-link', + sourceNodeId: 'node_4', + startPoint: { + x: 360, + y: 300, + }, + targetNodeId: 'node_2', + }, + { + type: 'flow-link', + sourceNodeId: 'node_3', + targetNodeId: 'node_5', + }, + { + type: 'flow-link', + sourceNodeId: 'node_5', + targetNodeId: 'node_6', + }, + { + type: 'flow-link', + sourceNodeId: 'node_5', + targetNodeId: 'node_7', + }, + ], + }) + // this.lf.render() + this.lf.on('node-red:start', () => { + // todo: 让流程跑起来 + console.log('我要开始执行流程了') + }) + this.lf.on('vue-node:click', (data) => { + this.lf.setProperties(data.id, { + t: ++data.val, + }) + }) + this.lf.on('node:click', ({ data }) => { + this.currentNode = data + }) + this.lf.on('blank:click', ({ data }) => { + this.currentNode = null + }) + }, + + unmounted() { + console.log('unmounted ===>>>', this.lf) + this.lf.render({}) + }, + + methods: { + changeStyle(style) { + this.lf.setProperties(this.currentNode.id, { + style, + }) + }, + }, + + beforeUnmount() { + console.log('this.lf', this.lf.viewMap) + this.lf.viewMap.forEach((value, key) => { + console.log('viewMap item', key, value.extendKey) + }) + // this.lf.clearData(); + // this.lf.container = null; + // this.lf.plugins = null; + // this.lf.tool = null; + // this.lf.history = null; + // this.lf.dnd = null; + // this.lf.keyboard = null; + // this.lf.graphModel = null; + // this.lf.extension.NodeRedExtension.destory(); + // this.lf = null; + }, +} + + + + + diff --git a/examples/vue3-memory-leak/src/components/engine/index.js b/examples/vue3-memory-leak/src/components/engine/index.js new file mode 100644 index 000000000..3ce9e0cb1 --- /dev/null +++ b/examples/vue3-memory-leak/src/components/engine/index.js @@ -0,0 +1,4 @@ +class Engine { + run() {} + initFlow() {} +} diff --git a/examples/vue3-memory-leak/src/components/index.vue b/examples/vue3-memory-leak/src/components/index.vue new file mode 100644 index 000000000..a8c4e9134 --- /dev/null +++ b/examples/vue3-memory-leak/src/components/index.vue @@ -0,0 +1,25 @@ + + diff --git a/examples/vue3-memory-leak/src/components/node-red/FlowLink.js b/examples/vue3-memory-leak/src/components/node-red/FlowLink.js new file mode 100644 index 000000000..4723e2bf6 --- /dev/null +++ b/examples/vue3-memory-leak/src/components/node-red/FlowLink.js @@ -0,0 +1,17 @@ +import { BezierEdge, BezierEdgeModel } from '@logicflow/core' + +class FlowLinkModel extends BezierEdgeModel { + getEdgeStyle() { + const style = super.getEdgeStyle() + style.strokeWidth = 3 + style.stroke = this.isSelected ? '#ff7f0e' : '#999' + return style + } +} +class FlowLink extends BezierEdge {} + +export default { + type: 'flow-link', + view: FlowLink, + model: FlowLinkModel, +} diff --git a/examples/vue3-memory-leak/src/components/node-red/index.js b/examples/vue3-memory-leak/src/components/node-red/index.js new file mode 100644 index 000000000..2252d0762 --- /dev/null +++ b/examples/vue3-memory-leak/src/components/node-red/index.js @@ -0,0 +1,41 @@ +import FunctionNode from './nodes/FunctionNode' +import SwitchNode from './nodes/SwitchNode' +import StartNode from './nodes/StartNode' +import FetchNode from './nodes/FetchNode' +import DelayNode from './nodes/DelayNode' +import FlowLink from './FlowLink' +import Palette from './tools/Palette.vue' +import VueHtmlNode from './nodes/VueHtmlNode' + +class NodeRedExtension { + static pluginName = 'NodeRedExtension' + + constructor({ lf }) { + lf.register(FunctionNode) + lf.register(SwitchNode) + lf.register(StartNode) + lf.register(FetchNode) + lf.register(DelayNode) + lf.register(VueHtmlNode) + + lf.register(FlowLink) + lf.setDefaultEdgeType('flow-link') + } + + render(lf, domOverlay) { + console.log('node-red-extension') + // const node = document.createElement('div') + // node.className = 'node-red-palette' + // domOverlay.appendChild(node) + // this.app.mount(node) + } + + destory() { + console.log('destory') + this.app.unmount() + this.app = null + console.log('this.app', this.app) + } +} + +export default NodeRedExtension diff --git a/examples/vue3-memory-leak/src/components/node-red/nodes/BaseNode.js b/examples/vue3-memory-leak/src/components/node-red/nodes/BaseNode.js new file mode 100644 index 000000000..f34fa8a19 --- /dev/null +++ b/examples/vue3-memory-leak/src/components/node-red/nodes/BaseNode.js @@ -0,0 +1,185 @@ +import { RectNode, RectNodeModel, h } from '@logicflow/core' +import { getBytesLength } from '../util' + +class RedNodeModel extends RectNodeModel { + componentWillUnmount() { + console.log('RedNodeM componentWillUnmount') + } + + /** + * 初始化 + */ + initNodeData(data) { + super.initNodeData(data) + this.width = 100 + this.height = 30 + this.radius = 5 + // this.text.editable = false; + this.text.x = this.x + 10 + this.iconPosition = '' // icon位置,left表示左边,'right'表示右边 + this.defaultFill = '#a6bbcf' + } + + getData() { + const data = super.getData() + data.properties.ui = 'node-red' + return data + } + + /** + * 动态设置初始化数据 + */ + setAttributes() { + if (this.text.value) { + let width = 30 + getBytesLength(this.text.value) * 9 + width = Math.ceil(width / 20) * 20 + if (width < 100) { + width = 100 + } + this.width = width + } + } + + updateText(val) { + super.updateText(val) + this.setAttributes() + } + + /** + * 重写节点样式 + */ + getNodeStyle() { + const style = super.getNodeStyle() + const dataStyle = this.properties.style || {} + if (this.isSelected) { + style.strokeWidth = Number(dataStyle.borderWidth) || 2 + style.stroke = dataStyle.borderColor || '#ff7f0e' + } else { + style.strokeWidth = Number(dataStyle.borderWidth) || 1 + style.stroke = dataStyle.borderColor || '#999' + } + style.fill = dataStyle.backgroundColor || this.defaultFill + return style + } + + /** + * 重写定义锚点 + */ + getDefaultAnchor() { + const { x, y, id, width, height } = this + const anchors = [ + { + x: x + width / 2, + y: y, + id: `${id}_right`, + type: 'right', + }, + { + x: x - width / 2, + y: y, + id: `${id}_left`, + type: 'left', + }, + ] + return anchors + } + + /** + * + */ + getOutlineStyle() { + const style = super.getOutlineStyle() + style.stroke = 'transparent' + style.hover.stroke = 'transparent' + return style + } +} + +class RedNode extends RectNode { + /** + * 1.1.7版本后支持在view中重写锚点形状。 + * 重写锚点新增 + */ + getAnchorShape(anchorData) { + const { x, y, type } = anchorData + return h('rect', { + x: x - 5, + y: y - 5, + width: 10, + height: 10, + className: 'custom-anchor', + }) + } + + getIcon() { + return null + } + + getShape() { + const { text, x, y, width, height, radius } = this.props.model + const style = this.props.model.getNodeStyle() + return h( + 'g', + { + className: 'lf-red-node', + }, + [ + h('rect', { + ...style, + x: x - width / 2, + y: y - height / 2, + width, + height, + rx: radius, + ry: radius, + }), + h( + 'g', + { + style: 'pointer-events: none;', + transform: `translate(${x}, ${y})`, + }, + [ + h('rect', { + x: -width / 2, + y: -height / 2, + width: 30, + height: 30, + fill: '#000', + fillOpacity: 0.05, + stroke: 'none', + }), + this.getIcon(), + h('path', { + d: `M ${30 - width / 2} ${1 - height / 2} l 0 28`, + stroke: '#000', + strokeOpacity: 0.1, + strokeWidth: 1, + }), + ], + ), + ], + ) + // return h( + // 'g', + // { + // className: 'lf-red-node', + // }, + // [h('rect', { + // ...style, + // x: x - width / 2, + // y: y - height / 2, + // width, + // height, + // rx: radius, + // ry: radius, + // })], + // ) + } +} + +export default { + type: 'red-node', + model: RedNodeModel, + view: RedNode, +} diff --git a/examples/vue3-memory-leak/src/components/node-red/nodes/DelayNode.js b/examples/vue3-memory-leak/src/components/node-red/nodes/DelayNode.js new file mode 100644 index 000000000..46574775c --- /dev/null +++ b/examples/vue3-memory-leak/src/components/node-red/nodes/DelayNode.js @@ -0,0 +1,28 @@ +import { h } from '@logicflow/core' +import BaseNode from './BaseNode' + +class DelayNode extends BaseNode.view { + getIcon() { + const { width, height } = this.props.model + return h('image', { + width: 30, + height: 30, + x: -width / 2, + y: -height / 2, + href: 'images/delay.svg', + }) + } +} + +class DelayNodeModel extends BaseNode.model { + initNodeData(data) { + super.initNodeData(data) + this.defaultFill = 'rgb(230, 224, 248)' + } +} + +export default { + type: 'delay-node', + model: DelayNodeModel, + view: DelayNode, +} diff --git a/examples/vue3-memory-leak/src/components/node-red/nodes/FetchNode.js b/examples/vue3-memory-leak/src/components/node-red/nodes/FetchNode.js new file mode 100644 index 000000000..e550e43e6 --- /dev/null +++ b/examples/vue3-memory-leak/src/components/node-red/nodes/FetchNode.js @@ -0,0 +1,28 @@ +import { h } from '@logicflow/core' +import BaseNode from './BaseNode' + +class FetchNode extends BaseNode.view { + getIcon() { + const { width, height } = this.props.model + return h('image', { + width: 30, + height: 30, + x: -width / 2, + y: -height / 2, + href: 'images/fetch.svg', + }) + } +} + +class FetchNodeModel extends BaseNode.model { + initNodeData(data) { + super.initNodeData(data) + this.defaultFill = 'rgb(231, 231, 174)' + } +} + +export default { + type: 'fetch-node', + model: FetchNodeModel, + view: FetchNode, +} diff --git a/examples/vue3-memory-leak/src/components/node-red/nodes/FunctionNode.js b/examples/vue3-memory-leak/src/components/node-red/nodes/FunctionNode.js new file mode 100644 index 000000000..28d6568b6 --- /dev/null +++ b/examples/vue3-memory-leak/src/components/node-red/nodes/FunctionNode.js @@ -0,0 +1,33 @@ +import { h } from '@logicflow/core' +import BaseNode from './BaseNode' + +class FunctionNode extends BaseNode.view { + componentWillUnmount() { + console.log('in custom compoenntWillUnmount') + super.componentWillUnmount() + } + + getIcon() { + const { width, height } = this.props.model + return h('image', { + width: 30, + height: 30, + x: -width / 2, + y: -height / 2, + href: 'images/function.svg', + }) + } +} + +class FunctionNodeModel extends BaseNode.model { + initNodeData(data) { + super.initNodeData(data) + this.defaultFill = 'rgb(253, 208, 162)' + } +} + +export default { + type: 'function-node', + model: FunctionNodeModel, + view: FunctionNode, +} diff --git a/examples/vue3-memory-leak/src/components/node-red/nodes/StartNode.js b/examples/vue3-memory-leak/src/components/node-red/nodes/StartNode.js new file mode 100644 index 000000000..525c1db86 --- /dev/null +++ b/examples/vue3-memory-leak/src/components/node-red/nodes/StartNode.js @@ -0,0 +1,49 @@ +import { h } from '@logicflow/core' +import BaseNode from './BaseNode' + +class StartNode extends BaseNode.view { + getIcon() { + const { model, graphModel } = this.props + const { width, height } = model + return h('image', { + width: 30, + height: 30, + x: -width / 2, + y: -height / 2, + className: 'node-red-start', + href: 'images/start.svg', + onClick: () => { + graphModel.eventCenter.emit('node-red:start') + }, + }) + } +} + +class StartNodeModel extends BaseNode.model { + /** + * 重写定义锚点 + */ + getDefaultAnchor() { + const { x, y, id, width } = this + const anchors = [ + { + x: x + width / 2, + y: y, + id: `${id}_right`, + type: 'right', + }, + ] + return anchors + } + + initNodeData(data) { + super.initNodeData(data) + this.defaultFill = 'rgb(166, 187, 207)' + } +} + +export default { + type: 'start-node', + model: StartNodeModel, + view: StartNode, +} diff --git a/examples/vue3-memory-leak/src/components/node-red/nodes/SwitchNode.js b/examples/vue3-memory-leak/src/components/node-red/nodes/SwitchNode.js new file mode 100644 index 000000000..8f387d42b --- /dev/null +++ b/examples/vue3-memory-leak/src/components/node-red/nodes/SwitchNode.js @@ -0,0 +1,28 @@ +import { h } from '@logicflow/core' +import BaseNode from './BaseNode' + +class SwitchNode extends BaseNode.view { + getIcon() { + const { width, height } = this.props.model + return h('image', { + width: 30, + height: 30, + x: -width / 2, + y: -height / 2, + href: 'images/switch.svg', + }) + } +} + +class SwitchNodeModel extends BaseNode.model { + initNodeData(data) { + super.initNodeData(data) + this.defaultFill = 'rgb(226, 217, 110)' + } +} + +export default { + type: 'switch-node', + model: SwitchNodeModel, + view: SwitchNode, +} diff --git a/examples/vue3-memory-leak/src/components/node-red/nodes/VueHtmlNode.js b/examples/vue3-memory-leak/src/components/node-red/nodes/VueHtmlNode.js new file mode 100644 index 000000000..49b244f8f --- /dev/null +++ b/examples/vue3-memory-leak/src/components/node-red/nodes/VueHtmlNode.js @@ -0,0 +1,73 @@ +import { HtmlNode, HtmlNodeModel } from '@logicflow/core' +import { createApp, ref, h } from 'vue' +import VueNode from './VueNode.vue' + +class VueHtmlNode extends HtmlNode { + constructor(props) { + super(props) + this.isMounted = false + this.r = h(VueNode, { + properties: props.model.getProperties(), + text: props.model.inputData, + onBtnClick: (i) => { + this.r.component.props.text = String( + Number(this.r.component.props.text) + Number(i), + ) + }, + }) + this.app = createApp({ + render: () => this.r, + }) + } + + componentWillUnmount() { + this.rootEl.innerHTML = '' + } + + setHtml(rootEl) { + if (!this.isMounted) { + this.isMounted = true + const node = document.createElement('div') + this.app.mount(node) + rootEl.appendChild(node) + } else { + this.r.component.props.properties = this.props.model.getProperties() + } + } + + getText() { + return null + } +} + +class VueHtmlNodeModel extends HtmlNodeModel { + setAttributes() { + this.width = 300 + this.height = 100 + this.text.editable = false + this.inputData = this.text.value + } + + getOutlineStyle() { + const style = super.getOutlineStyle() + style.stroke = 'none' + style.hover.stroke = 'none' + return style + } + + getDefaultAnchor() { + return [] + } + + getData() { + const data = super.getData() + data.text.value = this.inputData + return data + } +} + +export default { + type: 'vue-html', + model: VueHtmlNodeModel, + view: VueHtmlNode, +} diff --git a/examples/vue3-memory-leak/src/components/node-red/nodes/VueNode.vue b/examples/vue3-memory-leak/src/components/node-red/nodes/VueNode.vue new file mode 100644 index 000000000..b7d4f768e --- /dev/null +++ b/examples/vue3-memory-leak/src/components/node-red/nodes/VueNode.vue @@ -0,0 +1,56 @@ + + + + diff --git a/examples/vue3-memory-leak/src/components/node-red/readme.md b/examples/vue3-memory-leak/src/components/node-red/readme.md new file mode 100644 index 000000000..1b08033b5 --- /dev/null +++ b/examples/vue3-memory-leak/src/components/node-red/readme.md @@ -0,0 +1,4 @@ +# 说明 + +参考node-red样式,以logicflow插件的方式实现。 + diff --git a/examples/vue3-memory-leak/src/components/node-red/style.css b/examples/vue3-memory-leak/src/components/node-red/style.css new file mode 100644 index 000000000..6fa52a36d --- /dev/null +++ b/examples/vue3-memory-leak/src/components/node-red/style.css @@ -0,0 +1,26 @@ +.custom-anchor { + stroke: #999; + stroke-width: 1; + fill: #d9d9d9; + cursor: crosshair; + rx: 3; + ry: 3; +} + +.custom-anchor:hover { + fill: #ff7f0e; + stroke: #ff7f0e; +} + +.node-red-palette { + width: 150px; + overflow: hidden; + position: absolute; + left: 0px; + top: 0px; +} + +.node-red-start { + cursor: pointer; + pointer-events: all; +} diff --git a/examples/vue3-memory-leak/src/components/node-red/tools/Palette.vue b/examples/vue3-memory-leak/src/components/node-red/tools/Palette.vue new file mode 100644 index 000000000..c6c30c764 --- /dev/null +++ b/examples/vue3-memory-leak/src/components/node-red/tools/Palette.vue @@ -0,0 +1,155 @@ + + + + diff --git a/examples/vue3-memory-leak/src/components/node-red/tools/Setting.vue b/examples/vue3-memory-leak/src/components/node-red/tools/Setting.vue new file mode 100644 index 000000000..f065a2857 --- /dev/null +++ b/examples/vue3-memory-leak/src/components/node-red/tools/Setting.vue @@ -0,0 +1,84 @@ + + + + + diff --git a/examples/vue3-memory-leak/src/components/node-red/util.js b/examples/vue3-memory-leak/src/components/node-red/util.js new file mode 100644 index 000000000..32f256c94 --- /dev/null +++ b/examples/vue3-memory-leak/src/components/node-red/util.js @@ -0,0 +1,18 @@ +/* 求字符串的字节长度 */ +export const getBytesLength = (word) => { + if (!word) { + return 0 + } + let totalLength = 0 + for (let i = 0; i < word.length; i++) { + const c = word.charCodeAt(i) + if (word.match(/[A-Z]/)) { + totalLength += 1.5 + } else if ((c >= 0x0001 && c <= 0x007e) || (c >= 0xff60 && c <= 0xff9f)) { + totalLength += 1 + } else { + totalLength += 1.8 + } + } + return totalLength +} diff --git a/examples/vue3-memory-leak/src/main.js b/examples/vue3-memory-leak/src/main.js new file mode 100644 index 000000000..20608cd39 --- /dev/null +++ b/examples/vue3-memory-leak/src/main.js @@ -0,0 +1,9 @@ +import { createApp } from 'vue' +// import ElementPlus from 'element-plus' +import App from './App.vue' +import './style.css' + +const app = createApp(App) +// app.use(ElementPlus) + +app.mount('#app') diff --git a/examples/vue3-memory-leak/src/style.css b/examples/vue3-memory-leak/src/style.css new file mode 100644 index 000000000..2930a572f --- /dev/null +++ b/examples/vue3-memory-leak/src/style.css @@ -0,0 +1,11 @@ +html, +body { + padding: 0; + margin: 0; + width: 100%; + height: 100%; +} +#app { + width: 100%; + height: 100%; +} diff --git a/examples/vue3-memory-leak/vite.config.js b/examples/vue3-memory-leak/vite.config.js new file mode 100644 index 000000000..2908d63b3 --- /dev/null +++ b/examples/vue3-memory-leak/vite.config.js @@ -0,0 +1,24 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import AutoImport from 'unplugin-auto-import/vite' +import Components from 'unplugin-vue-components/vite' +import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' + +// https://vitejs.dev/config/ +export default defineConfig({ + // 在codesandbox中需要用443来保证开发时不会重复刷新 + // server: { + // hmr: { + // port: 443 + // } + // }, + plugins: [ + vue(), + AutoImport({ + resolvers: [ElementPlusResolver()], + }), + Components({ + resolvers: [ElementPlusResolver()], + }), + ], +}) diff --git a/packages/core/src/LogicFlow.tsx b/packages/core/src/LogicFlow.tsx index 564b7c912..cb3b740cd 100644 --- a/packages/core/src/LogicFlow.tsx +++ b/packages/core/src/LogicFlow.tsx @@ -194,8 +194,8 @@ export class LogicFlow { type, } // 为了能让后来注册的可以继承前面注册的 - // 例如我注册一个”开始节点“ - // 然后我再想注册一个”立即开始节点“ + // 例如我注册一个“开始节点” + // 然后我再想注册一个“立即开始节点” // 注册传递参数改为动态。 this.viewMap.forEach((component) => { const key = (component as any).extendKey @@ -689,7 +689,7 @@ export class LogicFlow { * @param id 元素的id * @param properties 自定义属性 */ - setProperties(id: string, properties: Record): void { + setProperties(id: string, properties: LogicFlow.PropertiesType): void { this.graphModel.getElement(id)?.setProperties(formatData(properties)) } @@ -702,7 +702,7 @@ export class LogicFlow { * @param id 元素的id * @returns 自定义属性 */ - getProperties(id: string): Record | undefined { + getProperties(id: string): Record | undefined { return this.graphModel.getElement(id)?.getProperties() } @@ -1270,6 +1270,7 @@ export namespace LogicFlow { [key: string]: string | undefined } + export type PropertiesType = Record export type AttributesType = Record export type EventArgsType = Record @@ -1353,7 +1354,7 @@ export namespace LogicFlow { export interface FakeNodeConfig { type: string text?: TextConfig | string - properties?: Record + properties?: PropertiesType [key: string]: unknown } @@ -1365,7 +1366,7 @@ export namespace LogicFlow { y: number text?: TextConfig | string zIndex?: number - properties?: Record + properties?: PropertiesType virtual?: boolean // 是否虚拟节点 rotate?: number @@ -1397,7 +1398,7 @@ export namespace LogicFlow { text?: TextConfig | string pointsList?: Point[] zIndex?: number - properties?: Record + properties?: PropertiesType } // TODO: 确认这种类型该如何定义(必需和非必需动态调整,优雅的处理方式) diff --git a/packages/core/src/model/edge/BaseEdgeModel.ts b/packages/core/src/model/edge/BaseEdgeModel.ts index f1480c378..914e942e6 100644 --- a/packages/core/src/model/edge/BaseEdgeModel.ts +++ b/packages/core/src/model/edge/BaseEdgeModel.ts @@ -29,8 +29,8 @@ export interface IBaseEdgeModel extends Model.BaseModel { sourceNodeId: string targetNodeId: string - startPoint: Point - endPoint: Point + startPoint?: Point + endPoint?: Point points: string pointsList: Point[] diff --git a/packages/core/src/model/edge/BezierEdgeModel.ts b/packages/core/src/model/edge/BezierEdgeModel.ts index 8aeeb13c5..24b15fbff 100644 --- a/packages/core/src/model/edge/BezierEdgeModel.ts +++ b/packages/core/src/model/edge/BezierEdgeModel.ts @@ -6,10 +6,19 @@ import { ModelType } from '../../constant' import { getBezierControlPoints, IBezierControls } from '../../util' import Point = LogicFlow.Point +import GraphModel from '../GraphModel' export class BezierEdgeModel extends BaseEdgeModel { modelType = ModelType.BEZIER_EDGE @observable path = '' + + constructor(data: LogicFlow.EdgeConfig, graphModel: GraphModel) { + super(data, graphModel) + + this.initEdgeData(data) + this.setAttributes() + } + initEdgeData(data): void { this.offset = 100 super.initEdgeData(data)