Forráskód Böngészése

feat: Panel 重构 (#998)

* feat: Panel 重构

* fix: e2e error
July 1 hónapja
szülő
commit
bb927fd46b

+ 2 - 0
apps/demo-free-layout/src/plugins/panel-manager-plugin/index.tsx

@@ -20,6 +20,8 @@ import { PanelType } from './constants';
 const nodeFormPanelFactory: PanelFactory<NodeFormPanelProps> = {
   key: PanelType.NodeFormPanel,
   defaultSize: 500,
+  maxSize: 800,
+  minSize: 300,
   render: (props: NodeFormPanelProps) => <NodeFormPanel {...props} />,
 };
 

+ 21 - 2
common/config/rush/pnpm-lock.yaml

@@ -3878,6 +3878,15 @@ importers:
       inversify:
         specifier: ^6.0.1
         version: 6.2.2(reflect-metadata@0.2.2)
+      nanoid:
+        specifier: ^5.0.9
+        version: 5.1.5
+      use-sync-external-store:
+        specifier: ^1.6.0
+        version: 1.6.0(react@18.3.1)
+      zustand:
+        specifier: ^5.0.8
+        version: 5.0.8(@types/react@18.3.24)(immer@10.1.3)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1))
     devDependencies:
       '@flowgram.ai/eslint-config':
         specifier: workspace:*
@@ -4052,7 +4061,7 @@ importers:
         version: 5.1.5
       zustand:
         specifier: ^5.0.8
-        version: 5.0.8(@types/react@18.3.24)(immer@10.1.3)(react@18.3.1)
+        version: 5.0.8(@types/react@18.3.24)(immer@10.1.3)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1))
     devDependencies:
       '@flowgram.ai/eslint-config':
         specifier: workspace:*
@@ -12906,6 +12915,11 @@ packages:
     resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==}
     engines: {node: '>= 0.4'}
 
+  use-sync-external-store@1.6.0:
+    resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
   util-deprecate@1.0.2:
     resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
 
@@ -23963,6 +23977,10 @@ snapshots:
       punycode: 1.4.1
       qs: 6.14.0
 
+  use-sync-external-store@1.6.0(react@18.3.1):
+    dependencies:
+      react: 18.3.1
+
   util-deprecate@1.0.2: {}
 
   util-ts-types@1.0.0: {}
@@ -24456,10 +24474,11 @@ snapshots:
 
   zod@3.25.76: {}
 
-  zustand@5.0.8(@types/react@18.3.24)(immer@10.1.3)(react@18.3.1):
+  zustand@5.0.8(@types/react@18.3.24)(immer@10.1.3)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)):
     optionalDependencies:
       '@types/react': 18.3.24
       immer: 10.1.3
       react: 18.3.1
+      use-sync-external-store: 1.6.0(react@18.3.1)
 
   zwitch@2.0.4: {}

+ 1 - 1
e2e/fixed-layout/tests/drawer.spec.ts

