diff --git a/ui/src/views/application-workflow/index.vue b/ui/src/views/application-workflow/index.vue index 5369d2b04..9a6eac4b6 100644 --- a/ui/src/views/application-workflow/index.vue +++ b/ui/src/views/application-workflow/index.vue @@ -27,11 +27,11 @@ 添加组件 - {{ $t('common.debug')}} - {{ $t('common.save')}} + {{ $t('common.save') }} 发布 @@ -326,7 +326,7 @@ function getDetail() { saveTime.value = res.data?.update_time workflowRef.value?.clearGraphData() nextTick(() => { - workflowRef.value?.renderGraphData(detail.value.work_flow) + workflowRef.value?.render(detail.value.work_flow) }) }) } diff --git a/ui/src/workflow/common/app-node.ts b/ui/src/workflow/common/app-node.ts index 0a091df50..ade79beb8 100644 --- a/ui/src/workflow/common/app-node.ts +++ b/ui/src/workflow/common/app-node.ts @@ -3,39 +3,24 @@ import ElementPlus from 'element-plus' import * as ElementPlusIcons from '@element-plus/icons-vue' import zhCn from 'element-plus/dist/locale/zh-cn.mjs' import { HtmlResize } from '@logicflow/extension' - import { h as lh } from '@logicflow/core' import { createApp, h } from 'vue' import directives from '@/directives' import i18n from '@/locales' import { WorkflowType } from '@/enums/workflow' import { nodeDict } from '@/workflow/common/data' +import { isActive, connect, disconnect } from './teleport' class AppNode extends HtmlResize.view { isMounted - r - app - + r?: any + component: any + app: any + root?: any + VueNode: any constructor(props: any, VueNode: any) { super(props) + this.component = VueNode this.isMounted = false - this.r = h(VueNode, { - properties: props.model.properties, - nodeModel: props.model - }) - - this.app = createApp({ - render: () => this.r - }) - this.app.use(ElementPlus, { - locale: zhCn - }) - this.app.use(Components) - this.app.use(directives) - this.app.use(i18n) - for (const [key, component] of Object.entries(ElementPlusIcons)) { - this.app.component(key, component) - } - if (props.model.properties.noRender) { delete props.model.properties.noRender } else { @@ -137,13 +122,79 @@ class AppNode extends HtmlResize.view { this.isMounted = true const node = document.createElement('div') rootEl.appendChild(node) - this.app?.mount(node) + this.renderVueComponent(node) } else { if (this.r && this.r.component) { this.r.component.props.properties = this.props.model.getProperties() } } } + componentWillUnmount() { + super.componentWillUnmount() + this.unmount() + } + getComponentContainer() { + return this.root + } + protected targetId() { + return `${this.props.graphModel.flowId}:${this.props.model.id}` + } + protected renderVueComponent(root: any) { + this.unmountVueComponent() + this.root = root + const { model, graphModel } = this.props + + if (root) { + if (isActive()) { + connect(this.targetId(), this.component, root, model, graphModel) + } else { + this.r = h(this.component, { + properties: this.props.model.properties, + nodeModel: this.props.model + }) + this.app = createApp({ + render() { + return this.r + }, + provide() { + return { + getNode: () => model, + getGraph: () => graphModel + } + } + }) + + this.app.use(ElementPlus, { + locale: zhCn + }) + this.app.use(Components) + this.app.use(directives) + this.app.use(i18n) + for (const [key, component] of Object.entries(ElementPlusIcons)) { + this.app.component(key, component) + } + this.app?.mount(root) + } + } + } + + protected unmountVueComponent() { + if (this.app) { + this.app.unmount() + this.app = null + } + if (this.root) { + this.root.innerHTML = '' + } + return this.root + } + + unmount() { + if (isActive()) { + disconnect(this.targetId()) + } + this.unmountVueComponent() + } } class AppNodeModel extends HtmlResize.model { diff --git a/ui/src/workflow/common/edge.ts b/ui/src/workflow/common/edge.ts index 8093e0ac1..730891096 100644 --- a/ui/src/workflow/common/edge.ts +++ b/ui/src/workflow/common/edge.ts @@ -1,6 +1,6 @@ import { BezierEdge, BezierEdgeModel, h } from '@logicflow/core' import { createApp, h as vh } from 'vue' - +import { isActive, connect, disconnect } from './teleport' import CustomLine from './CustomLine.vue' function isMouseInElement(element: any, e: any) { const rect = element.getBoundingClientRect() @@ -15,7 +15,8 @@ const DEFAULT_WIDTH = 32 const DEFAULT_HEIGHT = 32 class CustomEdge2 extends BezierEdge { isMounted - + customLineApp?: any + root?: any constructor() { super() this.isMounted = false @@ -28,6 +29,64 @@ class CustomEdge2 extends BezierEdge { } } } + /** + * 渲染vue组件 + * @param root + */ + protected renderVueComponent(root: any) { + this.unmountVueComponent() + this.root = root + const { graphModel } = this.props + if (root) { + if (isActive()) { + connect( + this.targetId(), + CustomLine, + root, + this.props.model, + graphModel, + (node: any, graph: any) => { + return { model: node, graph } + } + ) + } else { + this.customLineApp = createApp({ + render: () => vh(CustomLine, { model: this.props.model }) + }) + this.customLineApp?.mount(root) + } + } + } + protected targetId() { + return `${this.props.graphModel.flowId}:${this.props.model.id}` + } + /** + * 组件即将卸载勾子 + */ + componentWillUnmount() { + if (super.componentWillUnmount) { + super.componentWillUnmount() + } + if (isActive()) { + console.log('unmount') + disconnect(this.targetId()) + } + this.unmountVueComponent() + } + /** + * 卸载vue + * @returns + */ + protected unmountVueComponent() { + if (this.customLineApp) { + this.customLineApp.unmount() + this.customLineApp = null + } + if (this.root) { + this.root.innerHTML = '' + } + return this.root + } getEdge() { const { model } = this.props @@ -57,14 +116,11 @@ class CustomEdge2 extends BezierEdge { height: customHeight } - const app = createApp({ - render: () => vh(CustomLine, { model: this.props.model }) - }) setTimeout(() => { const s = document.getElementById(id) if (s && !this.isMounted) { - app.mount(s) this.isMounted = true + this.renderVueComponent(s) } }, 0) diff --git a/ui/src/workflow/common/teleport.ts b/ui/src/workflow/common/teleport.ts new file mode 100644 index 000000000..6f88b414f --- /dev/null +++ b/ui/src/workflow/common/teleport.ts @@ -0,0 +1,80 @@ +import { BaseEdgeModel, BaseNodeModel, GraphModel } from '@logicflow/core' +import { defineComponent, h, reactive, isVue3, Teleport, markRaw, Fragment } from 'vue-demi' + +let active = false +const items = reactive<{ [key: string]: any }>({}) + +export function connect( + id: string, + component: any, + container: HTMLDivElement, + node: BaseNodeModel | BaseEdgeModel, + graph: GraphModel, + get_props?: any +) { + if (!get_props) { + get_props = (node: BaseNodeModel | BaseEdgeModel, graph: GraphModel) => { + return { nodeModel: node, graph } + } + } + if (active) { + items[id] = markRaw( + defineComponent({ + render: () => h(Teleport, { to: container } as any, [h(component, get_props(node, graph))]), + provide: () => ({ + getNode: () => node, + getGraph: () => graph + }) + }) + ) + } +} + +export function disconnect(id: string) { + if (active) { + delete items[id] + } +} + +export function isActive() { + return active +} + +export function getTeleport(): any { + if (!isVue3) { + throw new Error('teleport is only available in Vue3') + } + active = true + + return defineComponent({ + props: { + flowId: { + type: String, + required: true + } + }, + setup(props) { + return () => { + const children: Record[] = [] + Object.keys(items).forEach((id) => { + // https://github.com/didi/LogicFlow/issues/1768 + // 多个不同的VueNodeView都会connect注册到items中,因此items存储了可能有多个flowId流程图的数据 + // 当使用多个LogicFlow时,会创建多个flowId + 同时使用KeepAlive + // 每一次items改变,会触发不同flowId持有的setup()执行,由于每次setup()执行就是遍历items,因此存在多次重复渲染元素的问题 + // 即items[0]会在Page1的setup()执行,items[0]也会在Page2的setup()执行,从而生成两个items[0] + + // 比对当前界面显示的flowId,只更新items[当前页面flowId:nodeId]的数据 + // 比如items[0]属于Page1的数据,那么Page2无论active=true/false,都无法执行items[0] + if (id.startsWith(props.flowId)) { + children.push(items[id]) + } + }) + return h( + Fragment, + {}, + children.map((item) => h(item)) + ) + } + } + }) +} diff --git a/ui/src/workflow/index.vue b/ui/src/workflow/index.vue index f2a72b3c5..3feaf2afc 100644 --- a/ui/src/workflow/index.vue +++ b/ui/src/workflow/index.vue @@ -2,6 +2,7 @@
+