Quellcode durchsuchen

feat: 添加历史记录,撤销、重做功能

liaojiaxing vor 4 Monaten
Ursprung
Commit
6b327cf332

+ 3 - 2
apps/er-designer/src/components/NoticeNode.tsx

@@ -5,7 +5,7 @@ import { Input } from "antd";
 import { useSize } from 'ahooks';
 
 const NoticeNode = ({ node, graph }: { node: Node; graph: Graph }) => {
-  const { style, name, text } = node.getData();
+  const { style, name, text, id } = node.getData();
   const boxRef = useRef<HTMLDivElement>(null);
   const { width: w, height: h } = useSize(boxRef) || node.getSize();
 
@@ -50,7 +50,8 @@ const NoticeNode = ({ node, graph }: { node: Node; graph: Graph }) => {
             variant="borderless"
             className="w-full resize-none! flex-1 color-#666"
             value={text}
-            onChange={(e) => node.setData({ text: e.target.value })}
+            key={id}
+            onChange={(e) => node.prop('update:remark', { ...node.getData(), text: e.target.value })}
           />
         </div>
       </div>

+ 4 - 2
apps/er-designer/src/components/TableNode.tsx

@@ -12,7 +12,7 @@ function TableNode({ node, graph }: { node: Node; graph: Graph }) {
   useEffect(() => {
     const container = containerRef.current;
     if (container?.clientHeight) {
-      node.setSize(container.clientWidth, container.clientHeight);
+      node.setSize(220, container.clientHeight);
     }
   }, [tableColumnList.length]);
 
@@ -65,11 +65,13 @@ function TableNode({ node, graph }: { node: Node; graph: Graph }) {
     ports?.forEach((item) => {
       if (!tableColumnList.find((column) => item.id?.includes(column.id))) {
         node.removePort(item);
+        console.log("removePort", item);
       }
     });
 
     tableColumnList.forEach((item, index) => {
       if (!ports.find((port) => port.id?.includes(item.id))) {
+        console.log("addPort", item);
         node.addPort({
           id: item.id + "_port1",
           group: "columnPort",
@@ -138,7 +140,7 @@ function TableNode({ node, graph }: { node: Node; graph: Graph }) {
       >
         <span className="flex-1 truncate flex items-center justify-between">
           <span className="flex items-center">
-            <span className=" w-6px h-6px rounded-full mr-4px bg-#5684bb inline-block cursor-pointer" />
+            <span className=" w-6px h-6px rounded-full mr-4px  inline-block cursor-pointer" />
             {record.schemaName}
             {record.cn_name ? `(${record.cn_name})` : ""}
           </span>

+ 161 - 169
apps/er-designer/src/models/erModel.tsx

@@ -1,9 +1,10 @@
 import { useEffect, useMemo, useRef, useState } from "react";
-import { EventArgs, Graph, Shape } from "@antv/x6";
-import { History } from "@antv/x6-plugin-history";
+import { EventArgs, Graph, Shape, Point } from "@antv/x6";
 import { Transform } from "@antv/x6-plugin-transform";
 import { Scroller } from "@antv/x6-plugin-scroller";
 import { Snapline } from "@antv/x6-plugin-snapline";
+import { Keyboard } from "@antv/x6-plugin-keyboard";
+import { Export } from "@antv/x6-plugin-export";
 import { GetAllDesignTables } from "@/api";
 import { useRequest } from "umi";
 import type {
@@ -16,6 +17,7 @@ import type {
 } from "@/type";
 import { uuid } from "@/utils";
 import { RelationType } from "@/enum";
+import { render } from "./renderer";
 
 import "@/components/TableNode";
 import "@/components/TopicNode";
@@ -25,7 +27,9 @@ import { message } from "antd";
 export default function erModel() {
   const graphRef = useRef<Graph>();
   const [graph, setGraph] = useState<Graph>();
-  const [project, setProject] = useState<ProjectInfo>({
+  const historyRef = useRef<ProjectInfo[]>([]);
+  const activeIndex = useRef(0);
+  const [project, setProjectInfo] = useState<ProjectInfo>({
     id: "1",
     name: "项目1",
     folderId: "root",
@@ -50,6 +54,52 @@ export default function erModel() {
     },
   });
 
+  /**
+   * 统一修改数据
+   * @param info 模型信息
+   * @param ingoreHistory 忽略历史记录
+   * @param isInit  初始化
+   */
+  const setProject = (
+    info: ProjectInfo | ((state: ProjectInfo) => ProjectInfo),
+    ingoreHistory?: boolean,
+    isInit?: boolean
+  ) => {
+    if (isInit) {
+      historyRef.current = [];
+      activeIndex.current = 0;
+    }
+
+    if (info && typeof info === "function") {
+      setProjectInfo((state) => {
+        const result = info(state);
+        graphRef.current && render(graphRef.current, result);
+        // 添加记录
+        if (!ingoreHistory) {
+          historyRef.current?.push(result);
+          activeIndex.current = historyRef.current?.length - 1;
+          if (historyRef.current?.length > 20) {
+            historyRef.current?.shift();
+            activeIndex.current -= 1;
+          }
+        }
+        return result;
+      });
+    } else {
+      setProjectInfo(info);
+      graphRef.current && render(graphRef.current, info);
+      // 添加记录
+      if (!ingoreHistory) {
+        historyRef.current?.push(info);
+        activeIndex.current = historyRef.current?.length - 1;
+        if (historyRef.current?.length > 20) {
+          historyRef.current?.shift();
+          activeIndex.current -= 1;
+        }
+      }
+    }
+  };
+
   const initGraph = (container: HTMLElement) => {
     const instance = new Graph({
       container,
@@ -118,16 +168,19 @@ export default function erModel() {
       },
     });
 
-    instance.use(new History());
     instance.use(new Snapline({ enabled: true }));
     instance.use(
       new Transform({
         resizing: {
-          enabled: true,
+          enabled: (node) => {
+            return node.shape !== "table-node";
+          },
         },
       })
     );
     instance.use(new Scroller());
+    instance.use(new Keyboard());
+    instance.use(new Export());
 
     setGraph(instance);
     graphRef.current = instance;
@@ -142,12 +195,67 @@ export default function erModel() {
         }
       }
     );
+
+    instance.on(
+      "node:change:update:remark",
+      function (args: EventArgs["cell:change:*"]) {
+        console.log('修改备注:', args.current)
+        updateRemark(args.current);
+      }
+    );
+
+    // Graph.registerAnchor({
+    //   'custom-anchor': function(view, magnet, ref, options) {
+    //     // console.log(view, magnet, ref);
+    //     const source = magnet.getBoundingClientRect();
+    //     const target = (ref as SVGAElement).getBoundingClientRect();
+    //     console.log(source, target, this);
+    //     if(source.x + source.width < target.x + target.width) {
+    //       // 右侧
+    //       // 获取右侧点
+    //       if(this.sourceView?.cell.isNode()) {
+    //         return new Point(this.sourceView.getBBox().x + this.sourceView.getBBox().width, this.sourceView.getBBox().y + this.sourceView.getBBox().height / 2);
+    //       }
+    //     } else {
+    //       // 左侧
+    //       return new Point(this.sourceView.getBBox().x, this.sourceView.getBBox().y + this.sourceView.getBBox().height / 2);
+    //     }
+    //     return new Point(100, 200);
+    //   }
+    // });
+  };
+
+  // 能否重做
+  const canRedo = useMemo(() => {
+    return (
+      historyRef.current?.length > 1 &&
+      activeIndex.current < historyRef.current?.length - 1
+    );
+  }, [historyRef.current, activeIndex.current]);
+
+  // 能否撤销
+  const canUndo = useMemo(() => {
+    return activeIndex.current > 0 && historyRef.current?.length > 1;
+  }, [historyRef.current, activeIndex.current]);
+
+  // 撤销
+  const onUndo = () => {
+    const info = historyRef.current?.[activeIndex.current - 1];
+    activeIndex.current -= 1;
+    setProject(info, true);
+  };
+
+  // 重做
+  const onRedo = () => {
+    const info = historyRef.current?.[activeIndex.current + 1];
+    activeIndex.current += 1;
+    setProject(info, true);
   };
 
   /**
    * 添加表
    */
-  const addTable = () => {
+  const addTable = (parentId?: string) => {
     const tableId = uuid();
     const columnId = uuid();
     const newTable: TableItemType = {
@@ -163,13 +271,14 @@ export default function erModel() {
         name: "",
         cn_name: "新建表",
         en_name: "new table",
-        parentBusinessTableId: "",
+        parentBusinessTableId: parentId || "",
         schemaName: "new_table",
         type: 1,
         updateTime: "",
         openSync: false,
         style: {
-          color: "#616161",
+          // 随机颜色
+          color: "#" + Math.floor(Math.random() * 0x666666).toString(16),
           x: 0,
           y: 0,
         },
@@ -212,44 +321,16 @@ export default function erModel() {
       ],
     };
 
+    // 子表插入到父表后面
+    const tables = [...project.tables];
+    tables.splice(
+      tables.findIndex((item) => item.table.id === parentId) + 1,
+      0,
+      newTable
+    );
     setProject({
       ...project,
-      tables: [...project.tables, newTable],
-    });
-
-    graphRef.current?.addNode({
-      shape: "table-node",
-      x: 300,
-      y: 100,
-      width: 200,
-      height: 200,
-      id: tableId,
-      data: newTable,
-      zIndex: 3,
-      ports: {
-        groups: {
-          // 字段名前连接桩
-          columnPort: {
-            markup: [
-              {
-                tagName: "rect",
-                selector: "rect",
-              },
-              {
-                tagName: "circle",
-                selector: "circle",
-              },
-            ],
-            position: {
-              name: "absolute",
-              args: {
-                x: 12,
-                y: 42,
-              },
-            },
-          },
-        },
-      },
+      tables: parentId ? tables : [...project.tables, newTable],
     });
   };
 
@@ -272,13 +353,18 @@ export default function erModel() {
   };
 
   /**
-   * 删除表
+   * 删除表及其子表
    * @param tableId
    */
   const deleteTable = (tableId: string) => {
     setProject({
       ...project,
-      tables: project.tables.filter((item) => item.table.id !== tableId),
+      tables: project.tables.filter(
+        (item) =>
+          item.table.id !== tableId ||
+          item.table.parentBusinessTableId !== tableId
+      ),
+      // todo 处理子表对应关系
       relations: project.relations.filter(
         (item) => item.primaryTable !== tableId && item.foreignTable !== tableId
       ),
@@ -308,17 +394,6 @@ export default function erModel() {
       ...project,
       topicAreas: [...project.topicAreas, newTopicArea],
     });
-
-    graphRef.current?.addNode({
-      shape: "topic-node",
-      x: 300,
-      y: 100,
-      width: 200,
-      height: 200,
-      id: topicAreaId,
-      data: newTopicArea,
-      zIndex: 0,
-    });
   };
 
   /**
@@ -371,21 +446,6 @@ export default function erModel() {
       ...project,
       remarks: [...project.remarks, newRemark],
     });
-
-    const notice = graphRef.current?.addNode({
-      shape: "notice-node",
-      x: 300,
-      y: 100,
-      width: 200,
-      height: 200,
-      id: remarkId,
-      data: newRemark,
-      zIndex: 1,
-    });
-
-    notice?.on("change:data", function (args) {
-      updateRemark(args.current);
-    });
   };
 
   /**
@@ -436,7 +496,7 @@ export default function erModel() {
         );
       }
     });
-    console.log(sourceColumn, targetColumn, newRelation);
+
     if (!sourceColumn || !targetColumn) {
       return {
         relations: project.relations,
@@ -453,106 +513,29 @@ export default function erModel() {
 
     return {
       relations: [
-        ...project.relations, {
+        ...project.relations,
+        {
           ...newRelation,
           name: `${sourceTable?.table.schemaName}_${targetTable?.table.schemaName}_${sourceColumn.schemaName}`,
-        }],
+        },
+      ],
       canAdd: true,
     };
   };
 
-  const addRelationEdge = (
-    id: string,
-    source: RelationItem,
-    target: RelationItem
-  ) => {
-    // 添加关系连线
-    const relationEdge = graphRef.current?.addEdge({
-      id,
-      router: {
-        name: "manhattan",
-        args: {
-          direction: "H",
-        },
-      },
-      attrs: {
-        line: {
-          stroke: "#333",
-          strokeWidth: 1,
-          targetMarker: null,
-        },
-      },
-      source: {
-        cell: source.tableId,
-        port: source.columnId + "_port2",
-        anchor: "left",
-      },
-      target: {
-        cell: target.tableId,
-        port: target.columnId + "_port2",
-        anchor: "left",
-      },
-      data: {
-        type: "relation",
-        label: uuid(),
-      },
-      defaultLabel: {
-        markup: [
-          {
-            tagName: "circle",
-            selector: "bg",
-          },
-          {
-            tagName: "text",
-            selector: "txt",
-          },
-        ],
-        attrs: {
-          txt: {
-            fill: "#fff",
-            textAnchor: "middle",
-            textVerticalAnchor: "middle",
-          },
-          bg: {
-            ref: "txt",
-            fill: "#333",
-            r: 10,
-            strokeWidth: 0,
-          },
-        },
-      },
-    });
-    relationEdge?.appendLabel({
-      attrs: {
-        txt: {
-          text: 1,
-        },
-      },
-      position: {
-        distance: 25,
-      },
-    });
-    relationEdge?.appendLabel({
-      attrs: {
-        txt: {
-          text: 1,
-        },
-      },
-      position: {
-        distance: -25,
-      },
-    });
-  };
-
-  type RelationItem = {
-    tableId: string;
-    columnId: string;
-  };
-
   /**
    * 添加关系
    */
-  const addRelation = (source: RelationItem, target: RelationItem) => {
+  const addRelation = (
+    source: {
+      tableId: string;
+      columnId: string;
+    },
+    target: {
+      tableId: string;
+      columnId: string;
+    }
+  ) => {
     const newRelation: ColumnRelation = {
       id: uuid(),
       name: "",
@@ -566,9 +549,6 @@ export default function erModel() {
     setProject((state) => {
       const obj = getRelations(state, newRelation);
       if (obj.canAdd) {
-        // 添加连线
-        addRelationEdge(newRelation.id, source, target);
-
         return {
           ...state,
           relations: obj.relations,
@@ -611,7 +591,11 @@ export default function erModel() {
       relationEdge.setLabelAt(0, {
         attrs: {
           txt: {
-            text: relation.relationType === RelationType.OneToOne || relation.relationType === RelationType.OneToMany ? '1' : 'n',
+            text:
+              relation.relationType === RelationType.OneToOne ||
+              relation.relationType === RelationType.OneToMany
+                ? "1"
+                : "n",
           },
         },
         position: {
@@ -621,7 +605,11 @@ export default function erModel() {
       relationEdge.setLabelAt(1, {
         attrs: {
           txt: {
-            text: relation.relationType === RelationType.OneToMany || relation.relationType === RelationType.ManyToMany ? 'n' : '1',
+            text:
+              relation.relationType === RelationType.OneToMany ||
+              relation.relationType === RelationType.ManyToMany
+                ? "n"
+                : "1",
           },
         },
         position: {
@@ -661,5 +649,9 @@ export default function erModel() {
     addRelation,
     updateRelation,
     deleteRelation,
+    canRedo,
+    canUndo,
+    onRedo,
+    onUndo,
   };
 }

+ 207 - 0
apps/er-designer/src/models/renderer.ts

@@ -0,0 +1,207 @@
+import { ProjectInfo, RemarkInfo, TableItemType, TopicAreaInfo } from "@/type";
+import { Graph } from "@antv/x6";
+
+export const render = (graph: Graph, project: ProjectInfo) => {
+  const { tables, relations, topicAreas, remarks } = project;
+  // 渲染表格
+  const renderTable = (tableItem: TableItemType) => {
+    graph.addNode({
+      shape: "table-node",
+      x: 300,
+      y: 100,
+      width: 220,
+      height: 69,
+      id: tableItem.table.id,
+      data: tableItem,
+      zIndex: 3,
+      ports: {
+        groups: {
+          // 字段名前连接桩
+          columnPort: {
+            markup: [
+              {
+                tagName: "rect",
+                selector: "rect",
+              },
+              {
+                tagName: "circle",
+                selector: "circle",
+              },
+            ],
+            position: {
+              name: "absolute",
+              args: {
+                x: 12,
+                y: 42,
+              },
+            },
+          },
+        },
+      },
+    });
+  };
+  // 渲染主题区域
+  const renderTopicArea = (topicArea: TopicAreaInfo) => {
+    graph.addNode({
+      shape: "topic-node",
+      x: 300,
+      y: 100,
+      width: 200,
+      height: 200,
+      id: topicArea.id,
+      data: topicArea,
+      zIndex: 0,
+    });
+  };
+  // 渲染备注
+  const renderRemark = (remark: RemarkInfo) => {
+    const notice = graph?.addNode({
+      shape: "notice-node",
+      x: 300,
+      y: 100,
+      width: 200,
+      height: 200,
+      id: remark.id,
+      data: remark,
+      zIndex: 1,
+    });
+  };
+  // 渲染关系
+  const renderRelationEdge = (
+    id: string,
+    source: {
+      tableId: string;
+      columnId: string;
+    },
+    target: {
+      tableId: string;
+      columnId: string;
+    }
+  ) => {
+    // 添加关系连线
+    const relationEdge = graph?.addEdge({
+      id,
+      router: {
+        name: "er",
+        args: {
+          offset: "center",
+          direction: "H",
+        },
+      },
+      connector: { name: "rounded" },
+      attrs: {
+        line: {
+          stroke: "#333",
+          strokeWidth: 1,
+          targetMarker: null,
+        },
+      },
+      source: {
+        cell: source.tableId,
+        port: source.columnId + "_port2",
+        anchor: "left",
+      },
+      target: {
+        cell: target.tableId,
+        port: target.columnId + "_port2",
+        anchor: "left",
+      },
+      data: {
+        type: "relation",
+      },
+      defaultLabel: {
+        markup: [
+          {
+            tagName: "circle",
+            selector: "bg",
+          },
+          {
+            tagName: "text",
+            selector: "txt",
+          },
+        ],
+        attrs: {
+          txt: {
+            fill: "#fff",
+            textAnchor: "middle",
+            textVerticalAnchor: "middle",
+          },
+          bg: {
+            ref: "txt",
+            fill: "#333",
+            r: 10,
+            strokeWidth: 0,
+          },
+        },
+      },
+    });
+    relationEdge?.appendLabel({
+      attrs: {
+        txt: {
+          text: 1,
+        },
+      },
+      position: {
+        distance: 25,
+      },
+    });
+    relationEdge?.appendLabel({
+      attrs: {
+        txt: {
+          text: 1,
+        },
+      },
+      position: {
+        distance: -25,
+      },
+    });
+  };
+
+  const cells = graph.getCells();
+
+  const allIds = [
+    ...tables.map((table) => table.table.id),
+    ...topicAreas.map((topicArea) => topicArea.id),
+    ...remarks.map((remark) => remark.id),
+    ...relations.map((relation) => relation.id),
+  ];
+  // 移除空数据节点
+  cells.forEach((cell) => {
+    if (!allIds.includes(cell.id)) {
+      cell.remove();
+    }
+  });
+  tables.forEach((tableItem) => {
+    if (!graph.getCellById(tableItem.table.id)) {
+      renderTable(tableItem);
+    }
+  });
+  topicAreas.forEach((topicArea) => {
+    if (!graph.getCellById(topicArea.id)) {
+      renderTopicArea(topicArea);
+    }
+  });
+  remarks.forEach((remark) => {
+    if (!graph.getCellById(remark.id)) {
+      renderRemark(remark);
+    }
+  });
+  // setTimeout(() => {
+    
+  // }, 100);
+  relations.forEach((relation) => {
+    if (!graph.getCellById(relation.id)) {
+      renderRelationEdge(
+        relation.id,
+        {
+          tableId: relation.primaryTable,
+          columnId: relation.primaryKey,
+        },
+        {
+          tableId: relation.foreignTable,
+          columnId: relation.foreignKey,
+        }
+      );
+    }
+  });
+};

+ 6 - 6
apps/er-designer/src/pages/er/components/RelationPanel.tsx

@@ -83,16 +83,16 @@ export default function RelationPanel() {
               m-b-[10px]"
               onClick={() => setActive(active === item.id ? '' : item.id)}
             >
-              <div className="font-bold">{item.name}</div>
+              <div className="font-bold truncate">{item.name}</div>
               <div>
                 <i className="iconfont icon-open m-r-10px" />
               </div>
             </div>
             <div
-              className="content overflow-hidden"
-              style={{ height: active === item.id ? "auto" : 0 }}
+              className="content overflow-hidden grid"
+              style={{ gridTemplateRows: active === item.id ? "1fr" : '0fr', transition: 'all 0.3s' }}
             >
-              <Form layout="vertical">
+              <Form layout="vertical" className="overflow-hidden">
                 <div className="flex justify-between">
                   <Form.Item label="主键">
                     {getPrimaryColumn(item)?.table?.schemaName}
@@ -141,7 +141,7 @@ export default function RelationPanel() {
                     cancelText="取消"
                     onConfirm={() => deleteRelation(item.id)}
                   >
-                    <Button color="danger" danger className="m-y-10px w-full">
+                    <Button danger className="m-y-10px w-full">
                       删除
                     </Button>
                   </Popconfirm>
@@ -154,7 +154,7 @@ export default function RelationPanel() {
       {list.length === 0 && (
         <div className="flex flex-col items-center justify-center h-[300px]">
           <img src={noData} alt="暂无数据" className="w-[200px] h-[200px]" />
-          <div className="text-gray-400">添加额外注释内容!</div>
+          <div className="text-gray-400">拖拽字段形成关系!</div>
         </div>
       )}
     </div>

+ 14 - 13
apps/er-designer/src/pages/er/components/RemarkPanel.tsx

@@ -57,18 +57,21 @@ export default function RemarkPanel() {
               hover:bg-[#fafafa] 
               cursor-pointer 
               m-b-[10px]"
-              onClick={() => setActiveKey(item.id)}
+              onClick={() => setActiveKey(activeKey === item.id ? "" : item.id)}
             >
-              <div className="font-bold">{item.name}</div>
+              <div className="font-bold truncate">{item.name}</div>
               <div>
                 <i className="iconfont icon-open m-r-10px" />
               </div>
             </div>
             <div
-              className="content overflow-hidden"
-              style={{ height: activeKey === item.id ? "auto" : 0 }}
+              className="content overflow-hidden grid"
+              style={{
+                gridTemplateRows: activeKey === item.id ? "1fr" : "0fr",
+                transition: "all 0.3s",
+              }}
             >
-              <Form layout="vertical">
+              <Form layout="vertical" className="overflow-hidden">
                 <Form.Item label="标题" layout="vertical">
                   <Input
                     placeholder="请输入"
@@ -123,14 +126,12 @@ export default function RemarkPanel() {
           </div>
         );
       })}
-      {
-        list.length === 0 && (
-          <div className="flex flex-col items-center justify-center h-[300px]">
-            <img src={noData} alt="暂无数据" className="w-[200px] h-[200px]" />
-            <div className="text-gray-400">添加额外注释内容!</div>
-          </div>
-        )
-      }
+      {list.length === 0 && (
+        <div className="flex flex-col items-center justify-center h-[300px]">
+          <img src={noData} alt="暂无数据" className="w-[200px] h-[200px]" />
+          <div className="text-gray-400">添加额外注释内容!</div>
+        </div>
+      )}
     </div>
   );
 }

+ 256 - 205
apps/er-designer/src/pages/er/components/TableItem.tsx

@@ -1,7 +1,4 @@
-import {
-  DeleteOutlined,
-  HolderOutlined,
-} from "@ant-design/icons";
+import { DeleteOutlined, HolderOutlined } from "@ant-design/icons";
 import {
   Col,
   Row,
@@ -25,11 +22,14 @@ import { useModel } from "umi";
 export default function TableItem({
   data,
   onChange,
+  active,
+  setActive,
 }: {
   data: TableItemType;
   onChange: (data: TableItemType) => void;
+  active: string;
+  setActive: (active: string) => void;
 }) {
-  const [collapsed, setCollapsed] = React.useState(false);
   const { tableColumnList = [] } = data;
   const { addTable, deleteTable } = useModel("erModel");
 
@@ -52,12 +52,12 @@ export default function TableItem({
         }
         return item;
       }),
-    })
+    });
   };
 
   // 添加表
   const handleAddChildTable = () => {
-    addTable();
+    addTable(table.id);
   };
 
   // 添加字段
@@ -83,6 +83,13 @@ export default function TableItem({
     });
   };
 
+  const handleDeleteColumn = (columnId: string) => {
+    onChange({
+      table,
+      tableColumnList: tableColumnList.filter((item) => item.id !== columnId),
+    });
+  };
+
   return (
     <div
       className="
@@ -97,6 +104,7 @@ export default function TableItem({
       p-l-16px"
       style={{
         borderLeftColor: table.style?.color || "#eee",
+        marginLeft: table.parentBusinessTableId ? 10 : 0
       }}
     >
       <div
@@ -110,9 +118,9 @@ export default function TableItem({
         hover:bg-[#fafafa] 
         cursor-pointer 
         m-b-[10px]"
-        onClick={() => setCollapsed(!collapsed)}
+        onClick={() => setActive(active === table.id ? "" : table.id)}
       >
-        <div className="font-bold">
+        <div className="font-bold truncate">
           {table.schemaName}({table.cn_name})
         </div>
         <div>
@@ -158,107 +166,121 @@ export default function TableItem({
       </div>
       <div
         className="content overflow-hidden"
-        style={{ height: collapsed ? 0 : "auto" }}
+        style={{
+          display: "grid",
+          gridTemplateRows: active === table.id ? "1fr" : "0fr",
+          transition: "all 0.3s",
+        }}
       >
-        <Form layout="horizontal" labelCol={{ span: 8 }}>
-          <Row gutter={8}>
-            <Col span={12}>
-              <Form.Item label="编码">
-                <Input
-                  placeholder="请输入"
-                  value={table.schemaName}
-                  onChange={(e) =>
-                    handleTableChange("schemaName", e.target.value)
-                  }
-                />
-              </Form.Item>
-            </Col>
-            <Col span={12}>
-              <Form.Item label="别名">
-                <Input
-                  placeholder="请输入"
-                  value={table.aliasName}
-                  onChange={(e) =>
-                    handleTableChange("aliasName", e.target.value)
-                  }
-                />
-              </Form.Item>
-            </Col>
-          </Row>
-          <Form.Item
-            label="类型"
-            labelCol={{ span: 4 }}
-            wrapperCol={{ span: 21 }}
-          >
-            <Select
-              placeholder="请选择"
-              options={TABLE_TYPE_OPTIONS}
-              value={table.type}
-              onChange={(val) => handleTableChange("type", val)}
-            />
-          </Form.Item>
-        </Form>
+        <div className="overflow-hidden">
+          <Form layout="horizontal" labelCol={{ span: 8 }}>
+            <Row gutter={8}>
+              <Col span={12}>
+                <Form.Item label="编码">
+                  <Input
+                    placeholder="请输入"
+                    value={table.schemaName}
+                    onChange={(e) =>
+                      handleTableChange("schemaName", e.target.value)
+                    }
+                  />
+                </Form.Item>
+              </Col>
+              <Col span={12}>
+                <Form.Item label="别名">
+                  <Input
+                    placeholder="请输入"
+                    value={table.aliasName}
+                    onChange={(e) =>
+                      handleTableChange("aliasName", e.target.value)
+                    }
+                  />
+                </Form.Item>
+              </Col>
+            </Row>
+            <Form.Item
+              label="类型"
+              labelCol={{ span: 4 }}
+              wrapperCol={{ span: 21 }}
+            >
+              <Select
+                placeholder="请选择"
+                options={TABLE_TYPE_OPTIONS}
+                value={table.type}
+                onChange={(val) => handleTableChange("type", val)}
+              />
+            </Form.Item>
+          </Form>
 
-        <div className="flex justify-between m-b-10px">
-          <CustomColorPicker
-            color={table.style?.background}
-            onChange={(color) =>
-              handleTableChange("style", { ...table.style, color })
-            }
-          >
-            <div
-              className="rounded-4px cus-btn w-32px h-32px bg-#eee flex-none cursor-pointer shadow-inner"
-              style={{ background: table.style?.color || "#eee" }}
-            ></div>
-          </CustomColorPicker>
-          <div className="flex gap-4px">
-            <Button type="primary" onClick={handleAddChildTable}>
-              添加子表
-            </Button>
-            <Button type="primary" onClick={handleAddColumn}>
-              添加字段
-            </Button>
-            <Popconfirm
-              okType="primary"
-              title="确定删除该表?"
-              okText="确定"
-              cancelText="取消"
-              onConfirm={() => deleteTable(table.id)}
+          <div className="flex justify-between m-b-10px">
+            <CustomColorPicker
+              color={table.style?.background}
+              onChange={(color) =>
+                handleTableChange("style", { ...table.style, color })
+              }
             >
-              <div className="rounded-4px cus-btn w-32px h-32px bg-#eee flex-none text-center leading-32px color-red cursor-pointer">
-                <DeleteOutlined />
-              </div>
-            </Popconfirm>
+              <div
+                className="rounded-4px cus-btn w-32px h-32px bg-#eee flex-none cursor-pointer shadow-inner"
+                style={{ background: table.style?.color || "#eee" }}
+              ></div>
+            </CustomColorPicker>
+            <div className="flex gap-4px">
+              {!table.parentBusinessTableId && (
+                <Button type="primary" onClick={handleAddChildTable}>
+                  添加子表
+                </Button>
+              )}
+              <Button type="primary" onClick={handleAddColumn}>
+                添加字段
+              </Button>
+              <Popconfirm
+                okType="primary"
+                title="确定删除该表?"
+                okText="确定"
+                cancelText="取消"
+                onConfirm={() => deleteTable(table.id)}
+              >
+                <div className="rounded-4px cus-btn w-32px h-32px bg-#eee flex-none text-center leading-32px color-red cursor-pointer">
+                  <DeleteOutlined />
+                </div>
+              </Popconfirm>
+            </div>
           </div>
-        </div>
 
-        <div className="column-content border-solid border-1px border-#e4e4e4 border-x-none p-y-10px">
-          {/* 字段内容 */}
-          {tableColumnList.map((column, index) => {
-            return (
-              <div key={column.id} className="column-item flex gap-4px items-center jutify-space-between hover:bg-gray-100 mb-4px">
-                <HolderOutlined className="cursor-move" />
-                <Tooltip title="字段编码">
-                  <Input
-                    placeholder="编码"
-                    value={column.schemaName}
-                    className="flex-1"
-                    onChange={(e) => handleChangeColumn(index, "schemaName", e.target.value)}
-                  />
-                </Tooltip>
-                <Tooltip title="字段类型">
-                  <Select
-                    placeholder="类型"
-                    className="w-80px"
-                    options={DATA_TYPE_OPTIONS}
-                    value={column.type}
-                    onChange={(value) => handleChangeColumn(index, "type", value)}
-                    dropdownStyle={{ width: 120 }}
-                  />
-                </Tooltip>
-                <Tooltip title="非空">
-                  <div
-                    className="
+          <div className="column-content border-solid border-1px border-#e4e4e4 border-x-none p-y-10px">
+            {/* 字段内容 */}
+            {tableColumnList.map((column, index) => {
+              return (
+                <div
+                  key={column.id}
+                  className="column-item flex gap-4px items-center jutify-space-between hover:bg-gray-100 mb-4px"
+                >
+                  <HolderOutlined className="cursor-move" />
+                  <Tooltip title="字段编码">
+                    <Input
+                      placeholder="编码"
+                      value={column.schemaName}
+                      className="flex-1"
+                      onChange={(e) =>
+                        handleChangeColumn(index, "schemaName", e.target.value)
+                      }
+                    />
+                  </Tooltip>
+                  <Tooltip title="字段类型">
+                    <Select
+                      placeholder="类型"
+                      className="w-80px"
+                      options={DATA_TYPE_OPTIONS}
+                      value={column.type}
+                      onChange={(value) =>
+                        handleChangeColumn(index, "type", value)
+                      }
+                      dropdownStyle={{ width: 120 }}
+                    />
+                  </Tooltip>
+                  <Tooltip title="非空">
+                    <div
+                      className="
                     rounded-4px 
                     cus-btn 
                     w-32px 
@@ -269,19 +291,25 @@ export default function TableItem({
                     leading-32px 
                     cursor-pointer 
                     hover:bg-#ddd"
-                    style={
-                      column.isRequired
-                        ? { background: "#1677ff", color: "#fff" }
-                        : {}
-                    }
-                    onClick={() => handleChangeColumn(index, "isRequired", !column.isRequired)}
-                  >
-                    !
-                  </div>
-                </Tooltip>
-                <Tooltip title="唯一">
-                  <div
-                    className="
+                      style={
+                        column.isRequired
+                          ? { background: "#1677ff", color: "#fff" }
+                          : {}
+                      }
+                      onClick={() =>
+                        handleChangeColumn(
+                          index,
+                          "isRequired",
+                          !column.isRequired
+                        )
+                      }
+                    >
+                      !
+                    </div>
+                  </Tooltip>
+                  <Tooltip title="唯一">
+                    <div
+                      className="
                     rounded-4px 
                     cus-btn 
                     w-32px 
@@ -292,96 +320,119 @@ export default function TableItem({
                     leading-32px 
                     cursor-pointer 
                     hover:bg-#ddd"
-                    style={
-                      column.isUnique
-                        ? { background: "#1677ff", color: "#fff" }
-                        : {}
+                      style={
+                        column.isUnique
+                          ? { background: "#1677ff", color: "#fff" }
+                          : {}
+                      }
+                      onClick={() =>
+                        handleChangeColumn(index, "isUnique", !column.isUnique)
+                      }
+                    >
+                      1
+                    </div>
+                  </Tooltip>
+                  <Popover
+                    trigger="click"
+                    placement="right"
+                    content={
+                      <div
+                        className="w-200px max-h-400px overflow-y-auto"
+                        onClick={(e) => e.stopPropagation()}
+                      >
+                        <Form layout="vertical">
+                          <Form.Item label="字段名称" name="pkName">
+                            <Input
+                              className="w-full"
+                              placeholder="中文"
+                              value={column.cn_name}
+                              onChange={(e) =>
+                                handleChangeColumn(
+                                  index,
+                                  "cn_name",
+                                  e.target.value
+                                )
+                              }
+                            />
+                            <Input
+                              className="w-full"
+                              placeholder="英文"
+                              value={column.en_name}
+                              onChange={(e) =>
+                                handleChangeColumn(
+                                  index,
+                                  "en_name",
+                                  e.target.value
+                                )
+                              }
+                            />
+                          </Form.Item>
+                          <Form.Item label="描述" name="pkName">
+                            <Input.TextArea
+                              className="w-full"
+                              placeholder="描述中文..."
+                            />
+                            <Input.TextArea
+                              className="w-full"
+                              placeholder="描述英文..."
+                            />
+                          </Form.Item>
+                          <Form.Item label="长度">
+                            <InputNumber
+                              placeholder="请输入"
+                              min={0}
+                              className="w-full"
+                              value={column.maxLength}
+                              onChange={(num) =>
+                                handleChangeColumn(index, "maxLength", num)
+                              }
+                            />
+                          </Form.Item>
+                          <Form.Item label="精度">
+                            <InputNumber
+                              placeholder="请输入"
+                              min={0}
+                              className="w-full"
+                              value={column.precision}
+                              onChange={(num) =>
+                                handleChangeColumn(index, "precision", num)
+                              }
+                            />
+                          </Form.Item>
+                          <Form.Item label="默认值" name="pkName">
+                            <Input
+                              className="w-full"
+                              placeholder="默认值"
+                              value={column.defaultValue}
+                              onChange={(e) =>
+                                handleChangeColumn(
+                                  index,
+                                  "defaultValue",
+                                  e.target.value
+                                )
+                              }
+                            />
+                          </Form.Item>
+                        </Form>
+                        <Button
+                          type="default"
+                          danger
+                          className="w-full"
+                          onClick={() => handleDeleteColumn(column.id)}
+                        >
+                          删除
+                        </Button>
+                      </div>
                     }
-                    onClick={() => handleChangeColumn(index, "isUnique", !column.isUnique)}
                   >
-                    1
-                  </div>
-                </Tooltip>
-                <Popover
-                  trigger="click"
-                  placement="right"
-                  content={
-                    <div
-                      className="w-200px"
-                      onClick={(e) => e.stopPropagation()}
-                    >
-                      <Form layout="vertical">
-                        <Form.Item label="字段名称" name="pkName">
-                          <Input
-                            className="w-full"
-                            placeholder="中文"
-                            value={column.cn_name}
-                            onChange={(e) =>
-                              handleChangeColumn(index, "cn_name", e.target.value)
-                            }
-                          />
-                          <Input
-                            className="w-full"
-                            placeholder="英文"
-                            value={column.en_name}
-                            onChange={(e) =>
-                              handleChangeColumn(index, "en_name", e.target.value)
-                            }
-                          />
-                        </Form.Item>
-                        <Form.Item label="描述" name="pkName">
-                          <Input.TextArea
-                            className="w-full"
-                            placeholder="描述中文..."
-                          />
-                          <Input.TextArea
-                            className="w-full"
-                            placeholder="描述英文..."
-                          />
-                        </Form.Item>
-                        <Form.Item label="长度">
-                          <InputNumber
-                            placeholder="请输入"
-                            min={0}
-                            className="w-full"
-                            value={column.maxLength}
-                            onChange={(num) =>
-                              handleChangeColumn(index, "maxLength", num)
-                            }
-                          />
-                        </Form.Item>
-                        <Form.Item label="精度">
-                          <InputNumber
-                            placeholder="请输入"
-                            min={0}
-                            className="w-full"
-                            value={column.precision}
-                            onChange={(num) =>
-                              handleChangeColumn(index, "precision", num)
-                            }
-                          />
-                        </Form.Item>
-                        <Form.Item label="默认值" name="pkName">
-                          <Input
-                            className="w-full"
-                            placeholder="默认值"
-                            value={column.defaultValue}
-                            onChange={(e) =>
-                              handleChangeColumn(index, "defaultValue", e.target.value)
-                            }
-                          />
-                        </Form.Item>
-                      </Form>
+                    <div className="rounded-4px cus-btn w-32px h-32px bg-#eee flex-none text-center leading-32px cursor-pointer hover:bg-#ddd">
+                      <i className="iconfont icon-gengduo" />
                     </div>
-                  }
-                >
-                  <div className="rounded-4px cus-btn w-32px h-32px bg-#eee flex-none text-center leading-32px cursor-pointer hover:bg-#ddd">
-                    <i className="iconfont icon-gengduo" />
-                  </div>
-                </Popover>
-              </div>
-            );
-          })}
+                  </Popover>
+                </div>
+              );
+            })}
+          </div>
         </div>
       </div>
     </div>

+ 4 - 3
apps/er-designer/src/pages/er/components/TablePanel.tsx

@@ -6,9 +6,10 @@ import { useModel } from 'umi'
 import type { TableItemType } from '@/type'
 import noData from '@/assets/no-data.png'
 export default function TablePanel() {
-  const { project, updateTable } = useModel('erModel');
+  const { project, updateTable, addTable } = useModel('erModel');
   const contentRef = React.useRef<HTMLDivElement>(null);
   const [contentStyle, setContentStyle] = React.useState<React.CSSProperties>({});
+  const [active, setActive] = React.useState<string>('');
 
   useEffect(() => {
     // 计算高度
@@ -21,12 +22,12 @@ export default function TablePanel() {
     <div className='px-12px overflow-y-auto' ref={contentRef} style={contentStyle}>
       <div className="search-box flex gap-4px mb-12px">
         <Input placeholder="输入关键字搜索" suffix={<SearchOutlined />} />
-        <Button type="primary">添加表</Button>
+        <Button type="primary" onClick={() => addTable()}>添加表</Button>
         <Button type="primary">导入表</Button>
       </div>
       {
         project.tables.map((item) => {
-          return <TableItem data={item} onChange={updateTable} key={item.table.id}/>
+          return <TableItem data={item} onChange={updateTable} key={item.table.id} active={active} setActive={setActive} />
         })
       }
       {

+ 32 - 10
apps/er-designer/src/pages/er/components/Toolbar.tsx

@@ -3,10 +3,11 @@ import { Button, Tooltip, Divider, Dropdown } from "antd";
 import { DownOutlined } from "@ant-design/icons";
 import { useModel } from "umi";
 export default function Toolbar() {
-  const { addTable, addTopicArea, addRemark } = useModel("erModel");
+  const { addTable, addTopicArea, addRemark, graph, canRedo, canUndo, onRedo, onUndo } =
+    useModel("erModel");
   const scaleMenu = {
     style: {
-      width: 200
+      width: 200,
     },
     items: [
       {
@@ -33,22 +34,34 @@ export default function Toolbar() {
         key: "6",
         label: "200%",
       },
-    ]
+    ],
   };
-  
+
   return (
     <div className="flex items-center py-0px bg-#fafafa justify-center">
       <div className="group">
         <div className="flex items-center">
           <Tooltip title="撤销上一步操作">
-            <div className="btn flex flex-col items-center cursor-pointer py-4px px-10px hover:bg-gray-200">
+            <div
+              className="btn flex flex-col items-center cursor-pointer py-4px px-10px hover:bg-gray-200"
+              style={{
+                opacity: canUndo ? 1 : 0.5,
+              }}
+              onClick={() => canUndo && onUndo()}
+            >
               <svg className="icon h-24px w-24px" aria-hidden="true">
                 <use xlinkHref="#icon-undo"></use>
               </svg>
             </div>
           </Tooltip>
           <Tooltip title="恢复上一步操作">
-            <div className="btn flex flex-col items-center cursor-pointer py-4px px-10px hover:bg-gray-200 opacity-50">
+            <div
+              className="btn flex flex-col items-center cursor-pointer py-4px px-10px hover:bg-gray-200"
+              style={{
+                opacity: canRedo ? 1 : 0.5,
+              }}
+              onClick={() => canRedo && onRedo()}
+            >
               <svg className="icon h-24px w-24px" aria-hidden="true">
                 <use xlinkHref="#icon-redo"></use>
               </svg>
@@ -62,21 +75,30 @@ export default function Toolbar() {
       <div className="group">
         <div className="flex items-center">
           <Tooltip title="创建表">
-            <div className="btn flex flex-col items-center cursor-pointer py-4px px-10px hover:bg-gray-200" onClick={addTable}>
+            <div
+              className="btn flex flex-col items-center cursor-pointer py-4px px-10px hover:bg-gray-200"
+              onClick={() => addTable()}
+            >
               <svg className="icon h-24px w-24px" aria-hidden="true">
                 <use xlinkHref="#icon-biaoge"></use>
               </svg>
             </div>
           </Tooltip>
           <Tooltip title="创建主题域">
-            <div className="btn flex flex-col items-center cursor-pointer py-4px px-10px hover:bg-gray-200" onClick={addTopicArea}>
+            <div
+              className="btn flex flex-col items-center cursor-pointer py-4px px-10px hover:bg-gray-200"
+              onClick={addTopicArea}
+            >
               <svg className="icon h-24px w-24px" aria-hidden="true">
                 <use xlinkHref="#icon-ditukuangxuan"></use>
               </svg>
             </div>
           </Tooltip>
           <Tooltip title="创建备注">
-            <div className="btn flex flex-col items-center cursor-pointer py-4px px-10px hover:bg-gray-200" onClick={addRemark}>
+            <div
+              className="btn flex flex-col items-center cursor-pointer py-4px px-10px hover:bg-gray-200"
+              onClick={addRemark}
+            >
               <svg className="icon h-24px w-24px" aria-hidden="true">
                 <use xlinkHref="#icon-beizhu"></use>
               </svg>
@@ -127,7 +149,7 @@ export default function Toolbar() {
           <Dropdown menu={scaleMenu}>
             <span className="text-14px leading-18px">
               <span>100%</span>
-              <DownOutlined className="text-12px ml-4px"/>
+              <DownOutlined className="text-12px ml-4px" />
             </span>
           </Dropdown>
         </div>