@@ -19,7 +19,7 @@ test.describe('test llm drawer', () => {
   test('sync data', async ({ page }) => {
     // 确保 llm drawer 更改表单数据,数据同步
     const LLM_NODE_ID = 'llm_0';
-    const DRAWER_CLASSNAME = 'float-panel-wrap';
+    const DRAWER_CLASSNAME = 'gedit-flow-panel-wrap';
 
     const TEST_FILL_VALUE = '123';
 

+ 3 - 0
packages/plugins/panel-manager-plugin/package.json

@@ -25,6 +25,9 @@
   "dependencies": {
     "inversify": "^6.0.1",
     "clsx": "^1.1.1",
+    "nanoid": "^5.0.9",
+    "zustand": "^5.0.8",
+    "use-sync-external-store": "^1.6.0",
     "@flowgram.ai/core": "workspace:*",
     "@flowgram.ai/utils": "workspace:*"
   },

+ 13 - 8
packages/plugins/panel-manager-plugin/src/components/panel-layer/css.ts

@@ -43,8 +43,9 @@ export const globalCSS = `
     min-width: 0;
     display: flex;
     column-gap: 4px;
+    max-width: 100%;
   }
-  
+
   .gedit-flow-panel-main-area {
     position: relative;
     overflow: hidden;
@@ -59,11 +60,15 @@ export const globalCSS = `
     width: 100%;
     min-height: 0;
   }
+  .gedit-flow-panel-wrap {
+    pointer-events: auto;
+    overflow: auto;
+    position: relative;
+  }
+  .gedit-flow-panel-wrap.panel-horizontal {
+    height: 100%;
+  }
+  .gedit-flow-panel-wrap.panel-vertical {
+    width: 100%;
+  }
 `;
-
-export const floatPanelWrap: React.CSSProperties = {
-  pointerEvents: 'auto',
-  height: '100%',
-  width: '100%',
-  overflow: 'auto',
-};

+ 0 - 59
packages/plugins/panel-manager-plugin/src/components/panel-layer/float-panel.tsx

@@ -1,59 +0,0 @@
-/**
- * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
- * SPDX-License-Identifier: MIT
- */
-
-import { useEffect, useRef, startTransition, useState, useCallback } from 'react';
-
-import { Area } from '../../types';
-import { usePanelManager } from '../../hooks/use-panel-manager';
-import { floatPanelWrap } from './css';
-
-export const FloatPanel: React.FC<{ area: Area }> = ({ area }) => {
-  const [, setVersion] = useState(0);
-  const panelManager = usePanelManager();
-  const panel = useRef(panelManager.getPanel(area));
-
-  const isHorizontal = ['right', 'docked-right'].includes(area);
-
-  const render = () =>
-    panel.current.elements.map((i) => (
-      <div className="float-panel-wrap" key={i.key} style={{ ...floatPanelWrap, ...i.style }}>
-        {i.el}
-      </div>
-    ));
-  const node = useRef(render());
-
-  useEffect(() => {
-    const dispose = panel.current.onUpdate(() => {
-      startTransition(() => {
-        node.current = render();
-        setVersion((v) => v + 1);
-      });
-    });
-    return () => dispose.dispose();
-  }, [panel]);
-  const onResize = useCallback((newSize: number) => panel.current!.updateSize(newSize), []);
-  const size = panel.current!.currentSize;
-  const sizeStyle = isHorizontal
-    ? { width: size, height: '100%' }
-    : { height: size, width: '100%' };
-
-  return (
-    <div
-      className="gedit-flow-panel"
-      style={{
-        position: 'relative',
-        display: panel.current.visible ? 'block' : 'none',
-        ...sizeStyle,
-      }}
-    >
-      {panelManager.config.resizeBarRender({
-        size,
-        direction: isHorizontal ? 'vertical' : 'horizontal',
-        onResize,
-      })}
-      {node.current}
-    </div>
-  );
-};

+ 3 - 3
packages/plugins/panel-manager-plugin/src/components/panel-layer/panel-layer.tsx

@@ -6,7 +6,7 @@
 import clsx from 'clsx';
 
 import { useGlobalCSS } from '../../hooks/use-global-css';
-import { FloatPanel } from './float-panel';
+import { PanelArea } from './panel';
 import { globalCSS } from './css';
 
 export type PanelLayerProps = React.PropsWithChildren<{
@@ -40,11 +40,11 @@ export const PanelLayer: React.FC<PanelLayerProps> = ({
       <div className="gedit-flow-panel-left-area">
         <div className="gedit-flow-panel-main-area">{children}</div>
         <div className="gedit-flow-panel-bottom-area">
-          <FloatPanel area={mode === 'docked' ? 'docked-bottom' : 'bottom'} />
+          <PanelArea area={mode === 'docked' ? 'docked-bottom' : 'bottom'} />
         </div>
       </div>
       <div className="gedit-flow-panel-right-area">
-        <FloatPanel area={mode === 'docked' ? 'docked-right' : 'right'} />
+        <PanelArea area={mode === 'docked' ? 'docked-right' : 'right'} />
       </div>
     </div>
   );

+ 90 - 0
packages/plugins/panel-manager-plugin/src/components/panel-layer/panel.tsx

@@ -0,0 +1,90 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { useEffect, startTransition, useState, useRef } from 'react';
+
+import { useStoreWithEqualityFn } from 'zustand/traditional';
+import { shallow } from 'zustand/shallow';
+import clsx from 'clsx';
+
+import { Area } from '../../types';
+import { PanelEntity } from '../../services/panel-factory';
+import { usePanelManager } from '../../hooks/use-panel-manager';
+import { PanelContext } from '../../contexts';
+
+const PanelItem: React.FC<{ panel: PanelEntity }> = ({ panel }) => {
+  const panelManager = usePanelManager();
+  const ref = useRef<HTMLDivElement>(null);
+  const resize =
+    panel.factory.resize !== undefined ? panel.factory.resize : panelManager.config.autoResize;
+
+  const isHorizontal = ['right', 'docked-right'].includes(panel.area);
+
+  const size = useStoreWithEqualityFn(panel.store, (s) => s.size, shallow);
+
+  const sizeStyle = isHorizontal ? { width: size } : { height: size };
+  const handleResize = (next: number) => {
+    let nextSize = next;
+    if (typeof panel.factory.maxSize === 'number' && nextSize > panel.factory.maxSize) {
+      nextSize = panel.factory.maxSize;
+    } else if (typeof panel.factory.minSize === 'number' && nextSize < panel.factory.minSize) {
+      nextSize = panel.factory.minSize;
+    }
+    panel.store.setState({ size: nextSize });
+  };
+
+  useEffect(() => {
+    /** The set size may be illegal and needs to be updated according to the real element rendered for the first time. */
+    if (ref.current) {
+      const { width, height } = ref.current.getBoundingClientRect();
+      const realSize = isHorizontal ? width : height;
+      panel.store.setState({ size: realSize });
+    }
+  }, []);
+
+  return (
+    <div
+      className={clsx(
+        'gedit-flow-panel-wrap',
+        isHorizontal ? 'panel-horizontal' : 'panel-vertical'
+      )}
+      key={panel.id}
+      ref={ref}
+      style={{ ...panel.factory.style, ...panel.config.style, ...sizeStyle }}
+    >
+      {resize &&
+        panelManager.config.resizeBarRender({
+          size,
+          direction: isHorizontal ? 'vertical' : 'horizontal',
+          onResize: handleResize,
+        })}
+      {panel.renderer}
+    </div>
+  );
+};
+
+export const PanelArea: React.FC<{ area: Area }> = ({ area }) => {
+  const panelManager = usePanelManager();
+  const [panels, setPanels] = useState(panelManager.getPanels(area));
+
+  useEffect(() => {
+    const dispose = panelManager.onPanelsChange(() => {
+      startTransition(() => {
+        setPanels(panelManager.getPanels(area));
+      });
+    });
+    return () => dispose.dispose();
+  }, []);
+
+  return (
+    <>
+      {panels.map((panel) => (
+        <PanelContext.Provider value={panel} key={panel.id}>
+          <PanelItem panel={panel} />
+        </PanelContext.Provider>
+      ))}
+    </>
+  );
+};

+ 10 - 0
packages/plugins/panel-manager-plugin/src/contexts.ts

@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { createContext } from 'react';
+
+import type { PanelEntity } from './services/panel-factory';
+
+export const PanelContext = createContext({} as PanelEntity);

+ 31 - 1
packages/plugins/panel-manager-plugin/src/create-panel-manager-plugin.ts

@@ -5,13 +5,43 @@
 
 import { definePluginCreator } from '@flowgram.ai/core';
 
+import {
+  PanelEntityFactory,
+  PanelEntity,
+  PanelEntityFactoryConstant,
+  PanelEntityConfigConstant,
+} from './services/panel-factory';
 import { defineConfig } from './services/panel-config';
-import { PanelManager, PanelManagerConfig, PanelLayer } from './services';
+import {
+  PanelManager,
+  PanelManagerConfig,
+  PanelLayer,
+  PanelRestore,
+  PanelRestoreImpl,
+} from './services';
 
 export const createPanelManagerPlugin = definePluginCreator<Partial<PanelManagerConfig>>({
   onBind: ({ bind }, opt) => {
     bind(PanelManager).to(PanelManager).inSingletonScope();
+    bind(PanelRestore).to(PanelRestoreImpl).inSingletonScope();
     bind(PanelManagerConfig).toConstantValue(defineConfig(opt));
+    bind(PanelEntityFactory).toFactory(
+      (context) =>
+        ({
+          factory,
+          config,
+        }: {
+          factory: PanelEntityFactoryConstant;
+          config: PanelEntityConfigConstant;
+        }) => {
+          const container = context.container.createChild();
+          container.bind(PanelEntityFactoryConstant).toConstantValue(factory);
+          container.bind(PanelEntityConfigConstant).toConstantValue(config);
+          const panel = container.resolve(PanelEntity);
+          panel.init();
+          return panel;
+        }
+    );
   },
   onInit(ctx) {
     ctx.playground.registerLayer(PanelLayer);

+ 10 - 0
packages/plugins/panel-manager-plugin/src/hooks/use-panel.ts

@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { useContext } from 'react';
+
+import { PanelContext } from '../contexts';
+
+export const usePanel = () => useContext(PanelContext);

+ 2 - 1
packages/plugins/panel-manager-plugin/src/index.ts

@@ -7,10 +7,11 @@
 export { createPanelManagerPlugin } from './create-panel-manager-plugin';
 
 /** services */
-export { PanelManager, type PanelManagerConfig } from './services';
+export { PanelManager, PanelRestore, type PanelManagerConfig } from './services';
 
 /** react hooks */
 export { usePanelManager } from './hooks/use-panel-manager';
+export { usePanel } from './hooks/use-panel';
 
 export { DockedPanelLayer, type DockedPanelLayerProps } from './components/panel-layer';
 export { ResizeBar } from './components/resize-bar';

+ 0 - 75
packages/plugins/panel-manager-plugin/src/services/float-panel.ts

@@ -1,75 +0,0 @@
-/**
- * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
- * SPDX-License-Identifier: MIT
- */
-
-import { Emitter } from '@flowgram.ai/utils';
-
-import type { PanelFactory, PanelConfig } from '../types';
-
-export interface PanelElement {
-  key: string;
-  style?: React.CSSProperties;
-  el: React.ReactNode;
-}
-
-const PANEL_SIZE_DEFAULT = 400;
-
-export class FloatPanel {
-  elements: PanelElement[] = [];
-
-  private onUpdateEmitter = new Emitter<void>();
-
-  sizeMap = new Map<string, number>();
-
-  onUpdate = this.onUpdateEmitter.event;
-
-  currentFactoryKey = '';
-
-  updateSize(newSize: number) {
-    this.sizeMap.set(this.currentFactoryKey, newSize);
-    this.onUpdateEmitter.fire();
-  }
-
-  get currentSize(): number {
-    return this.sizeMap.get(this.currentFactoryKey) || PANEL_SIZE_DEFAULT;
-  }
-
-  constructor(private config: PanelConfig) {}
-
-  open(factory: PanelFactory<any>, options: any) {
-    const el = factory.render(options?.props);
-    const idx = this.elements.findIndex((e) => e.key === factory.key);
-    this.currentFactoryKey = factory.key;
-    if (!this.sizeMap.has(factory.key)) {
-      this.sizeMap.set(factory.key, factory.defaultSize || PANEL_SIZE_DEFAULT);
-    }
-    if (idx >= 0) {
-      this.elements[idx] = { el, key: factory.key, style: factory.style };
-    } else {
-      this.elements.push({ el, key: factory.key, style: factory.style });
-      if (this.elements.length > this.config.max) {
-        this.elements.shift();
-      }
-    }
-    this.onUpdateEmitter.fire();
-  }
-
-  get visible() {
-    return this.elements.length > 0;
-  }
-
-  close(key?: string) {
-    if (!key) {
-      this.elements = [];
-    } else {
-      this.elements = this.elements.filter((e) => e.key !== key);
-    }
-    this.onUpdateEmitter.fire();
-  }
-
-  dispose() {
-    this.elements = [];
-    this.onUpdateEmitter.dispose();
-  }
-}

+ 1 - 0
packages/plugins/panel-manager-plugin/src/services/index.ts

@@ -6,3 +6,4 @@
 export { PanelManager } from './panel-manager';
 export { PanelManagerConfig } from './panel-config';
 export { PanelLayer } from './panel-layer';
+export { PanelRestore, PanelRestoreImpl } from './panel-restore';

+ 1 - 0
packages/plugins/panel-manager-plugin/src/services/panel-config.ts

@@ -15,6 +15,7 @@ export interface PanelManagerConfig {
   bottom: PanelConfig;
   dockedRight: PanelConfig;
   dockedBottom: PanelConfig;
+  /** Resizable, and multi-panel options mutually exclusive */
   autoResize: boolean;
   layerProps: PanelLayerProps;
   resizeBarRender: ({

+ 81 - 0
packages/plugins/panel-manager-plugin/src/services/panel-factory.ts

@@ -0,0 +1,81 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { createStore, StoreApi } from 'zustand/vanilla';
+import { nanoid } from 'nanoid';
+import { inject, injectable } from 'inversify';
+
+import type { PanelFactory, PanelEntityConfig, Area } from '../types';
+import { PanelRestore } from './panel-restore';
+
+export const PanelEntityFactory = Symbol('PanelEntityFactory');
+export type PanelEntityFactory = (options: {
+  factory: PanelEntityFactoryConstant;
+  config: PanelEntityConfigConstant;
+}) => PanelEntity;
+
+export const PanelEntityFactoryConstant = Symbol('PanelEntityFactoryConstant');
+export type PanelEntityFactoryConstant = PanelFactory<any>;
+export const PanelEntityConfigConstant = Symbol('PanelEntityConfigConstant');
+export type PanelEntityConfigConstant = PanelEntityConfig<any> & {
+  area: Area;
+};
+
+const PANEL_SIZE_DEFAULT = 400;
+
+interface PanelEntityState {
+  size: number;
+}
+
+@injectable()
+export class PanelEntity {
+  @inject(PanelRestore) restore: PanelRestore;
+
+  /** 面板工厂 */
+  @inject(PanelEntityFactoryConstant) public factory: PanelEntityFactoryConstant;
+
+  @inject(PanelEntityConfigConstant) public config: PanelEntityConfigConstant;
+
+  private initialized = false;
+
+  /** 实例唯一标识 */
+  id: string = nanoid();
+
+  /** 渲染缓存 */
+  node: React.ReactNode = null;
+
+  store: StoreApi<PanelEntityState>;
+
+  get area() {
+    return this.config.area;
+  }
+
+  get key() {
+    return this.factory.key;
+  }
+
+  get renderer() {
+    if (!this.node) {
+      this.node = this.factory.render(this.config.props);
+    }
+    return this.node;
+  }
+
+  init() {
+    if (this.initialized) {
+      return;
+    }
+    this.initialized = true;
+    const cache = this.restore.restore<PanelEntityState>(this.key);
+    this.store = createStore<PanelEntityState>(() => ({
+      size: this.config.defaultSize || this.factory.defaultSize || PANEL_SIZE_DEFAULT,
+      ...(cache ?? {}),
+    }));
+  }
+
+  dispose() {
+    this.restore.store(this.key, this.store.getState());
+  }
+}

+ 71 - 31
packages/plugins/panel-manager-plugin/src/services/panel-manager.ts

@@ -4,74 +4,114 @@
  */
 
 import { injectable, inject } from 'inversify';
-import { Playground } from '@flowgram.ai/core';
+import { Emitter } from '@flowgram.ai/utils';
 
 import { PanelManagerConfig } from './panel-config';
-import type { Area, PanelFactory } from '../types';
-import { FloatPanel } from './float-panel';
+import type { Area, PanelEntityConfig, PanelFactory } from '../types';
+import { PanelEntity, PanelEntityFactory } from './panel-factory';
 
 @injectable()
 export class PanelManager {
-  @inject(Playground) readonly playground: Playground;
-
   @inject(PanelManagerConfig) readonly config: PanelManagerConfig;
 
-  readonly panelRegistry = new Map<string, PanelFactory<any>>();
+  @inject(PanelEntityFactory) readonly createPanel: PanelEntityFactory;
 
-  right: FloatPanel;
+  readonly panelRegistry = new Map<string, PanelFactory<any>>();
 
-  bottom: FloatPanel;
+  private panels = new Map<string, PanelEntity>();
 
-  dockedRight: FloatPanel;
+  private onPanelsChangeEvent = new Emitter<void>();
 
-  dockedBottom: FloatPanel;
+  public onPanelsChange = this.onPanelsChangeEvent.event;
 
   init() {
     this.config.factories.forEach((factory) => this.register(factory));
-    this.right = new FloatPanel(this.config.right);
-    this.bottom = new FloatPanel(this.config.bottom);
-    this.dockedRight = new FloatPanel(this.config.dockedRight);
-    this.dockedBottom = new FloatPanel(this.config.dockedBottom);
   }
 
+  /** registry panel factory */
   register<T extends any>(factory: PanelFactory<T>) {
     this.panelRegistry.set(factory.key, factory);
   }
 
-  open(key: string, area: Area = 'right', options?: any) {
+  /** open panel */
+  public open(key: string, area: Area = 'right', options?: PanelEntityConfig) {
     const factory = this.panelRegistry.get(key);
     if (!factory) {
       return;
     }
-    const panel = this.getPanel(area);
-    panel.open(factory, options);
+
+    const sameKeyPanels = this.getPanels(area).filter((p) => p.key === key);
+    if (!factory.allowDuplicates && sameKeyPanels.length) {
+      sameKeyPanels.forEach((p) => this.remove(p.id));
+    }
+
+    const panel = this.createPanel({
+      factory,
+      config: {
+        area,
+        ...options,
+      },
+    });
+
+    this.panels.set(panel.id, panel);
+    this.trim(area);
+    this.onPanelsChangeEvent.fire();
+    console.log('jxj', this.panels);
+  }
+
+  /** close panel */
+  public close(key?: string) {
+    const panels = this.getPanels();
+    const closedPanels = key ? panels.filter((p) => p.key === key) : panels;
+    closedPanels.forEach((p) => this.remove(p.id));
+    this.onPanelsChangeEvent.fire();
+  }
+
+  private trim(area: Area) {
+    const panels = this.getPanels(area);
+    const areaConfig = this.getAreaConfig(area);
+    console.log('jxj', areaConfig.max, panels.length);
+    while (panels.length > areaConfig.max) {
+      const removed = panels.shift();
+      if (removed) {
+        this.remove(removed.id);
+      }
+    }
+  }
+
+  private remove(id: string) {
+    const panel = this.panels.get(id);
+    if (panel) {
+      panel.dispose();
+      this.panels.delete(id);
+    }
   }
 
-  close(key?: string) {
-    this.right.close(key);
-    this.bottom.close(key);
-    this.dockedRight.close(key);
-    this.dockedBottom.close(key);
+  getPanels(area?: Area) {
+    const panels: PanelEntity[] = [];
+    this.panels.forEach((panel) => {
+      if (!area || panel.area === area) {
+        panels.push(panel);
+      }
+    });
+    return panels;
   }
 
-  getPanel(area: Area) {
+  getAreaConfig(area: Area) {
     switch (area) {
       case 'docked-bottom':
-        return this.dockedBottom;
+        return this.config.dockedBottom;
       case 'docked-right':
-        return this.dockedRight;
+        return this.config.dockedRight;
       case 'bottom':
-        return this.bottom;
+        return this.config.bottom;
       case 'right':
       default:
-        return this.right;
+        return this.config.right;
     }
   }
 
   dispose() {
-    this.right.dispose();
-    this.bottom.dispose();
-    this.dockedBottom.dispose();
-    this.dockedRight.dispose();
+    this.onPanelsChangeEvent.dispose();
   }
 }

+ 25 - 0
packages/plugins/panel-manager-plugin/src/services/panel-restore.ts

@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { injectable } from 'inversify';
+
+export const PanelRestore = Symbol('PanelRestore');
+export interface PanelRestore {
+  store: (k: string, v: any) => void;
+  restore: <T>(k: string) => T | undefined;
+}
+
+@injectable()
+export class PanelRestoreImpl implements PanelRestore {
+  map = new Map<string, any>();
+
+  store(k: string, v: any) {
+    this.map.set(k, v);
+  }
+
+  restore<T>(k: string): T | undefined {
+    return this.map.get(k) as T;
+  }
+}

+ 11 - 0
packages/plugins/panel-manager-plugin/src/types.ts

@@ -13,6 +13,17 @@ export interface PanelConfig {
 export interface PanelFactory<T extends any> {
   key: string;
   defaultSize: number;
+  maxSize?: number;
+  minSize?: number;
   style?: React.CSSProperties;
+  /** Allows multiple panels with the same key to be rendered simultaneously  */
+  allowDuplicates?: boolean;
+  resize?: boolean;
   render: (props: T) => React.ReactNode;
 }
+
+export interface PanelEntityConfig<T extends any = any> {
+  defaultSize?: number;
+  style?: React.CSSProperties;
+  props?: T;
+}