瀏覽代碼

feat: 添加关系

liaojiaxing 4 月之前
父節點
當前提交
6fca30b033

+ 121 - 15
apps/er-designer/src/components/TableNode.tsx

@@ -1,8 +1,10 @@
 import React, { useEffect, useMemo, useRef } from "react";
 import { register } from "@antv/x6-react-shape";
-import { Graph, Node } from "@antv/x6";
-import type { ColumnItem, TableItemType, ViewTable } from "@/type";
+import { Edge, Graph, Node } from "@antv/x6";
+import type { ColumnItem, TableItemType } from "@/type";
 import { DATA_TYPE_OPTIONS } from "@/constants";
+import { uuid } from "@/utils";
+
 function TableNode({ node, graph }: { node: Node; graph: Graph }) {
   const { table, tableColumnList } = node.getData<TableItemType>();
   const containerRef = useRef<HTMLDivElement>(null);
@@ -14,27 +16,131 @@ function TableNode({ node, graph }: { node: Node; graph: Graph }) {
     }
   }, [tableColumnList.length]);
 
+  const hasListener = useRef(false);
+  const relationSource = useRef<any>();
   useEffect(() => {
-    node.removePorts();
-    tableColumnList.forEach((item, index) => {
-      node.addPort({
-        id: item.id + '_port',
-        group: "in",
-        args: {
-          y: 42 + 16 + (index * 27),
-        },
+    if (!hasListener.current) {
+      hasListener.current = true;
+      // 参考线添加完成
+      graph.on("edge:added", (args) => {
+        if (args.edge.data?.type === "refer") {
+          relationSource.current = args.edge.source;
+        }
+      });
+      // 参考线移除
+      graph.on("edge:removed", (args) => {
+        if (args.edge.data?.type === "refer") {
+          relationSource.current = undefined;
+        }
+      });
+      graph.on("edge:connected", (args) => {
+        // 连接完成后删除refer类型的连线
+        if (args.edge.data?.type === "refer") {
+          graph.removeEdge(args.edge);
+          const target: any = args.edge.target;
+          const source: any = args.edge.source;
+          if (target?.cell === node.id && target?.port?.includes("port1")) {
+            node.prop("add:relation", {
+              source: {
+                tableId: source?.cell,
+                columnId: source?.port?.slice(
+                  0,
+                  source?.port?.indexOf("_port")
+                ),
+              },
+              target: {
+                tableId: node.id,
+                columnId: target.port.slice(0, target.port.indexOf("_port")),
+              },
+            });
+          }
+        }
       });
+    }
+  }, []);
+
+  // 连接桩,1用于开始连接拖拽,2用于关系连线连接
+  useEffect(() => {
+    const ports = node.getPorts();
+    ports?.forEach((item) => {
+      if (!tableColumnList.find((column) => item.id?.includes(column.id))) {
+        node.removePort(item);
+      }
+    });
+
+    tableColumnList.forEach((item, index) => {
+      if (!ports.find((port) => port.id?.includes(item.id))) {
+        node.addPort({
+          id: item.id + "_port1",
+          group: "columnPort",
+          args: {
+            y: 42 + 16 + index * 27,
+          },
+          attrs: {
+            circle: {
+              r: 4,
+              magnet: true,
+              stroke: "#31d0c6",
+              strokeWidth: 2,
+              fill: "#fff",
+            },
+          },
+          zIndex: 5,
+        });
+        node.addPort({
+          id: item.id + "_port2",
+          group: "columnPort",
+          args: {
+            x: 0,
+            y: 42 + index * 27,
+          },
+          attrs: {
+            rect: {
+              magnet: true,
+              fill: "rgba(255,255,255,0)",
+              strokeWidth: 0,
+              width: node.size().width,
+              height: 27,
+            },
+          },
+          zIndex: 0,
+        });
+      }
     });
   }, [tableColumnList]);
 
-  const FiledItem = ({ record }: { record: ColumnItem }) => {
+  // 触发添加关系操作
+  // 字段位置鼠标抬起,判断有没有开始连线操作
+  const handleColumnMouseUp = (record: ColumnItem) => {
+    if (relationSource.current) {
+      node.prop("add:relation", {
+        source: {
+          tableId: relationSource.current?.cell,
+          columnId: relationSource.current?.port.slice(
+            0,
+            relationSource.current.port.indexOf("_port")
+          ),
+        },
+        target: {
+          tableId: node.id,
+          columnId: record.id,
+        },
+      });
+    }
+  };
+
+  const ColumnItem = ({ record }: { record: ColumnItem }) => {
     const type = DATA_TYPE_OPTIONS.find((item) => item.value === record.type);
     return (
-      <div className="w-full flex py-4px px-8px">
+      <div
+        className="w-full flex py-4px px-8px"
+        onMouseUp={() => handleColumnMouseUp(record)}
+      >
         <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"/>
-            {record.schemaName}{record.cn_name ? `(${record.cn_name})` : ""}
+            <span className=" w-6px h-6px rounded-full mr-4px bg-#5684bb inline-block cursor-pointer" />
+            {record.schemaName}
+            {record.cn_name ? `(${record.cn_name})` : ""}
           </span>
           <span>
             {record.isUnique ? (
@@ -84,7 +190,7 @@ function TableNode({ node, graph }: { node: Node; graph: Graph }) {
       <div className="bg-#fafafa flex-1">
         <div className="field-info">
           {tableColumnList.map((item) => {
-            return <FiledItem key={item.id} record={item} />;
+            return <ColumnItem key={item.id} record={item} />;
           })}
         </div>
       </div>

+ 9 - 0
apps/er-designer/src/constants/index.ts

@@ -25,4 +25,13 @@ export const TABLE_TYPE_OPTIONS = [
   {label: "流程表", value: 2},
   {label: "业务表", value: 3},
   {label: "视图", value: 4},
+];
+
+/**
+ * 对应关系
+ */
+export const RELATION_TYPE_OPTIONS = [
+  {label: "一对一", value: 1},
+  {label: "一对多", value: 2},
+  {label: "多对一", value: 3},
 ];

+ 7 - 0
apps/er-designer/src/enum/index.ts

@@ -6,4 +6,11 @@ export enum DataType {
   Bit = 4,
   Decimal = 5,
   DateTime = 6
+}
+
+export enum RelationType {
+  OneToOne = 1,
+  OneToMany = 2,
+  ManyToOne = 3,
+  ManyToMany = 4,
 }

+ 254 - 78
apps/er-designer/src/models/erModel.tsx

@@ -1,17 +1,26 @@
 import { useEffect, useMemo, useRef, useState } from "react";
-import { Graph, Shape } from "@antv/x6";
+import { EventArgs, Graph, Shape } from "@antv/x6";
 import { History } from "@antv/x6-plugin-history";
 import { Transform } from "@antv/x6-plugin-transform";
 import { Scroller } from "@antv/x6-plugin-scroller";
-import { Snapline } from '@antv/x6-plugin-snapline'
+import { Snapline } from "@antv/x6-plugin-snapline";
 import { GetAllDesignTables } from "@/api";
 import { useRequest } from "umi";
-import type { ColumnRelation, ProjectInfo, RemarkInfo, TableItemType, TopicAreaInfo } from "@/type";
+import type {
+  ColumnItem,
+  ColumnRelation,
+  ProjectInfo,
+  RemarkInfo,
+  TableItemType,
+  TopicAreaInfo,
+} from "@/type";
 import { uuid } from "@/utils";
+import { RelationType } from "@/enum";
 
 import "@/components/TableNode";
 import "@/components/TopicNode";
 import "@/components/NoticeNode";
+import { message } from "antd";
 
 export default function erModel() {
   const graphRef = useRef<Graph>();
@@ -56,12 +65,12 @@ export default function erModel() {
       },
       highlighting: {
         nodeAvailable: {
-          name: 'stroke',
+          name: "stroke",
           args: {
             padding: 4,
             attrs: {
-              'stroke-width': 2,
-              stroke: 'red',
+              "stroke-width": 2,
+              stroke: "red",
             },
           },
         },
@@ -71,26 +80,22 @@ export default function erModel() {
         allowEdge: false,
         allowLoop: false,
         router: {
-          name: 'normal',
+          name: "normal",
         },
         createEdge() {
           return new Shape.Edge({
             attrs: {
               line: {
-                stroke: '#ff0000',
+                stroke: "#ff0000",
                 strokeWidth: 1,
                 strokeDasharray: 5,
-                targetMarker: null
+                targetMarker: null,
               },
             },
             data: {
-              type: 'refference'
-            }
-          })
-        },
-        validateMagnet(args) {
-          console.log('validateMagnet', args);
-          return true;
+              type: "refer",
+            },
+          });
         },
       },
       grid: {
@@ -120,7 +125,6 @@ export default function erModel() {
         resizing: {
           enabled: true,
         },
-
       })
     );
     instance.use(new Scroller());
@@ -128,26 +132,18 @@ export default function erModel() {
     setGraph(instance);
     graphRef.current = instance;
 
-    instance.on("node:port:click", (args) => {
-      console.log('port click', args)
-    })
-
-    instance.on("edge:added", (args) => {
-      console.log('edge:added', args)
-    })
-
-    instance.on("edge:connected", (args) => {
-      console.log('edge:connected', args)
-    })
+    instance.on(
+      "node:change:add:relation",
+      (args: EventArgs["cell:change:*"]) => {
+        console.log("node:change:add:relation", args.current);
+        const { source, target } = args.current;
+        if (source && target) {
+          addRelation(source, target);
+        }
+      }
+    );
   };
 
-  const { data, run, loading } = useRequest(
-    () => GetAllDesignTables({ groupType: "" }),
-    {
-      manual: true,
-    }
-  );
-
   /**
    * 添加表
    */
@@ -232,32 +228,34 @@ export default function erModel() {
       zIndex: 3,
       ports: {
         groups: {
-          in: {
+          // 字段名前连接桩
+          columnPort: {
+            markup: [
+              {
+                tagName: "rect",
+                selector: "rect",
+              },
+              {
+                tagName: "circle",
+                selector: "circle",
+              },
+            ],
             position: {
-              name: 'absolute',
+              name: "absolute",
               args: {
                 x: 12,
-                y: 42
-              }
-            },
-            attrs: {
-              circle: {
-                r: 4,
-                magnet: true,
-                stroke: '#31d0c6',
-                strokeWidth: 2,
-                fill: '#fff',
+                y: 42,
               },
             },
           },
-        }
-      }
+        },
+      },
     });
   };
 
   /**
    * 更新表
-   * @param table 
+   * @param table
    */
   const updateTable = (table: TableItemType) => {
     setProject({
@@ -271,19 +269,27 @@ export default function erModel() {
         return item;
       }),
     });
-  }
+  };
 
   /**
    * 删除表
-   * @param tableId 
+   * @param tableId
    */
   const deleteTable = (tableId: string) => {
     setProject({
       ...project,
       tables: project.tables.filter((item) => item.table.id !== tableId),
+      relations: project.relations.filter(
+        (item) => item.primaryTable !== tableId && item.foreignTable !== tableId
+      ),
     });
     graphRef.current?.removeCell(tableId);
-    // todo删除关联关系
+    // 删除关联关系
+    project.relations.forEach((item) => {
+      if (item.primaryTable === tableId || item.foreignTable === tableId) {
+        graphRef.current?.removeCell(item.id);
+      }
+    });
   };
 
   /**
@@ -296,7 +302,7 @@ export default function erModel() {
       name: "主题域_" + (project.topicAreas.length + 1),
       style: {
         background: "#175e7a",
-      }
+      },
     };
     setProject({
       ...project,
@@ -316,8 +322,8 @@ export default function erModel() {
   };
 
   /**
-     * 修改主题域
-     */
+   * 修改主题域
+   */
   const updateTopicArea = (topicArea: TopicAreaInfo) => {
     setProject({
       ...project,
@@ -328,13 +334,13 @@ export default function erModel() {
           return topicArea;
         }
         return item;
-      })
-    })
-  }
+      }),
+    });
+  };
 
   /**
    * 删除主题域
-   * @param topicAreaId 
+   * @param topicAreaId
    */
   const deleteTopicArea = (topicAreaId: string) => {
     setProject({
@@ -359,7 +365,7 @@ export default function erModel() {
         width: 200,
         height: 100,
         background: "#fcf7ac",
-      }
+      },
     };
     setProject({
       ...project,
@@ -377,7 +383,7 @@ export default function erModel() {
       zIndex: 1,
     });
 
-    notice?.on("change:data", function(args) {
+    notice?.on("change:data", function (args) {
       updateRemark(args.current);
     });
   };
@@ -387,7 +393,7 @@ export default function erModel() {
    */
   const updateRemark = (remark: RemarkInfo) => {
     setProject((state) => ({
-      ...state || {},
+      ...(state || {}),
       remarks: state.remarks.map((item) => {
         if (item.id === remark.id) {
           const remarkNode = graphRef.current?.getCellById(remark.id);
@@ -395,13 +401,13 @@ export default function erModel() {
           return remark;
         }
         return item;
-      })
+      }),
     }));
   };
 
   /**
    * 删除备注
-   * @param remarkId 
+   * @param remarkId
    */
   const deleteRemark = (remarkId: string) => {
     setProject({
@@ -411,30 +417,166 @@ export default function erModel() {
     graphRef.current?.removeCell(remarkId);
   };
 
-  type RelationType = {
+  const getRelations = (project: ProjectInfo, newRelation: ColumnRelation) => {
+    let sourceColumn: ColumnItem | undefined;
+    let targetColumn: ColumnItem | undefined;
+    let sourceTable: TableItemType | undefined;
+    let targetTable: TableItemType | undefined;
+    project.tables.forEach((table) => {
+      if (table.table.id === newRelation.primaryTable) {
+        sourceTable = table;
+        sourceColumn = table.tableColumnList.find(
+          (item) => item.id === newRelation.primaryKey
+        );
+      }
+      if (table.table.id === newRelation.foreignTable) {
+        targetTable = table;
+        targetColumn = table.tableColumnList.find(
+          (item) => item.id === newRelation.foreignKey
+        );
+      }
+    });
+    console.log(sourceColumn, targetColumn, newRelation);
+    if (!sourceColumn || !targetColumn) {
+      return {
+        relations: project.relations,
+        canAdd: false,
+      };
+    }
+    if (sourceColumn.type !== targetColumn.type) {
+      message.warning("数据类型不一致");
+      return {
+        relations: project.relations,
+        canAdd: false,
+      };
+    }
+
+    return {
+      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: RelationType, target: RelationType) => {
+  const addRelation = (source: RelationItem, target: RelationItem) => {
     const newRelation: ColumnRelation = {
       id: uuid(),
-      name: '',
+      name: "",
       primaryKey: source.columnId,
       primaryTable: source.tableId,
       foreignKey: target.columnId,
-      foreignTable: target.columnId,
+      foreignTable: target.tableId,
       relationType: 1,
-      style: {}
+      style: {},
     };
-    setProject({
-      ...project,
-      relations: [...project.relations, newRelation],
+    setProject((state) => {
+      const obj = getRelations(state, newRelation);
+      if (obj.canAdd) {
+        // 添加连线
+        addRelationEdge(newRelation.id, source, target);
+
+        return {
+          ...state,
+          relations: obj.relations,
+        };
+      } else {
+        return state;
+      }
     });
-    // todo 添加连线
   };
 
   /**
@@ -452,7 +594,41 @@ export default function erModel() {
         }),
       };
     });
-    // todo 更新连线
+    // 更新连线
+    const relationEdge = graphRef.current?.getCellById(relation.id);
+    if (relationEdge?.isEdge()) {
+      relationEdge.setSource({
+        cell: relation.primaryTable,
+        port: relation.primaryKey + "_port2",
+        anchor: "left",
+      });
+      relationEdge.setTarget({
+        cell: relation.foreignTable,
+        port: relation.foreignKey + "_port2",
+        anchor: "left",
+      });
+      // 更新标签
+      relationEdge.setLabelAt(0, {
+        attrs: {
+          txt: {
+            text: relation.relationType === RelationType.OneToOne || relation.relationType === RelationType.OneToMany ? '1' : 'n',
+          },
+        },
+        position: {
+          distance: 25,
+        },
+      });
+      relationEdge.setLabelAt(1, {
+        attrs: {
+          txt: {
+            text: relation.relationType === RelationType.OneToMany || relation.relationType === RelationType.ManyToMany ? 'n' : '1',
+          },
+        },
+        position: {
+          distance: -25,
+        },
+      });
+    }
   };
 
   /**
@@ -463,14 +639,14 @@ export default function erModel() {
       ...project,
       relations: project.relations.filter((item) => item.id !== relationId),
     });
-    // todo 删除连线
+    // 删除连线
+    graphRef.current?.removeCell(relationId);
   };
 
   return {
     initGraph,
     graph,
     graphRef,
-    loading,
     project,
     setProject,
     addTable,

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

@@ -1,69 +1,162 @@
-import { SearchOutlined } from "@ant-design/icons";
-import { Form, Input, Popover, Select } from "antd";
+import { SearchOutlined, SwapOutlined } from "@ant-design/icons";
+import { Button, Descriptions, Form, Input, Popconfirm, Popover, Select } from "antd";
 import React from "react";
+import { RELATION_TYPE_OPTIONS } from "@/constants";
+import { useModel } from "umi";
+import noData from "@/assets/no-data.png";
+import { ColumnRelation } from "@/type";
 
 export default function RelationPanel() {
-  const RelationItem = () => {
-    const [collapsed, setCollapsed] = React.useState(false);
-    const relationOptions = [
-      { label: "一对一", value: "1:1" },
-      { label: "一对多", value: "1:n" },
-      { label: "多对多", value: "n:m" },
-    ];
-    return (
-      <div className="
-        border-b-solid 
-        border-b-[#e4e4e4] 
-        border-b-[1px]"
-      >
-        <div
-          className="
-          header 
-          flex 
-          items-center 
-          justify-between 
-          leading-[40px] 
-          hover:bg-[#fafafa] 
-          cursor-pointer 
-          m-b-[10px]"
-          onClick={() => setCollapsed(!collapsed)}
-        >
-          <div className="font-bold">table_1_id_fk关系</div>
-          <div>
-            <i className="iconfont icon-open m-r-10px" />
-          </div>
-        </div>
-        <div className="content overflow-hidden" style={{ height: collapsed ? 0 : "auto" }}>
-          <Form layout="vertical">
-            <div className="flex justify-between">
-              <Form.Item label="主键">relation_name</Form.Item>
-              <Form.Item label="外键">relation_name</Form.Item>
-              <Popover
-                trigger="click"
-                placement="right"
-                content={
-                  <div className="w-200px">
-                    
-                  </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>
-            <Form.Item label="对应关系" layout="vertical">
-              <Select className="w-full" options={relationOptions}></Select>
-            </Form.Item>
-          </Form>
-        </div>
-      </div>
-    );
+  const { project, addRelation, updateRelation, deleteRelation } =
+    useModel("erModel");
+  const [search, setSearch] = React.useState("");
+
+  const list = React.useMemo(() => {
+    return project.relations.filter((item) => item.name.includes(search));
+  }, [search, project.relations]);
+
+  const [active, setActive] = React.useState("");
+
+  const handleChange = (index: number, key: string, value: any) => {
+    const data = project.relations[index];
+    updateRelation({
+      ...data,
+      [key]: value,
+    });
+  };
+
+  const handleSwitchChange = (record: ColumnRelation) => {
+    updateRelation({
+      ...record,
+      primaryTable: record.foreignTable,
+      primaryKey: record.foreignKey,
+      foreignTable: record.primaryTable,
+      foreignKey: record.primaryKey,
+    })
+  }
+
+  const getPrimaryColumn = (item: ColumnRelation) => {
+    const tableItem = project.tables.find((table) => table.table.id === item.primaryTable);
+      
+    return {
+      table: tableItem?.table,
+      column: tableItem?.tableColumnList.find((column) => column.id === item.primaryKey)
+    }
+  };
+
+  const getForeignColumn = (item: ColumnRelation) => {
+    const tableItem = project.tables
+      .find((table) => table.table.id === item.foreignTable);
+    return {
+      table: tableItem?.table,
+      column: tableItem?.tableColumnList.find((column) => column.id === item.foreignKey)
+    }
   };
+
   return (
     <div className="px-12px">
-      <Input placeholder="输入关键字搜索" className="m-b-10px" suffix={<SearchOutlined />} />
-      <RelationItem />
+      <Input
+        placeholder="输入关键字搜索"
+        className="m-b-10px"
+        suffix={<SearchOutlined />}
+        value={search}
+        onChange={(e) => setSearch(e.target.value)}
+      />
+      {list.map((item, index) => {
+        return (
+          <div
+            className="
+            border-b-solid 
+            border-b-[#e4e4e4] 
+            border-b-[1px]"
+            key={item.id}
+          >
+            <div
+              className="
+              header 
+              flex 
+              items-center 
+              justify-between 
+              leading-[40px] 
+              hover:bg-[#fafafa] 
+              cursor-pointer 
+              m-b-[10px]"
+              onClick={() => setActive(active === item.id ? '' : item.id)}
+            >
+              <div className="font-bold">{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 }}
+            >
+              <Form layout="vertical">
+                <div className="flex justify-between">
+                  <Form.Item label="主键">
+                    {getPrimaryColumn(item)?.table?.schemaName}
+                  </Form.Item>
+                  <Form.Item label="外键">
+                    {getForeignColumn(item)?.table?.schemaName}
+                  </Form.Item>
+                  <Popover
+                    trigger="click"
+                    placement="right"
+                    content={<div className="min-w-200px">
+                      <Descriptions layout="vertical" bordered items={[
+                        {
+                          key: '1',
+                          label: '主键',
+                          children: <span>{getPrimaryColumn(item)?.table?.schemaName}({getPrimaryColumn(item)?.column?.schemaName})</span>
+                        },
+                        {
+                          key: '2',
+                          label: '外键',
+                          children: <span>{getForeignColumn(item)?.table?.schemaName}({getForeignColumn(item)?.column?.schemaName})</span>
+                        }
+                      ]}/>
+                      <Button className="w-full m-y-4px" icon={<SwapOutlined/>} type="primary" onClick={() => handleSwitchChange(item)}>交换</Button>
+                    </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>
+                <Form.Item label="对应关系" layout="vertical">
+                  <Select
+                    placeholder="请选择对应关系"
+                    className="w-full"
+                    options={RELATION_TYPE_OPTIONS}
+                    value={item.relationType}
+                    onChange={(val) => handleChange(index, "relationType", val)}
+                  ></Select>
+                </Form.Item>
+                <div>
+                  <Popconfirm
+                    okType="default"
+                    title="确定删除该关系?"
+                    okText="确定"
+                    cancelText="取消"
+                    onConfirm={() => deleteRelation(item.id)}
+                  >
+                    <Button color="danger" danger className="m-y-10px w-full">
+                      删除
+                    </Button>
+                  </Popconfirm>
+                </div>
+              </Form>
+            </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>
   );
 }

+ 2 - 2
apps/er-designer/src/type.d.ts

@@ -72,8 +72,8 @@ export interface ColumnRelation {
   foreignKey: string;
   // 外键表
   foreignTable: string;
-  // 关系类型 1 一对一 2 一对多 3 多对多
-  relationType: 1 | 2 | 3;
+  // 关系类型 1 一对一 2 一对多 3 多对一 4 多对
+  relationType: 1 | 2 | 3 | 4;
   // 连线样式
   style: Record<string, any>;
 }