Quellcode durchsuchen

feat: 添加关系连线样式配置

liaojiaxing vor 4 Monaten
Ursprung
Commit
6ddf69ae3b

BIN
apps/er-designer/src/assets/image/line-dashdot.png


BIN
apps/er-designer/src/assets/image/line-dashed.png


BIN
apps/er-designer/src/assets/image/line-dotted.png


BIN
apps/er-designer/src/assets/image/line-solid.png


+ 0 - 1
apps/er-designer/src/components/TableNode.tsx

@@ -3,7 +3,6 @@ import { register } from "@antv/x6-react-shape";
 import { Graph, Node } from "@antv/x6";
 import type { ColumnItem, TableItemType } from "@/type";
 import { DATA_TYPE_OPTIONS } from "@/constants";
-import { MinusOutlined } from "@ant-design/icons";
 import { useSessionStorageState } from "ahooks";
 
 function TableNode({ node, graph }: { node: Node; graph: Graph }) {

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

@@ -13,4 +13,11 @@ export enum RelationType {
   OneToMany = 2,
   ManyToOne = 3,
   ManyToMany = 4,
+}
+
+export enum RelationLineType {
+  Dashdot,
+  Dash,
+  Solid,
+  Dotted,
 }

+ 32 - 17
apps/er-designer/src/models/erModel.tsx

@@ -17,8 +17,9 @@ import type {
   TopicAreaInfo,
 } from "@/type";
 import { uuid } from "@/utils";
-import { RelationType } from "@/enum";
+import { RelationLineType, RelationType } from "@/enum";
 import { render } from "./renderer";
+import { useSessionStorageState } from "ahooks";
 
 import "@/components/TableNode";
 import "@/components/TopicNode";
@@ -56,6 +57,10 @@ export default function erModel() {
       tableWidth: 220,
     },
   });
