Browse Source

refactor: loop node render with sub canvas inside form (#75)

* refactor(container): loop node render with form

* refactor(container): simplified sub-canvas configuration

* refactor(container): move sub-canvas render into plugin
Louis Young 9 tháng trước cách đây
mục cha
commit
5dbb2f4b55
36 tập tin đã thay đổi với 202 bổ sung419 xóa
  1. 2 2
      apps/demo-free-layout/src/components/base-node/index.tsx
  2. 4 2
      apps/demo-free-layout/src/components/base-node/styles.tsx
  3. 0 14
      apps/demo-free-layout/src/components/container-content/index.tsx
  4. 0 1
      apps/demo-free-layout/src/components/index.ts
  5. 1 4
      apps/demo-free-layout/src/hooks/use-editor-props.tsx
  6. 8 4
      apps/demo-free-layout/src/nodes/loop/index.ts
  7. 15 0
      apps/demo-free-layout/src/nodes/loop/loop-form-render.tsx
  8. 0 32
      packages/plugins/free-container-plugin/src/container-node-render/components/background/index.tsx
  9. 0 6
      packages/plugins/free-container-plugin/src/container-node-render/components/background/style.ts
  10. 0 21
      packages/plugins/free-container-plugin/src/container-node-render/components/border/index.tsx
  11. 0 72
      packages/plugins/free-container-plugin/src/container-node-render/components/container/index.tsx
  12. 0 18
      packages/plugins/free-container-plugin/src/container-node-render/components/container/style.ts
  13. 0 13
      packages/plugins/free-container-plugin/src/container-node-render/components/form/index.tsx
  14. 0 7
      packages/plugins/free-container-plugin/src/container-node-render/components/form/style.ts
  15. 0 27
      packages/plugins/free-container-plugin/src/container-node-render/components/header/index.tsx
  16. 0 74
      packages/plugins/free-container-plugin/src/container-node-render/components/header/style.ts
  17. 0 6
      packages/plugins/free-container-plugin/src/container-node-render/components/index.ts
  18. 0 33
      packages/plugins/free-container-plugin/src/container-node-render/components/ports/index.tsx
  19. 0 1
      packages/plugins/free-container-plugin/src/container-node-render/constant.ts
  20. 0 1
      packages/plugins/free-container-plugin/src/container-node-render/hooks/index.ts
  21. 0 22
      packages/plugins/free-container-plugin/src/container-node-render/hooks/use-render-props.ts
  22. 0 4
      packages/plugins/free-container-plugin/src/container-node-render/index.ts
  23. 0 19
      packages/plugins/free-container-plugin/src/container-node-render/render.tsx
  24. 0 19
      packages/plugins/free-container-plugin/src/container-node-render/type.ts
  25. 1 1
      packages/plugins/free-container-plugin/src/index.ts
  26. 0 13
      packages/plugins/free-container-plugin/src/plugin.tsx
  27. 24 0
      packages/plugins/free-container-plugin/src/sub-canvas/components/background/index.tsx
  28. 8 0
      packages/plugins/free-container-plugin/src/sub-canvas/components/background/style.ts
  29. 19 0
      packages/plugins/free-container-plugin/src/sub-canvas/components/border/index.tsx
  30. 2 3
      packages/plugins/free-container-plugin/src/sub-canvas/components/border/style.ts
  31. 3 0
      packages/plugins/free-container-plugin/src/sub-canvas/components/index.ts
  32. 44 0
      packages/plugins/free-container-plugin/src/sub-canvas/components/render/index.tsx
  33. 6 0
      packages/plugins/free-container-plugin/src/sub-canvas/components/render/style.ts
  34. 1 0
      packages/plugins/free-container-plugin/src/sub-canvas/hooks/index.ts
  35. 62 0
      packages/plugins/free-container-plugin/src/sub-canvas/hooks/use-node-size.ts
  36. 2 0
      packages/plugins/free-container-plugin/src/sub-canvas/index.ts

+ 2 - 2
apps/demo-free-layout/src/components/base-node/index.tsx

@@ -30,10 +30,10 @@ export const BaseNode = ({ node }: { node: FlowNodeEntity }) => {
 
   return (
     <ConfigProvider getPopupContainer={getPopupContainer}>
-      <WorkflowNodeRenderer node={node}>
+      <WorkflowNodeRenderer className="flowgram-node" node={node}>
         {form?.state.invalid && <ErrorIcon />}
         <BaseNodeStyle
-          className={nodeRender.selected ? 'selected' : ''}
+          className={`flowgram-node-render ${nodeRender.selected ? 'selected' : ''}`}
           style={{
             outline: form?.state.invalid ? '1px solid red' : 'none',
           }}

+ 4 - 2
apps/demo-free-layout/src/components/base-node/styles.tsx

@@ -8,9 +8,11 @@ export const BaseNodeStyle = styled.div`
   border-radius: 8px;
   display: flex;
   flex-direction: column;
-  justify-content: center;
+  justify-content: flex-start;
   position: relative;
-  width: 360px;
+  min-width: 360px;
+  width: 100%;
+  height: 100%;
 
   &.selected {
     border: 1px solid var(--coz-stroke-hglt, #4e40e5);

+ 0 - 14
apps/demo-free-layout/src/components/container-content/index.tsx

@@ -1,14 +0,0 @@
-import { useNodeRender } from '@flowgram.ai/free-layout-editor';
-import { ContainerNodeForm } from '@flowgram.ai/free-container-plugin';
-
-import { NodeRenderContext } from '../../context';
-
-export const ContainerNodeContent = () => {
-  const nodeRender = useNodeRender();
-
-  return (
-    <NodeRenderContext.Provider value={nodeRender}>
-      <ContainerNodeForm />;
-    </NodeRenderContext.Provider>
-  );
-};

+ 0 - 1
apps/demo-free-layout/src/components/index.ts

@@ -1,4 +1,3 @@
 export * from './base-node';
 export * from './line-add-button';
 export * from './node-panel';
-export * from './container-content';

+ 1 - 4
apps/demo-free-layout/src/hooks/use-editor-props.tsx

@@ -17,7 +17,6 @@ import { createSyncVariablePlugin } from '../plugins';
 import { defaultFormMeta } from '../nodes/default-form-meta';
 import { SelectorBoxPopover } from '../components/selector-box-popover';
 import { BaseNode, LineAddButton, NodePanel } from '../components';
-import { ContainerNodeContent } from '../components';
 
 export function useEditorProps(
   initialData: FlowDocumentJSON,
@@ -201,9 +200,7 @@ export function useEditorProps(
         createFreeNodePanelPlugin({
           renderer: NodePanel,
         }),
-        createContainerNodePlugin({
-          renderContent: <ContainerNodeContent />,
-        }),
+        createContainerNodePlugin({}),
       ],
     }),
     []

+ 8 - 4
apps/demo-free-layout/src/nodes/loop/index.ts

@@ -4,10 +4,11 @@ import {
   PositionSchema,
   FlowNodeTransformData,
 } from '@flowgram.ai/free-layout-editor';
-import { ContainerNodeRenderKey } from '@flowgram.ai/free-container-plugin';
 
+import { defaultFormMeta } from '../default-form-meta';
 import { FlowNodeRegistry } from '../../typings';
 import iconLoop from '../../assets/icon-loop.jpg';
+import { LoopFormRender } from './loop-form-render';
 
 let index = 0;
 export const LoopNodeRegistry: FlowNodeRegistry = {
@@ -18,15 +19,14 @@ export const LoopNodeRegistry: FlowNodeRegistry = {
       'Used to repeatedly execute a series of tasks by setting the number of iterations and logic.',
   },
   meta: {
-    renderKey: ContainerNodeRenderKey,
     isContainer: true,
     size: {
       width: 560,
       height: 400,
     },
     padding: () => ({
-      top: 205,
-      bottom: 50,
+      top: 150,
+      bottom: 100,
       left: 100,
       right: 100,
     }),
@@ -64,6 +64,10 @@ export const LoopNodeRegistry: FlowNodeRegistry = {
       },
     };
   },
+  formMeta: {
+    ...defaultFormMeta,
+    render: LoopFormRender,
+  },
   onCreate() {
     // NOTICE: 这个函数是为了避免触发固定布局 flowDocument.addBlocksAsChildren
   },

+ 15 - 0
apps/demo-free-layout/src/nodes/loop/loop-form-render.tsx

@@ -0,0 +1,15 @@
+import { FormRenderProps, FlowNodeJSON } from '@flowgram.ai/free-layout-editor';
+import { SubCanvasRender } from '@flowgram.ai/free-container-plugin';
+
+import { FormHeader, FormContent, FormInputs, FormOutputs } from '../../form-components';
+
+export const LoopFormRender = ({ form }: FormRenderProps<FlowNodeJSON>) => (
+  <>
+    <FormHeader />
+    <FormContent>
+      <FormInputs />
+      <SubCanvasRender />
+      <FormOutputs />
+    </FormContent>
+  </>
+);

+ 0 - 32
packages/plugins/free-container-plugin/src/container-node-render/components/background/index.tsx

@@ -1,32 +0,0 @@
-import React, { type FC } from 'react';
-
-import { useNodeRender } from '@flowgram.ai/free-layout-core';
-
-import { ContainerNodeBackgroundStyle } from './style';
-
-export const ContainerNodeBackground: FC = () => {
-  const { node } = useNodeRender();
-  return (
-    <ContainerNodeBackgroundStyle
-      className="container-node-background"
-      data-flow-editor-selectable="true"
-    >
-      <svg width="100%" height="100%">
-        <pattern
-          id="container-node-dot-pattern"
-          width="20"
-          height="20"
-          patternUnits="userSpaceOnUse"
-        >
-          <circle cx="1" cy="1" r="1" stroke="#eceeef" fillOpacity="0.5" />
-        </pattern>
-        <rect
-          width="100%"
-          height="100%"
-          fill="url(#container-node-dot-pattern)"
-          data-node-panel-container={node.id}
-        />
-      </svg>
-    </ContainerNodeBackgroundStyle>
-  );
-};

+ 0 - 6
packages/plugins/free-container-plugin/src/container-node-render/components/background/style.ts

@@ -1,6 +0,0 @@
-import styled from 'styled-components';
-
-export const ContainerNodeBackgroundStyle = styled.div`
-  position: absolute;
-  inset: 56px 18px 18px;
-`;

+ 0 - 21
packages/plugins/free-container-plugin/src/container-node-render/components/border/index.tsx

@@ -1,21 +0,0 @@
-import React, { type FC } from 'react';
-
-import { useNodeRender } from '@flowgram.ai/free-layout-core';
-import { FlowNodeTransformData } from '@flowgram.ai/document';
-
-import { ContainerNodeBorderStyle } from './style';
-
-export const ContainerNodeBorder: FC = () => {
-  const { node } = useNodeRender();
-  const transformData = node.getData(FlowNodeTransformData);
-  const topWidth = Math.max(transformData.padding.top - 50, 50);
-
-  return (
-    <ContainerNodeBorderStyle
-      className="container-node-border"
-      style={{
-        borderTopWidth: topWidth,
-      }}
-    />
-  );
-};

+ 0 - 72
packages/plugins/free-container-plugin/src/container-node-render/components/container/index.tsx

@@ -1,72 +0,0 @@
-import React, { useEffect, useState, type FC, type ReactNode } from 'react';
-
-import { useNodeRender, WorkflowNodePortsData } from '@flowgram.ai/free-layout-core';
-import { FlowNodeTransformData } from '@flowgram.ai/document';
-
-import { useContainerNodeRenderProps } from '../../hooks';
-import { ContainerNodeContainerStyle } from './style';
-
-interface IContainerNodeContainer {
-  children: ReactNode | ReactNode[];
-}
-
-export const ContainerNodeContainer: FC<IContainerNodeContainer> = ({ children }) => {
-  const { node, selected, selectNode, nodeRef } = useNodeRender();
-  const nodeMeta = node.getNodeMeta();
-  const { size = { width: 300, height: 200 } } = nodeMeta;
-  const { style = {} } = useContainerNodeRenderProps();
-
-  const transform = node.getData<FlowNodeTransformData>(FlowNodeTransformData);
-  const [width, setWidth] = useState(size.width);
-  const [height, setHeight] = useState(size.height);
-
-  const updatePorts = () => {
-    const portsData = node.getData<WorkflowNodePortsData>(WorkflowNodePortsData);
-    portsData.updateDynamicPorts();
-  };
-
-  const updateSize = () => {
-    // 无子节点时
-    if (node.blocks.length === 0) {
-      setWidth(size.width);
-      setHeight(size.height);
-      return;
-    }
-    // 存在子节点时,只监听宽高变化
-    setWidth(transform.bounds.width);
-    setHeight(transform.bounds.height);
-  };
-
-  useEffect(() => {
-    const dispose = transform.onDataChange(() => {
-      updateSize();
-      updatePorts();
-    });
-    return () => dispose.dispose();
-  }, [transform, width, height]);
-
-  useEffect(() => {
-    // 初始化触发一次
-    updateSize();
-  }, []);
-
-  return (
-    <ContainerNodeContainerStyle
-      className="container-node-container"
-      style={{
-        width,
-        height,
-        outline: selected ? '1px solid var(--coz-stroke-hglt, #4E40E5)' : '',
-        ...style,
-      }}
-      ref={nodeRef}
-      data-node-selected={String(selected)}
-      onMouseDown={selectNode}
-      onClick={(e) => {
-        selectNode(e);
-      }}
-    >
-      {children}
-    </ContainerNodeContainerStyle>
-  );
-};

+ 0 - 18
packages/plugins/free-container-plugin/src/container-node-render/components/container/style.ts

@@ -1,18 +0,0 @@
-import styled from 'styled-components';
-
-export const ContainerNodeContainerStyle = styled.div`
-  display: flex;
-  align-items: flex-start;
-
-  box-sizing: border-box;
-  min-width: 400px;
-  min-height: 300px;
-
-  background-color: #f2f3f5;
-  border-radius: 8px;
-  outline: 1px solid var(--coz-stroke-plus, rgba(6, 7, 9, 15%));
-
-  .container-node-container-selected {
-    outline: 1px solid var(--coz-stroke-hglt, #4e40e5);
-  }
-`;

+ 0 - 13
packages/plugins/free-container-plugin/src/container-node-render/components/form/index.tsx

@@ -1,13 +0,0 @@
-import React, { type FC } from 'react';
-
-import { useNodeRender } from '@flowgram.ai/free-layout-core';
-
-import { ContainerNodeFormStyle } from './style';
-
-export const ContainerNodeForm: FC = () => {
-  const { form } = useNodeRender();
-  if (!form) {
-    return null;
-  }
-  return <ContainerNodeFormStyle>{form.render()}</ContainerNodeFormStyle>;
-};

+ 0 - 7
packages/plugins/free-container-plugin/src/container-node-render/components/form/style.ts

@@ -1,7 +0,0 @@
-import styled from 'styled-components';
-
-export const ContainerNodeFormStyle = styled.div`
-  background-color: #fff;
-  border-radius: 8px 8px 0 0;
-  width: 100%;
-`;

+ 0 - 27
packages/plugins/free-container-plugin/src/container-node-render/components/header/index.tsx

@@ -1,27 +0,0 @@
-import React, { ReactNode, type FC } from 'react';
-
-import { useNodeRender } from '@flowgram.ai/free-layout-core';
-
-import { ContainerNodeHeaderStyle } from './style';
-
-interface IContainerNodeHeader {
-  children?: ReactNode | ReactNode[];
-}
-
-export const ContainerNodeHeader: FC<IContainerNodeHeader> = ({ children }) => {
-  const { startDrag, onFocus, onBlur } = useNodeRender();
-
-  return (
-    <ContainerNodeHeaderStyle
-      className="container-node-header"
-      draggable={true}
-      onMouseDown={(e) => {
-        startDrag(e);
-      }}
-      onFocus={onFocus}
-      onBlur={onBlur}
-    >
-      {children}
-    </ContainerNodeHeaderStyle>
-  );
-};

+ 0 - 74
packages/plugins/free-container-plugin/src/container-node-render/components/header/style.ts

@@ -1,74 +0,0 @@
-import styled from 'styled-components';
-
-export const ContainerNodeHeaderStyle = styled.div`
-  z-index: 0;
-
-  display: flex;
-  gap: 8px;
-  align-items: center;
-  justify-content: flex-start;
-
-  width: 100%;
-  height: auto;
-
-  border-radius: 8px 8px 0 0;
-
-  .container-node-logo {
-    position: relative;
-
-    flex-shrink: 0;
-
-    width: 24px;
-    height: 24px;
-
-    border-radius: 4px;
-
-    &::after {
-      content: '';
-
-      position: absolute;
-      top: 0;
-      left: 0;
-
-      display: block;
-
-      width: 100%;
-      height: 100%;
-
-      border-radius: 4px;
-      box-shadow: inset 0 0 0 1px var(--coz-stroke-plus);
-    }
-
-    > img {
-      width: 24px;
-      height: 24px;
-      border-radius: 4px;
-    }
-  }
-
-  .container-node-title {
-    margin: 0;
-    padding: 0;
-
-    font-size: 16px;
-    font-weight: 500;
-    font-style: normal;
-    line-height: 22px;
-    color: var(--coz-fg-primary, rgba(6, 7, 9, 80%));
-    text-overflow: ellipsis;
-  }
-
-  .container-node-tooltip {
-    height: 100%;
-  }
-
-  .container-node-tooltip-icon {
-    display: flex;
-    align-items: center;
-
-    width: 16px;
-    height: 100%;
-
-    color: #a7a9b0;
-  }
-`;

+ 0 - 6
packages/plugins/free-container-plugin/src/container-node-render/components/index.ts

@@ -1,6 +0,0 @@
-export { ContainerNodeHeader } from './header';
-export { ContainerNodeBackground } from './background';
-export { ContainerNodeContainer } from './container';
-export { ContainerNodeBorder } from './border';
-export { ContainerNodePorts } from './ports';
-export { ContainerNodeForm } from './form';

+ 0 - 33
packages/plugins/free-container-plugin/src/container-node-render/components/ports/index.tsx

@@ -1,33 +0,0 @@
-import React, { useEffect, type FC } from 'react';
-
-import { WorkflowPortRender } from '@flowgram.ai/free-lines-plugin';
-import { WorkflowNodePortsData, useNodeRender } from '@flowgram.ai/free-layout-core';
-
-import { useContainerNodeRenderProps } from '../../hooks';
-
-export const ContainerNodePorts: FC = () => {
-  const { node, ports } = useNodeRender();
-  const { renderPorts } = useContainerNodeRenderProps();
-
-  useEffect(() => {
-    const portsData = node.getData<WorkflowNodePortsData>(WorkflowNodePortsData);
-    portsData.updateDynamicPorts();
-  }, [node]);
-
-  return (
-    <>
-      {renderPorts.map((p) => (
-        <div
-          key={`canvas-port${p.id}`}
-          className="container-node-port"
-          data-port-id={p.id}
-          data-port-type={p.type}
-          style={p.style}
-        />
-      ))}
-      {ports.map((p) => (
-        <WorkflowPortRender key={p.id} entity={p} />
-      ))}
-    </>
-  );
-};

+ 0 - 1
packages/plugins/free-container-plugin/src/container-node-render/constant.ts

@@ -1 +0,0 @@
-export const ContainerNodeRenderKey = 'container-node-render-key';

+ 0 - 1
packages/plugins/free-container-plugin/src/container-node-render/hooks/index.ts

@@ -1 +0,0 @@
-export { useContainerNodeRenderProps } from './use-render-props';

+ 0 - 22
packages/plugins/free-container-plugin/src/container-node-render/hooks/use-render-props.ts

@@ -1,22 +0,0 @@
-import { useNodeRender } from '@flowgram.ai/free-layout-core';
-
-import { type ContainerNodeMetaRenderProps } from '../type';
-
-export const useContainerNodeRenderProps = (): ContainerNodeMetaRenderProps => {
-  const { node } = useNodeRender();
-  const nodeMeta = node.getNodeMeta();
-
-  const {
-    title = '',
-    tooltip,
-    renderPorts = [],
-    style = {},
-  } = (nodeMeta?.renderContainerNode?.() ?? {}) as Partial<ContainerNodeMetaRenderProps>;
-
-  return {
-    title,
-    tooltip,
-    renderPorts,
-    style,
-  };
-};

+ 0 - 4
packages/plugins/free-container-plugin/src/container-node-render/index.ts

@@ -1,4 +0,0 @@
-export { ContainerNodeRenderKey } from './constant';
-export { ContainerNodeRender } from './render';
-export type { ContainerNodeMetaRenderProps, ContainerNodeRenderProps } from './type';
-export * from './components';

+ 0 - 19
packages/plugins/free-container-plugin/src/container-node-render/render.tsx

@@ -1,19 +0,0 @@
-import React, { type FC } from 'react';
-
-import type { ContainerNodeRenderProps } from './type';
-import {
-  ContainerNodeBackground,
-  ContainerNodeHeader,
-  ContainerNodePorts,
-  ContainerNodeBorder,
-  ContainerNodeContainer,
-} from './components';
-
-export const ContainerNodeRender: FC<ContainerNodeRenderProps> = ({ content }) => (
-  <ContainerNodeContainer>
-    <ContainerNodeBackground />
-    <ContainerNodeBorder />
-    <ContainerNodeHeader>{content}</ContainerNodeHeader>
-    <ContainerNodePorts />
-  </ContainerNodeContainer>
-);

+ 0 - 19
packages/plugins/free-container-plugin/src/container-node-render/type.ts

@@ -1,19 +0,0 @@
-import { ReactNode } from 'react';
-
-import type { WorkflowNodeEntity } from '@flowgram.ai/free-layout-core';
-
-export interface ContainerNodeMetaRenderProps {
-  title: string;
-  tooltip?: string;
-  renderPorts: {
-    id: string;
-    type: 'input' | 'output';
-    style: React.CSSProperties;
-  }[];
-  style: React.CSSProperties;
-}
-
-export interface ContainerNodeRenderProps {
-  node: WorkflowNodeEntity;
-  content?: ReactNode;
-}

+ 1 - 1
packages/plugins/free-container-plugin/src/index.ts

@@ -1,4 +1,4 @@
-export * from './container-node-render';
 export * from './node-into-container';
+export * from './sub-canvas';
 export { createContainerNodePlugin } from './plugin';
 export type { WorkflowContainerPluginOptions } from './type';

+ 0 - 13
packages/plugins/free-container-plugin/src/plugin.tsx

@@ -1,15 +1,7 @@
-import React from 'react';
-
-import { FlowRendererRegistry } from '@flowgram.ai/renderer';
 import { definePluginCreator } from '@flowgram.ai/core';
 
 import type { WorkflowContainerPluginOptions } from './type';
 import { NodeIntoContainerService } from './node-into-container';
-import {
-  ContainerNodeRenderKey,
-  ContainerNodeRender,
-  ContainerNodeRenderProps,
-} from './container-node-render';
 
 export const createContainerNodePlugin = definePluginCreator<WorkflowContainerPluginOptions>({
   onBind: ({ bind }) => {
@@ -17,11 +9,6 @@ export const createContainerNodePlugin = definePluginCreator<WorkflowContainerPl
   },
   onInit(ctx, options) {
     ctx.get(NodeIntoContainerService).init();
-
-    const registry = ctx.get<FlowRendererRegistry>(FlowRendererRegistry);
-    registry.registerReactComponent(ContainerNodeRenderKey, (props: ContainerNodeRenderProps) => (
-      <ContainerNodeRender {...props} content={options.renderContent} />
-    ));
   },
   onReady(ctx, options) {
     if (options.disableNodeIntoContainer !== true) {

+ 24 - 0
packages/plugins/free-container-plugin/src/sub-canvas/components/background/index.tsx

@@ -0,0 +1,24 @@
+import React, { type FC } from 'react';
+
+import { useCurrentEntity } from '@flowgram.ai/free-layout-core';
+
+import { SubCanvasBackgroundStyle } from './style';
+
+export const SubCanvasBackground: FC = () => {
+  const node = useCurrentEntity();
+  return (
+    <SubCanvasBackgroundStyle className="sub-canvas-background" data-flow-editor-selectable="true">
+      <svg width="100%" height="100%">
+        <pattern id="sub-canvas-dot-pattern" width="20" height="20" patternUnits="userSpaceOnUse">
+          <circle cx="1" cy="1" r="1" stroke="#eceeef" fillOpacity="0.5" />
+        </pattern>
+        <rect
+          width="100%"
+          height="100%"
+          fill="url(#sub-canvas-dot-pattern)"
+          data-node-panel-container={node.id}
+        />
+      </svg>
+    </SubCanvasBackgroundStyle>
+  );
+};

+ 8 - 0
packages/plugins/free-container-plugin/src/sub-canvas/components/background/style.ts

@@ -0,0 +1,8 @@
+import styled from 'styled-components';
+
+export const SubCanvasBackgroundStyle = styled.div`
+  width: 100%;
+  height: 100%;
+  inset: 56px 18px 18px;
+  background-color: #f2f3f5;
+`;

+ 19 - 0
packages/plugins/free-container-plugin/src/sub-canvas/components/border/index.tsx

@@ -0,0 +1,19 @@
+import React, { CSSProperties, ReactNode, type FC } from 'react';
+
+import { SubCanvasBorderStyle } from './style';
+
+interface ISubCanvasBorder {
+  style?: CSSProperties;
+  children?: ReactNode | ReactNode[];
+}
+
+export const SubCanvasBorder: FC<ISubCanvasBorder> = ({ style, children }) => (
+  <SubCanvasBorderStyle
+    className="sub-canvas-border"
+    style={{
+      ...style,
+    }}
+  >
+    {children}
+  </SubCanvasBorderStyle>
+);

+ 2 - 3
packages/plugins/free-container-plugin/src/container-node-render/components/border/style.ts → packages/plugins/free-container-plugin/src/sub-canvas/components/border/style.ts

@@ -1,9 +1,9 @@
 import styled from 'styled-components';
 
-export const ContainerNodeBorderStyle = styled.div`
+export const SubCanvasBorderStyle = styled.div`
   pointer-events: none;
 
-  position: absolute;
+  position: relative;
 
   display: flex;
   align-items: center;
@@ -17,7 +17,6 @@ export const ContainerNodeBorderStyle = styled.div`
   border-style: solid;
   border-width: 8px;
   border-radius: 8px;
-  box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 4%), 0 4px 12px 0 rgba(0, 0, 0, 2%);
 
   &::before {
     content: '';

+ 3 - 0
packages/plugins/free-container-plugin/src/sub-canvas/components/index.ts

@@ -0,0 +1,3 @@
+export { SubCanvasBackground } from './background';
+export { SubCanvasBorder } from './border';
+export { SubCanvasRender } from './render';

+ 44 - 0
packages/plugins/free-container-plugin/src/sub-canvas/components/render/index.tsx

@@ -0,0 +1,44 @@
+import React, { CSSProperties, useLayoutEffect, type FC } from 'react';
+
+import { useCurrentEntity } from '@flowgram.ai/free-layout-core';
+
+import { SubCanvasRenderStyle } from './style';
+import { SubCanvasBorder } from '../border';
+import { SubCanvasBackground } from '../background';
+import { useNodeSize } from '../../hooks';
+
+interface ISubCanvasBorder {
+  className?: string;
+  style?: CSSProperties;
+}
+
+export const SubCanvasRender: FC<ISubCanvasBorder> = ({ className, style }) => {
+  const node = useCurrentEntity();
+  const nodeSize = useNodeSize();
+  const { height, width } = nodeSize ?? {};
+  const nodeHeight = nodeSize?.height ?? 0;
+  const { padding } = node.transform;
+
+  useLayoutEffect(() => {
+    node.renderData.node.style.width = width + 'px';
+    node.renderData.node.style.height = height + 'px';
+  }, [height, width]);
+
+  return (
+    <SubCanvasRenderStyle
+      className={`sub-canvas-render ${className ?? ''}`}
+      style={{
+        height: nodeHeight - padding.top,
+        ...style,
+      }}
+      data-flow-editor-selectable="true"
+      onDragStart={(e) => {
+        e.stopPropagation();
+      }}
+    >
+      <SubCanvasBorder>
+        <SubCanvasBackground />
+      </SubCanvasBorder>
+    </SubCanvasRenderStyle>
+  );
+};

+ 6 - 0
packages/plugins/free-container-plugin/src/sub-canvas/components/render/style.ts

@@ -0,0 +1,6 @@
+import styled from 'styled-components';
+
+export const SubCanvasRenderStyle = styled.div`
+  width: 100%;
+  height: 100%;
+`;

+ 1 - 0
packages/plugins/free-container-plugin/src/sub-canvas/hooks/index.ts

@@ -0,0 +1 @@
+export { useNodeSize } from './use-node-size';

+ 62 - 0
packages/plugins/free-container-plugin/src/sub-canvas/hooks/use-node-size.ts

@@ -0,0 +1,62 @@
+import { useState, useEffect } from 'react';
+
+import {
+  useCurrentEntity,
+  WorkflowNodeMeta,
+  WorkflowNodePortsData,
+} from '@flowgram.ai/free-layout-core';
+import { FlowNodeTransformData } from '@flowgram.ai/document';
+
+interface NodeSize {
+  width: number;
+  height: number;
+}
+
+export const useNodeSize = (): NodeSize | undefined => {
+  const node = useCurrentEntity();
+  const nodeMeta = node.getNodeMeta<WorkflowNodeMeta>();
+  const { size = { width: 300, height: 200 }, isContainer } = nodeMeta;
+
+  const transform = node.getData<FlowNodeTransformData>(FlowNodeTransformData);
+  const [width, setWidth] = useState(size.width);
+  const [height, setHeight] = useState(size.height);
+
+  const updatePorts = () => {
+    const portsData = node.getData<WorkflowNodePortsData>(WorkflowNodePortsData);
+    portsData.updateDynamicPorts();
+  };
+
+  const updateSize = () => {
+    // 无子节点时
+    if (node.blocks.length === 0) {
+      setWidth(size.width);
+      setHeight(size.height);
+      return;
+    }
+    // 存在子节点时,只监听宽高变化
+    setWidth(transform.bounds.width);
+    setHeight(transform.bounds.height);
+  };
+
+  useEffect(() => {
+    const dispose = transform.onDataChange(() => {
+      updateSize();
+      updatePorts();
+    });
+    return () => dispose.dispose();
+  }, [transform, width, height]);
+
+  useEffect(() => {
+    // 初始化触发一次
+    updateSize();
+  }, []);
+
+  if (!isContainer) {
+    return;
+  }
+
+  return {
+    width,
+    height,
+  };
+};

+ 2 - 0
packages/plugins/free-container-plugin/src/sub-canvas/index.ts

@@ -0,0 +1,2 @@
+export * from './hooks';
+export * from './components';