+  const [_tabActiveKey, setTabActiveKey] =
+    useSessionStorageState("tabs-active-key");
+  const [_relationActive, setRelationActive] =
+    useSessionStorageState("relation-active");
 
   /**
    * 统一修改数据
@@ -218,6 +223,12 @@ export default function erModel() {
       }
     );
 
+    instance.on("edge:dblclick", (args: EventArgs["edge:dblclick"]) => {
+      console.log("edge:dblclick", args);
+      setTabActiveKey("2");
+      setRelationActive(args.cell.id);
+    });
+
     instance.on(
       "node:change:update:remark",
       function (args: EventArgs["cell:change:*"]) {
@@ -231,13 +242,14 @@ export default function erModel() {
     instance.bindKey("ctrl+c", onCopy);
     instance.bindKey("ctrl+x", onCut);
     instance.bindKey("ctrl+v", onPaste);
+    instance.bindKey("delete", onDelete);
     instance.bindKey("ctrl+down", () => {
       const scale = instance.zoom() - 0.1;
-      instance.zoom(scale < 0.2 ? 0.2 : scale);
-    });
+      instance.zoomTo(scale < 0.2 ? 0.2 : scale);
+    })
     instance.bindKey("ctrl+up", () => {
       const scale = instance.zoom() + 0.1;
-      instance.zoom(scale > 2 ? 2 : scale);
+      instance.zoomTo(scale > 2 ? 2 : scale);
     });
     instance.bindKey("ctrl+n", () => {
       // todo 新建
@@ -345,18 +357,17 @@ export default function erModel() {
     };
 
     // 子表插入到父表后面
-    setProject((state) => {
-      const tables = [...state.tables];
-      tables.splice(
-        tables.findIndex((item) => item.table.id === parentId) + 1,
-        0,
-        newTable
-      );
-      return {
-        ...state,
-        tables: parentId ? tables : [...state.tables, newTable],
-      };
-    });
+    const list = [...project.tables];
+    if (parentId) {
+      const index = list.findIndex((item) => item.table.id === parentId);
+      list.splice(index + 1, 0, newTable);
+    } else {
+      list.push(newTable);
+    }
+    setProject({
+      ...project,
+      tables: list
+    })
   };
 
   /**
@@ -566,7 +577,11 @@ export default function erModel() {
       foreignKey: target.columnId,
       foreignTable: target.tableId,
       relationType: 1,
-      style: {},
+      style: {
+        color: '#333',
+        lineType: RelationLineType.Solid,
+        width: 1
+      },
     };
     setProject((state) => {
       const obj = getRelations(state, newRelation);

+ 46 - 33
apps/er-designer/src/models/renderer.ts

@@ -1,7 +1,13 @@
-import { RelationType } from "@/enum";
-import { ProjectInfo, RemarkInfo, TableItemType, TopicAreaInfo } from "@/type";
+import { RelationLineType, RelationType } from "@/enum";
+import { ColumnRelation, ProjectInfo, RemarkInfo, TableItemType, TopicAreaInfo } from "@/type";
 import { Graph } from "@antv/x6";
 
+const dasharrayMap = {
+  // [RelationLineType.Dashdot]: "5 5,1 5",
+  [RelationLineType.Dash]: "5,5",
+  [RelationLineType.Solid]: "0",
+  [RelationLineType.Dotted]: "1,5",
+}
 export const render = (graph: Graph, project: ProjectInfo) => {
   const { tables, relations, topicAreas, remarks } = project;
   // 渲染表格
@@ -68,20 +74,10 @@ export const render = (graph: Graph, project: ProjectInfo) => {
     });
   };
   // 渲染关系
-  const renderRelationEdge = (
-    id: string,
-    source: {
-      tableId: string;
-      columnId: string;
-    },
-    target: {
-      tableId: string;
-      columnId: string;
-    }
-  ) => {
+  const renderRelationEdge = (relation: ColumnRelation) => {
     // 添加关系连线
     const relationEdge = graph?.addEdge({
-      id,
+      id: relation.id,
       router: {
         name: "er",
         args: {
@@ -92,19 +88,20 @@ export const render = (graph: Graph, project: ProjectInfo) => {
       connector: { name: "rounded" },
       attrs: {
         line: {
-          stroke: "#333",
-          strokeWidth: 1,
+          stroke: relation.style?.color || "#333",
+          strokeWidth: relation.style?.width || 1,
           targetMarker: null,
+          strokeDasharray: dasharrayMap[relation.style?.lineType as RelationLineType || RelationLineType.Solid]
         },
       },
       source: {
-        cell: source.tableId,
-        port: source.columnId + "_port2",
+        cell: relation.primaryTable,
+        port: relation.primaryKey + "_port2",
         anchor: "left",
       },
       target: {
-        cell: target.tableId,
-        port: target.columnId + "_port2",
+        cell: relation.foreignTable,
+        port: relation.foreignKey + "_port2",
         anchor: "left",
       },
       data: {
@@ -136,12 +133,19 @@ export const render = (graph: Graph, project: ProjectInfo) => {
         },
       },
     });
+
     if (project.setting.showRelation) {
       relationEdge?.appendLabel({
         attrs: {
           txt: {
-            text: 1,
+            text: relation.relationType === RelationType.OneToOne ||
+            relation.relationType === RelationType.OneToMany
+              ? "1"
+              : "n",
           },
+          bg: {
+            fill: relation.style?.color || "#333",
+          }
         },
         position: {
           distance: 25,
@@ -150,8 +154,14 @@ export const render = (graph: Graph, project: ProjectInfo) => {
       relationEdge?.appendLabel({
         attrs: {
           txt: {
-            text: 1,
+            text: relation.relationType === RelationType.OneToMany ||
+            relation.relationType === RelationType.ManyToMany
+              ? "n"
+              : "1",
           },
+          bg: {
+            fill: relation.style?.color || "#333",
+          }
         },
         position: {
           distance: -25,
@@ -210,17 +220,7 @@ export const render = (graph: Graph, project: ProjectInfo) => {
   // }, 100);
   relations.forEach((relation) => {
     if (!graph.getCellById(relation.id)) {
-      renderRelationEdge(
-        relation.id,
-        {
-          tableId: relation.primaryTable,
-          columnId: relation.primaryKey,
-        },
-        {
-          tableId: relation.foreignTable,
-          columnId: relation.foreignKey,
-        }
-      );
+      renderRelationEdge(relation);
     } else {
       const relationEdge = graph.getCellById(relation.id);
       if (relationEdge.isEdge()) {
@@ -235,6 +235,9 @@ export const render = (graph: Graph, project: ProjectInfo) => {
                     ? "1"
                     : "n",
               },
+              bg: {
+                fill: relation.style?.color || "#333",
+              }
             },
             position: {
               distance: 25,
@@ -249,6 +252,9 @@ export const render = (graph: Graph, project: ProjectInfo) => {
                     ? "n"
                     : "1",
               },
+              bg: {
+                fill: relation.style?.color || "#333",
+              }
             },
             position: {
               distance: -25,
@@ -257,6 +263,13 @@ export const render = (graph: Graph, project: ProjectInfo) => {
         } else {
           relationEdge.setLabels([]);
         }
+        relationEdge.setAttrs({
+          line: {
+            stroke: relation.style?.color || "#333",
+            strokeWidth: relation.style?.width || 1,
+            strokeDasharray: dasharrayMap[relation.style?.lineType as RelationLineType || RelationLineType.Solid]
+          },
+        });
       }
     }
   });

+ 147 - 68
apps/er-designer/src/pages/er/components/Menu.tsx

@@ -60,9 +60,25 @@ export default function Menu() {
       label: "文件",
       type: "group",
       children: [
-        { key: "1-1", label: "新建" },
+        {
+          key: "1-1",
+          label: (
+            <span className="flex items-center justify-between">
+              <span>新建</span>
+              <span className="color-#666">ctrl+n</span>
+            </span>
+          ),
+        },
         { key: "1-2", label: "新标签页打开" },
-        { key: "1-3", label: "保存" },
+        {
+          key: "1-3",
+          label: (
+            <span className="flex items-center justify-between">
+              <span>保存</span>
+              <span className="color-#666">ctrl+s</span>
+            </span>
+          ),
+        },
         { key: "1-4", label: "保存为模版" },
         { key: "1-5", label: "发布模版" },
         { key: "1-6", label: "同步到数据表" },
@@ -78,13 +94,67 @@ export default function Menu() {
       label: "编辑",
       type: "group",
       children: [
-        { key: "1-1", label: "撤销", onClick: () => canUndo && onUndo() },
-        { key: "1-2", label: "恢复", onClick: () => canRedo && onRedo() },
+        {
+          key: "1-1",
+          label: (
+            <span className="flex items-center justify-between">
+              <span>撤销</span>
+              <span className="color-#666">ctrl+z</span>
+            </span>
+          ),
+          onClick: () => canUndo && onUndo(),
+        },
+        {
+          key: "1-2",
+          label: (
+            <span className="flex items-center justify-between">
+              <span>恢复</span>
+              <span className="color-#666">ctrl+y</span>
+            </span>
+          ),
+          onClick: () => canRedo && onRedo(),
+        },
         { key: "1-3", label: "清除", onClick: handleClean },
-        { key: "1-4", label: "剪切", onClick: onCut },
-        { key: "1-5", label: "复制", onClick: onCopy },
-        { key: "1-6", label: "粘贴", onClick: onPaste },
-        { key: "1-7", label: "删除", onClick: onDelete },
+        {
+          key: "1-4",
+          label: (
+            <span className="flex items-center justify-between">
+              <span>剪切</span>
+              <span className="color-#666">ctrl+x</span>
+            </span>
+          ),
+          onClick: onCut,
+        },
+        {
+          key: "1-5",
+          label: (
+            <span className="flex items-center justify-between">
+              <span>复制</span>
+              <span className="color-#666">ctrl+c</span>
+            </span>
+          ),
+          onClick: onCopy,
+        },
+        {
+          key: "1-6",
+          label: (
+            <span className="flex items-center justify-between">
+              <span>粘贴</span>
+              <span className="color-#666">ctrl+v</span>
+            </span>
+          ),
+          onClick: onPaste,
+        },
+        {
+          key: "1-7",
+          label: (
+            <span className="flex items-center justify-between">
+              <span>删除</span>
+              <span className="color-#666">delete</span>
+            </span>
+          ),
+          onClick: onDelete,
+        },
       ],
     },
     {
@@ -167,12 +237,22 @@ export default function Menu() {
         { key: "1-7", label: "重置视图", onClick: () => graph?.zoomTo(1) },
         {
           key: "1-8",
-          label: "放大",
+          label: (
+            <span className="flex items-center justify-between">
+              <span>放大</span>
+              <span className="color-#666">ctrl+up</span>
+            </span>
+          ),
           onClick: () => handleZoom((graph?.zoom() || 1) + 0.2),
         },
         {
           key: "1-9",
-          label: "缩小",
+          label: (
+            <span className="flex items-center justify-between">
+              <span>缩小</span>
+              <span className="color-#666">ctrl+down</span>
+            </span>
+          ),
           onClick: () => handleZoom((graph?.zoom() || 1) - 0.2),
         },
         {
@@ -215,7 +295,7 @@ export default function Menu() {
         {
           key: "1-4",
           label: "表格宽度",
-          onClick: () => setOpen(true)
+          onClick: () => setOpen(true),
         },
       ],
     },
@@ -242,66 +322,65 @@ export default function Menu() {
 
   return (
     <>
-    <div className="flex-1 flex items-center">
-      {contextHolder}
-      <div className="logo h-48px m-l-12px">
-        <svg className="icon h-48px w-48px" aria-hidden="true">
-          <use xlinkHref="#icon-shujujianmo"></use>
-        </svg>
-      </div>
-      <div className="h-full">
-        <div className="leading-32px flex items-center">
-          <Input
-            className="text-24px max-w-200px"
-            variant="borderless"
-            value={"模型名称"}
-          />
-          <div className="bg-#eee text-12px leading-20px color-#666 rounded-4px p-x-4px p-y-2px">
-            上次保存时间:2024-12-13 13:21:23
-          </div>
+      <div className="flex-1 flex items-center">
+        {contextHolder}
+        <div className="logo h-48px m-l-12px">
+          <svg className="icon h-48px w-48px" aria-hidden="true">
+            <use xlinkHref="#icon-shujujianmo"></use>
+          </svg>
         </div>
-        <div className="flex">
-          {menuData.map((item) => {
-            return (
-              <Dropdown
-                key={item.key}
-                menu={{ items: item.children, style: { width: 200 } }}
-                placement="bottomLeft"
-                open={openKey === item.key}
-                onOpenChange={(nextOpen, info) =>
-                  handleOpenChange(nextOpen, info, item.key)
-                }
-              >
-                <Button type="text" size="small">
-                  {item.label}
-                </Button>
-              </Dropdown>
-            );
-          })}
+        <div className="h-full">
+          <div className="leading-32px flex items-center">
+            <Input
+              className="text-24px max-w-200px"
+              variant="borderless"
+              value={"模型名称"}
+            />
+            <div className="bg-#eee text-12px leading-20px color-#666 rounded-4px p-x-4px p-y-2px">
+              上次保存时间:2024-12-13 13:21:23
+            </div>
+          </div>
+          <div className="flex">
+            {menuData.map((item) => {
+              return (
+                <Dropdown
+                  key={item.key}
+                  menu={{ items: item.children, style: { width: 200 } }}
+                  placement="bottomLeft"
+                  open={openKey === item.key}
+                  onOpenChange={(nextOpen, info) =>
+                    handleOpenChange(nextOpen, info, item.key)
+                  }
+                >
+                  <Button type="text" size="small">
+                    {item.label}
+                  </Button>
+                </Dropdown>
+              );
+            })}
+          </div>
         </div>
       </div>
-    </div>
-    <Modal
-      title="设置表格宽度"
-      centered
-      width={440}
-      open={open}
-      okText="确定"
-      onOk={() => setOpen(false)}
-      onCancel={() => setOpen(false)}
-      footer={(_, {OkBtn}) => {
-        return <OkBtn/>
-      }}
-    >
-      <InputNumber
-        className="w-full"
-        min={150}
-        max={1000}
-        value={project.setting.tableWidth}
-        onChange={(value) => handleChangeSetting("tableWidth", value)}
-      />
-    </Modal>
+      <Modal
+        title="设置表格宽度"
+        centered
+        width={440}
+        open={open}
+        okText="确定"
+        onOk={() => setOpen(false)}
+        onCancel={() => setOpen(false)}
+        footer={(_, { OkBtn }) => {
+          return <OkBtn />;
+        }}
+      >
+        <InputNumber
+          className="w-full"
+          min={150}
+          max={1000}
+          value={project.setting.tableWidth}
+          onChange={(value) => handleChangeSetting("tableWidth", value)}
+        />
+      </Modal>
     </>
-    
   );
 }

+ 1 - 1
apps/er-designer/src/pages/er/components/Navigator.tsx

@@ -23,7 +23,7 @@ export default function Navigator() {
   }, [graph, mapRef.current]);
 
   return (
-    <div className="absolute right-20px bottom-20px bg-#fafafa w-300px">
+    <div className="absolute right-20px bottom-20px bg-#fafafa w-300px z-2">
       <div className="text-12px color-#333 px-10px py-4px flex items-center cursor-pointer justify-between" onClick={() => setShow(!show)}>
         <span>导航</span>
         <i className="iconfont icon-open cursor-pointer" style={ show ? { transform: "rotate(180deg)" } : {}}/>

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

@@ -4,6 +4,7 @@ import {
   Descriptions,
   Form,
   Input,
+  InputNumber,
   Popconfirm,
   Popover,
   Select,
@@ -13,17 +14,27 @@ import { RELATION_TYPE_OPTIONS } from "@/constants";
 import { useModel } from "umi";
 import noData from "@/assets/no-data.png";
 import { ColumnRelation } from "@/type";
+import { useSessionStorageState } from "ahooks";
+import CustomColorPicker from "@/components/CustomColorPicker";
+import { RelationLineType } from "@/enum";
+
+// import line1 from "@/assets/image/line-dashdot.png";
+import line2 from "@/assets/image/line-dashed.png";
+import line3 from "@/assets/image/line-solid.png";
+import line4 from "@/assets/image/line-dotted.png";
 
 export default function RelationPanel() {
-  const { project, addRelation, updateRelation, deleteRelation } =
-    useModel("erModel");
+  const { project, 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 [active, setActive] = useSessionStorageState("relation-active", {
+    defaultValue: "",
+    listenStorageChange: true,
+  });
 
   const handleChange = (index: number, key: string, value: any) => {
     const data = project.relations[index];
@@ -172,6 +183,52 @@ export default function RelationPanel() {
                     </div>
                   </Popover>
                 </div>
+                <div className="flex justify-between">
+                  <Form.Item label="连线类型" className="flex-1 m-r-12px">
+                    <Select
+                      placeholder="请选择"
+                      options={[
+                        { value: RelationLineType.Dash, label: <img className="h-16px" src={line2}/> },
+                        { value: RelationLineType.Solid, label: <img className="h-16px" src={line3}/> },
+                        { value: RelationLineType.Dotted, label: <img  className="h-16px"src={line4}/> },
+                      ]}
+                      value={item.style?.lineType}
+                      onChange={(val) =>
+                        handleChange(index, "style", {
+                          ...item.style,
+                          lineType: val,
+                        })
+                      }
+                    />
+                  </Form.Item>
+                  <Form.Item label="宽度" className="flex-1 m-r-12px">
+                    <InputNumber
+                      min={1}
+                      max={10}
+                      value={item.style?.width}
+                      className="w-full"
+                      onChange={(val) =>
+                        handleChange(index, "style", {
+                          ...item.style,
+                          width: val,
+                        })
+                      }
+                    />
+                  </Form.Item>
+                  <Form.Item label="颜色">
+                    <CustomColorPicker
+                      color={item.style?.color}
+                      onChange={(color) =>
+                        handleChange(index, "style", { ...item.style, color })
+                      }
+                    >
+                      <div
+                        className="rounded-4px cus-btn w-32px h-32px bg-#eee flex-none cursor-pointer shadow-inner"
+                        style={{ background: item.style?.color || "#eee" }}
+                      ></div>
+                    </CustomColorPicker>
+                  </Form.Item>
+                </div>
                 <Form.Item label="对应关系" layout="vertical">
                   <Select
                     placeholder="请选择对应关系"