Prechádzať zdrojové kódy

feat: 添加表格绑定

liaojiaxing 4 mesiacov pred
rodič
commit
0174596d47

+ 37 - 43
apps/er-designer/src/components/TableNode.tsx

@@ -1,65 +1,64 @@
 import React, { useEffect, useMemo, useRef } from "react";
 import { register } from "@antv/x6-react-shape";
 import { Graph, Node } from "@antv/x6";
-import type { ColumnItem, ViewTable } from "@/type";
+import type { ColumnItem, TableItemType, ViewTable } from "@/type";
+import { DATA_TYPE_OPTIONS } from "@/constants";
 function TableNode({ node, graph }: { node: Node; graph: Graph }) {
-  const { table, tableColumnList } = node.getData<{
-    table: ViewTable;
-    tableColumnList: ColumnItem[];
-  }>();
-  const headerRef = useRef<HTMLDivElement>(null);
-  const contentRef = useRef<HTMLDivElement>(null);
+  const { table, tableColumnList } = node.getData<TableItemType>();
+  const containerRef = useRef<HTMLDivElement>(null);
 
-  // 重置宽高
   useEffect(() => {
-    if (table.tableExtInfoDto.width < 0) {
-      const width = headerRef.current?.offsetWidth || 0;
-      const height = (headerRef.current?.offsetHeight || 0) + (contentRef.current?.offsetHeight || 0);
-      if(width && height) {
-        node.resize(width, height);
-      }
+    const container = containerRef.current;
+    if (container?.clientHeight) {
+      node.setSize(container.clientWidth, container.clientHeight);
     }
-  }, []);
-
-  // 主键字段
-  const primaryKeyList = tableColumnList.filter((item) => item.isUnique);
-
-  // 非主键字段
-  const otherKeyList = tableColumnList.filter((item) => !item.isUnique);
+  }, [tableColumnList.length]);
 
   const FiledItem = ({ record }: { record: ColumnItem }) => {
+    const type = DATA_TYPE_OPTIONS.find((item) => item.value === record.type);
     return (
       <div className="w-full flex py-4px px-8px">
-        {record.isUnique ? (
-          <span className="m-r-4px">
-            <i className="iconfont icon-key-fill color-#efc553 m-r--4px" />
-            <span className="text-12px color-#f15359">P</span>
-            <span className="text-12px color-#8dafd1">F</span>
+        <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 "/>
+            {record.schemaName}({record.cn_name})
           </span>
-        ) : (
           <span>
-            <i className="iconfont icon-24_beizhu color-#efc553 m-r-4px text-12px" />
+            {record.isUnique ? (
+              <span className="m-r-4px">
+                <i className="iconfont icon-key-fill color-#efc553 m-r--4px" />
+                <span className="text-12px color-#f15359">P</span>
+                <span className="text-12px color-#8dafd1">F</span>
+              </span>
+            ) : (
+              <span>
+                <i className="iconfont icon-24_beizhu color-#efc553 m-r-4px text-12px" />
+              </span>
+            )}
+            <span>{type?.label}</span>
           </span>
-        )}
-        <span className="flex-1 truncate">
-          {record.schemaName}({record.name}):VARCHAR2(20)
         </span>
       </div>
     );
   };
 
   return (
-    <div className="w-full h-full border border-1px border-solid border-#333 flex flex-col overflow-hidden rounded-8px">
+    <div
+      ref={containerRef}
+      className="w-full border border-1px border-solid border-#333 flex flex-col overflow-hidden rounded-8px"
+    >
+      <div
+        className="w-full h-10px bg-#eee"
+        style={{ background: table.style?.color || "#eee" }}
+      ></div>
       <div
-        ref={headerRef}
         className="
           header 
           border-b-solid 
           border-b-1px 
           border-b-#333 
           truncate 
-          bg-#99b4d1 
-          text-center 
+          bg-#e4e4e7 
           py-4px 
           font-bold 
           text-14px 
@@ -67,16 +66,11 @@ function TableNode({ node, graph }: { node: Node; graph: Graph }) {
           px-8px
         "
       >
-        {table.schemaName}({table.name})
+        {table.schemaName}({table.cn_name})
       </div>
-      <div ref={contentRef} className="bg-#fafafa flex-1">
-        <div className="primary-key border-b-solid border-b-1px border-b-#333 py-4px min-h-32px">
-          {primaryKeyList.map((item) => {
-            return <FiledItem record={item} />;
-          })}
-        </div>
+      <div className="bg-#fafafa flex-1">
         <div className="field-info">
-          {otherKeyList.map((item) => {
+          {tableColumnList.map((item) => {
             return <FiledItem record={item} />;
           })}
         </div>

+ 20 - 0
apps/er-designer/src/components/TopicNode.tsx

@@ -0,0 +1,20 @@
+
+import React, { useEffect, useMemo, useRef } from "react";
+import { register } from "@antv/x6-react-shape";
+import { Graph, Node } from "@antv/x6";
+import type { TopicAreaInfo } from "@/type";
+
+const TopicAreaNode = ({ node, graph }: { node: Node; graph: Graph }) => {
+  const { style, name } = node.getData<TopicAreaInfo>();
+  return <div className="w-full h-full" style={style}>
+    <p>{name}</p>
+  </div>
+};
+
+register({
+  shape: "topic-node",
+  component: TopicAreaNode,
+  width: 300,
+  height: 300,
+  effect: ["data"],
+});

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

@@ -0,0 +1,28 @@
+import { DataType } from "@/enum";
+
+export interface DataTypeOption {
+  label: string;
+  value: DataType
+}
+
+/**
+ * 数据类型
+ */
+export const DATA_TYPE_OPTIONS: DataTypeOption[] = [
+  {label: DataType[DataType.Int], value: DataType.Int},
+  {label: DataType[DataType.Decimal], value: DataType.Decimal},
+  {label: DataType[DataType.Real], value: DataType.Real},
+  {label: DataType[DataType.Nvarchar], value: DataType.Nvarchar},
+  {label: DataType[DataType.DateTime], value: DataType.DateTime},
+  {label: DataType[DataType.Bit], value: DataType.Bit},
+];
+
+/**
+ * 表类型
+ */
+export const TABLE_TYPE_OPTIONS = [
+  {label: "系统表", value: 1},
+  {label: "流程表", value: 2},
+  {label: "业务表", value: 3},
+  {label: "视图", value: 4},
+];

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

@@ -0,0 +1,9 @@
+// 数据库数据类型
+export enum DataType {
+  Int = 1,
+  Nvarchar = 2,
+  Real = 3,
+  Bit = 4,
+  Decimal = 5,
+  DateTime = 6
+}

+ 12 - 0
apps/er-designer/src/layouts/index.less

@@ -1,3 +1,15 @@
 body {
   margin: 0;
+}
+
+// 滚动条
+::-webkit-scrollbar {
+  width: 4px;
+  height: 4px;
+}
+::-webkit-scrollbar-track {
+  background: #f1f1f1;
+}
+::-webkit-scrollbar-thumb {
+  background: #a19f9f;
 }

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

@@ -6,9 +6,35 @@ import { Scroller } from "@antv/x6-plugin-scroller";
 import "@/components/TableNode";
 import { GetAllDesignTables } from "@/api";
 import { useRequest } from "umi";
+import type { ProjectInfo, TableItemType } from "@/type";
+import { uuid } from "@/utils";
 export default function erModel() {
   const graphRef = useRef<Graph>();
   const [graph, setGraph] = useState<Graph>();
+  const [project, setProject] = useState<ProjectInfo>({
+    id: "1",
+    name: "项目1",
+    folderId: "root",
+    creationTime: "2024-12-12 12:12:12",
+    creatorUser: "管理员",
+    lastModificationTime: "2024-12-12 12:12:12",
+    description: "模型描述",
+    isTemplate: false,
+    industry: "互联网",
+    publishStatus: "未发布",
+    tables: [],
+    relations: [],
+    TopicAreas: [],
+    remarks: [],
+    history: [],
+    setting: {
+      showMenu: true,
+      showSidebar: true,
+      showColumnDetail: true,
+      showGrid: true,
+      showRelation: true,
+    },
+  });
 
   const initGraph = (container: HTMLElement) => {
     const instance = new Graph({
@@ -28,7 +54,7 @@ export default function erModel() {
       },
       grid: {
         visible: true,
-        size: 10
+        size: 10,
       },
       background: {
         color: "#F2F7FA",
@@ -47,46 +73,151 @@ export default function erModel() {
     });
 
     instance.use(new History());
-    instance.use(new Transform({
-      resizing: {
-        enabled: true,
-      }
-    }));
+    instance.use(
+      new Transform({
+        resizing: {
+          enabled: true,
+        },
+
+      })
+    );
     instance.use(new Scroller());
 
     setGraph(instance);
     graphRef.current = instance;
   };
 
-  const { data, run, loading } = useRequest(() => GetAllDesignTables({ groupType: "" }), {
-    manual: true
-  });
-
-  useEffect(() => {
-    const { result } = data || {};
-    if(result && graphRef.current) {
-      const { lines, objects, points, tableColumnList, tableList } = result;
-      // 创建表格节点
-      tableList?.forEach((table) => {
-        graphRef.current?.addNode({
-          shape: "table-node",
-          x: 300,
-          y: 100,
-          width: 200,
-          height: 200,
-          data: {
-            table,
-            tableColumnList: tableColumnList?.filter(item => item.businessTableId === table.id)
-          }
-        })
-      });
+  const { data, run, loading } = useRequest(
+    () => GetAllDesignTables({ groupType: "" }),
+    {
+      manual: true,
     }
-  }, [graphRef.current, data]);
+  );
+
+  /**
+   * 添加表
+   */
+  const addTable = () => {
+    const tableId = uuid();
+    const newTable: TableItemType = {
+      table: {
+        aliasName: "newtable",
+        creationTime: "",
+        creatorUserId: "",
+        displayOrder: 1,
+        id: tableId,
+        isDeleted: false,
+        langDescription: "",
+        langName: "",
+        name: "",
+        cn_name: "新建表",
+        en_name: "new table",
+        parentBusinessTableId: "",
+        schemaName: "new_table",
+        type: 1,
+        updateTime: "",
+        openSync: false,
+        style: {
+          color: "#616161",
+          x: 0,
+          y: 0,
+        },
+      },
+      tableColumnList: [
+        {
+          aggregateEnable: false,
+          businessTableId: tableId,
+          charset: "",
+          defaultValue: "",
+          displayEnable: false,
+          displayOrder: 1,
+          groupByEnable: false,
+          id: uuid(),
+          isAggregateEnable: false,
+          isDisplayEnable: false,
+          isGroupByEnable: false,
+          isLinkEnable: false,
+          isOrderByEnable: false,
+          isPreDefined: true,
+          isRequired: true,
+          isUnique: false,
+          isWhereEnable: false,
+          langName: "120d5c4e-a5d8-4d49-b5f5-492feb37e6c6",
+          linkEnable: false,
+          maxLength: 50,
+          name: "Id",
+          orderByEnable: false,
+          preDefined: true,
+          precision: 0,
+          required: true,
+          scale: 0,
+          schemaName: "id",
+          type: 2,
+          unique: false,
+          whereEnable: false,
+          cn_name: "id",
+          en_name: "id",
+        },
+      ],
+    };
+
+    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: 1,
+    });
+  };
+
+  /**
+   * 更新表
+   * @param table 
+   */
+  const updateTable = (table: TableItemType) => {
+    setProject({
+      ...project,
+      tables: project.tables.map((item) => {
+        if (item.table.id === table.table.id) {
+          const tableNode = graphRef.current?.getCellById(table.table.id);
+          tableNode?.setData(table);
+          return table;
+        }
+        return item;
+      }),
+    });
+  }
+
+  /**
+   * 删除表
+   * @param tableId 
+   */
+  const deleteTable = (tableId: string) => {
+    setProject({
+      ...project,
+      tables: project.tables.filter((item) => item.table.id !== tableId),
+    });
+    graphRef.current?.removeCell(tableId);
+    // todo删除关联关系
+  };
 
   return {
     initGraph,
     graph,
     graphRef,
-    loading
+    loading,
+    project,
+    setProject,
+    addTable,
+    updateTable,
+    deleteTable,
   };
 }

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

@@ -64,7 +64,7 @@ export default function Menu() {
       ],
     },
     {
-      key: "2",
+      key: "5",
       label: "帮助",
       type: "group",
       children: [

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

@@ -4,6 +4,7 @@ import { MiniMap } from "@antv/x6-plugin-minimap";
 export default function Navigator() {
   const { graph } = useModel("erModel");
   const mapRef = React.useRef<HTMLDivElement>(null);
+  const [show, setShow] = React.useState(true);
 
   useEffect(() => {
     console.log("graph", graph);
@@ -20,7 +21,13 @@ export default function Navigator() {
   }, [graph, mapRef.current]);
 
   return (
-    <div className="w-full h-full flex-1" ref={mapRef}>
+    <div className="absolute right-20px bottom-20px bg-#fafafa w-300px">
+      <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)" } : {}}/>
+      </div>
+      <div className="w-300px h-300px" ref={mapRef} style={{ height: show ? 300 : 0, overflow: "hidden" }}>
+      </div>
     </div>
   );
 }

+ 263 - 39
apps/er-designer/src/pages/er/components/TableItem.tsx

@@ -3,32 +3,161 @@ import {
   HolderOutlined,
   MoreOutlined,
 } from "@ant-design/icons";
-import { Col, Row, Form, Input, Select, Button, Tooltip, Popover, Popconfirm } from "antd";
-import React from "react";
+import {
+  Col,
+  Row,
+  Form,
+  Input,
+  Select,
+  Button,
+  Tooltip,
+  Popover,
+  Popconfirm,
+} from "antd";
+import React, { useState } from "react";
 import CustomColorPicker from "@/components/CustomColorPicker";
+import { ColumnItem, TableItemType } from "@/type";
+import { TABLE_TYPE_OPTIONS, DATA_TYPE_OPTIONS } from "@/constants";
+import { uuid } from "@/utils";
+import { DataType } from "@/enum";
 
-export default function TableItem() {
+export default function TableItem({
+  data,
+  onChange,
+}: {
+  data: TableItemType;
+  onChange: (data: TableItemType) => void;
+}) {
   const [collapsed, setCollapsed] = React.useState(false);
-  const ColumnItem = () => {
+
+  const [table, setTable] = React.useState(data?.table);
+  const [tableColumnList, setTableColumnList] = React.useState(data?.tableColumnList);
+
+  const handleTableChange = (key: string, value: any) => {
+    onChange({
+      tableColumnList,
+      table: { ...table, [key]: value },
+    });
+    setTable({ ...table, [key]: value });
+  };
+
+  const handleColumnChange = (index: number, value: any) => {
+    tableColumnList[index] = { ...tableColumnList[index], ...value };
+    onChange({
+      tableColumnList,
+      table,
+    });
+    setTableColumnList([...tableColumnList]);
+  };
+
+  // 添加表
+  const handleAddChildTable = () => {};
+
+  // 添加字段
+  const handleAddColumn = () => {
+    const newColumn: ColumnItem = {
+      id: uuid(),
+      schemaName: "",
+      name: "",
+      en_name: "",
+      cn_name: "",
+      type: DataType.Nvarchar,
+      maxLength: 100,
+      precision: 0,
+      scale: 0,
+      isRequired: false,
+      isUnique: false,
+      isPreDefined: false,
+      defaultValue: "",
+    };
+    onChange({
+      table,
+      tableColumnList: [...tableColumnList, newColumn],
+    });
+    setTableColumnList([...tableColumnList, newColumn]);
+  };
+
+  // 删除表
+  const handleDeleteTable = () => {};
+
+  const ColumnItem = ({
+    column,
+    onChange,
+  }: {
+    column: ColumnItem;
+    onChange: (data: ColumnItem) => void;
+  }) => {
+    const [data, setData] = useState(column);
+    const handleChange = (key: string, value: any) => {
+      onChange({
+        ...data,
+        [key]: value,
+      });
+      setData({
+        ...data,
+        [key]: value,
+      });
+    };
     return (
-      <div className="column-item flex gap-4px items-center jutify-space-between hover:bg-gray-100">
+      <div 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="字段编码" />
+          <Input
+            placeholder="编码"
+            value={data.schemaName}
+            className="flex-1"
+            onChange={(e) => handleChange("schemaName", e.target.value)}
+          />
         </Tooltip>
         <Tooltip title="字段类型">
-          <Select placeholder="字段" />
-        </Tooltip>
-        <Tooltip title="字段长度,精度用逗号分割">
-          <Input placeholder="长度" />
+          <Select
+            placeholder="类型"
+            className="w-80px"
+            options={DATA_TYPE_OPTIONS}
+            value={data.type}
+            onChange={(value) => handleChange("type", value)}
+            dropdownStyle={{width: 120}}
+          />
         </Tooltip>
         <Tooltip title="非空">
-          <div className="rounded-4px cus-btn w-32px h-32px bg-#eee flex-none text-center leading-32px cursor-pointer hover:bg-#ddd">
+          <div
+            className="
+            rounded-4px 
+            cus-btn 
+            w-32px 
+            h-32px 
+            bg-#eee 
+            flex-none 
+            text-center 
+            leading-32px 
+            cursor-pointer 
+            hover:bg-#ddd"
+            style={
+              data.isRequired ? { background: "#1677ff", color: "#fff" } : {}
+            }
+            onClick={() => handleChange("isRequired", !data.isRequired)}
+          >
             !
           </div>
         </Tooltip>
         <Tooltip title="唯一">
-          <div className="rounded-4px cus-btn w-32px h-32px bg-#eee flex-none text-center leading-32px cursor-pointer hover:bg-#ddd">
+          <div
+            className="
+            rounded-4px 
+            cus-btn 
+            w-32px 
+            h-32px 
+            bg-#eee 
+            flex-none 
+            text-center 
+            leading-32px 
+            cursor-pointer 
+            hover:bg-#ddd"
+            style={
+              data.isUnique ? { background: "#1677ff", color: "#fff" } : {}
+            }
+            onClick={() => handleChange("isUnique", !data.isRequired)}
+          >
             1
           </div>
         </Tooltip>
@@ -39,8 +168,18 @@ export default function TableItem() {
             <div className="w-200px">
               <Form layout="vertical">
                 <Form.Item label="字段名称" name="pkName">
-                  <Input className="w-full" placeholder="中文" />
-                  <Input className="w-full" placeholder="英文" />
+                  <Input
+                    className="w-full"
+                    placeholder="中文"
+                    value={data.cn_name}
+                    onChange={(e) => handleChange("cn_name", e.target.value)}
+                  />
+                  <Input
+                    className="w-full"
+                    placeholder="英文"
+                    value={data.en_name}
+                    onChange={(e) => handleChange("en_name", e.target.value)}
+                  />
                 </Form.Item>
                 <Form.Item label="描述" name="pkName">
                   <Input.TextArea
@@ -52,10 +191,30 @@ export default function TableItem() {
                     placeholder="描述英文..."
                   />
                 </Form.Item>
+                <Form.Item label="长度">
+                  <Input
+                    placeholder="请输入"
+                    value={data.maxLength}
+                    onChange={(e) => handleChange("maxLength", e.target.value)}
+                  />
+                </Form.Item>
+                <Form.Item label="精度">
+                  <Input
+                    placeholder="请输入"
+                    value={data.precision}
+                    onChange={(e) => handleChange("precision", e.target.value)}
+                  />
+                </Form.Item>
                 <Form.Item label="默认值" name="pkName">
-                  <Input className="w-full" placeholder="默认值" />
+                  <Input
+                    className="w-full"
+                    placeholder="默认值"
+                    value={data.defaultValue}
+                    onChange={(e) =>
+                      handleChange("defaultValue", e.target.value)
+                    }
+                  />
                 </Form.Item>
-
               </Form>
             </div>
           }
@@ -69,7 +228,8 @@ export default function TableItem() {
   };
 
   return (
-    <div className="
+    <div
+      className="
       w-full 
       border-l-solid 
       border-l-[#e6e6e6] 
@@ -77,7 +237,12 @@ export default function TableItem() {
       border-b-solid 
       border-b-[#e4e4e4] 
       border-b-[1px] 
-      p-l-16px">
+      m-b-4px
+      p-l-16px"
+      style={{
+        borderLeftColor: table.style?.color || '#eee'
+      }}
+    >
       <div
         className="
         header 
@@ -91,48 +256,76 @@ export default function TableItem() {
         m-b-[10px]"
         onClick={() => setCollapsed(!collapsed)}
       >
-        <div className="font-bold">数据表名</div>
+        <div className="font-bold">
+          {table.schemaName}({table.cn_name})
+        </div>
         <div>
           <Popover
             trigger="click"
             placement="right"
             content={
-              <div className="w-200px">
+              <div className="w-200px" onClick={(e) => e.stopPropagation()}>
                 <Form layout="vertical">
                   <Form.Item label="表名称" name="pkName">
-                    <Input className="w-full" placeholder="中文" />
-                    <Input className="w-full" placeholder="英文" />
-                  </Form.Item>
-                  <Form.Item label="描述" name="pkName">
-                    <Input.TextArea
+                    <Input
                       className="w-full"
-                      placeholder="中文..."
+                      placeholder="中文"
+                      value={table.cn_name}
+                      onChange={(e) =>
+                        handleTableChange("cn_name", e.target.value)
+                      }
                     />
-                    <Input.TextArea
+                    <Input
                       className="w-full"
-                      placeholder="英文..."
+                      placeholder="英文"
+                      value={table.en_name}
+                      onChange={(e) =>
+                        handleTableChange("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>
               </div>
             }
           >
-            <i className="iconfont icon-shezhi mr-[10px] cursor-pointer" />
+            <i
+              className="iconfont icon-shezhi mr-[10px] cursor-pointer"
+              onClick={(e) => e.stopPropagation()}
+            />
           </Popover>
           <i className="iconfont icon-open" />
         </div>
       </div>
-      <div className="content overflow-hidden" style={{ height: collapsed ? 0 : "auto" }}>
+      <div
+        className="content overflow-hidden"
+        style={{ height: collapsed ? 0 : "auto" }}
+      >
         <Form layout="horizontal" labelCol={{ span: 8 }}>
           <Row gutter={8}>
             <Col span={12}>
               <Form.Item label="编码">
-                <Input />
+                <Input
+                  placeholder="请输入"
+                  value={table.schemaName}
+                  onChange={(e) =>
+                    handleTableChange("schemaName", e.target.value)
+                  }
+                />
               </Form.Item>
             </Col>
             <Col span={12}>
               <Form.Item label="别名">
-                <Input />
+                <Input
+                  placeholder="请输入"
+                  value={table.aliasName}
+                  onChange={(e) =>
+                    handleTableChange("aliasName", e.target.value)
+                  }
+                />
               </Form.Item>
             </Col>
           </Row>
@@ -141,18 +334,41 @@ export default function TableItem() {
             labelCol={{ span: 4 }}
             wrapperCol={{ span: 21 }}
           >
-            <Select />
+            <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>
-            <div className="rounded-4px cus-btn w-32px h-32px bg-#eee flex-none cursor-pointer shadow-inner"></div>
+          <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">添加子表</Button>
-            <Button type="primary">添加字段</Button>
-            <Popconfirm okType="primary" title="确定删除该表?" okText="确定" cancelText="取消">
+            <Button type="primary" onClick={handleAddChildTable}>
+              添加子表
+            </Button>
+            <Button type="primary" onClick={handleAddColumn}>
+              添加字段
+            </Button>
+            <Popconfirm
+              okType="primary"
+              title="确定删除该表?"
+              okText="确定"
+              cancelText="取消"
+              onConfirm={handleDeleteTable}
+            >
               <div className="rounded-4px cus-btn w-32px h-32px bg-#eee flex-none text-center leading-32px color-red cursor-pointer">
                 <DeleteOutlined />
               </div>
@@ -161,7 +377,15 @@ export default function TableItem() {
         </div>
 
         <div className="column-content border-solid border-1px border-#e4e4e4 border-x-none p-y-10px">
-          <ColumnItem />
+          {tableColumnList.map((item, index) => {
+            return (
+              <ColumnItem
+                column={item}
+                key={item.id}
+                onChange={(val) => handleColumnChange(index, val)}
+              />
+            );
+          })}
         </div>
       </div>
     </div>

+ 20 - 16
apps/er-designer/src/pages/er/components/TablePanel.tsx

@@ -1,29 +1,33 @@
-import React from 'react'
+import React, { useEffect } from 'react'
 import { Button, Collapse, Input } from 'antd'
 import { SearchOutlined, SettingOutlined } from '@ant-design/icons'
 import TableItem from './TableItem'
+import { useModel } from 'umi'
+import type { TableItemType } from '@/type'
 export default function TablePanel() {
-  const tableList = [
-    {
-      key: '1',
-      label: '表1',
-      children: '内容1',
-      extra: <SettingOutlined />
-    },
-    {
-      key: '2',
-      label: '表2',
-      children: '内容2',
-    },
-  ]
+  const { project, updateTable } = useModel('erModel');
+  const contentRef = React.useRef<HTMLDivElement>(null);
+  const [contentStyle, setContentStyle] = React.useState<React.CSSProperties>({});
+
+  useEffect(() => {
+    // 计算高度
+    setContentStyle({
+      height: `calc(100vh - ${contentRef.current?.getBoundingClientRect().top}px)`,
+    });
+  }, []);
+
   return (
-    <div className='px-12px'>
+    <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">导入表</Button>
       </div>
-      <TableItem/>
+      {
+        project.tables.map((item) => {
+          return <TableItem data={item} onChange={updateTable} key={item.table.id}/>
+        })
+      }
     </div>
   )
 }

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

@@ -1,7 +1,9 @@
 import React from "react";
 import { Button, Tooltip, Divider, Dropdown } from "antd";
 import { DownOutlined } from "@ant-design/icons";
+import { useModel } from "umi";
 export default function Toolbar() {
+  const { addTable } = useModel("erModel");
   const scaleMenu = {
     style: {
       width: 200
@@ -60,7 +62,7 @@ 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">
+            <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>

+ 1 - 0
apps/er-designer/src/pages/er/index.tsx

@@ -70,6 +70,7 @@ const App: React.FC = () => {
         <Layout>
           <Content>
             <div id="graph-container" ref={containerRef}></div>
+            <Navigator/>
           </Content>
         </Layout>
       </Layout>

+ 134 - 32
apps/er-designer/src/type.d.ts

@@ -1,37 +1,44 @@
+/**
+ * 字段结构
+ */
 export interface ColumnItem {
-  aggregateEnable: boolean;
-  businessTableId: string;
-  charset: string;
+  aggregateEnable?: boolean;
+  businessTableId?: string;
+  charset?: string;
   cn_name: string;
   defaultValue: any;
-  displayEnable: boolean;
-  displayOrder: number;
+  displayEnable?: boolean;
+  displayOrder?: number;
   en_name: string;
-  groupByEnable: boolean;
+  groupByEnable?: boolean;
   id: string;
-  isAggregateEnable: boolean;
-  isDisplayEnable: boolean;
-  isGroupByEnable: boolean;
-  isLinkEnable: boolean;
-  isOrderByEnable: boolean;
-  isPreDefined: boolean;
+  isAggregateEnable?: boolean;
+  isDisplayEnable?: boolean;
+  isGroupByEnable?: boolean;
+  isLinkEnable?: boolean;
+  isOrderByEnable?: boolean;
+  isPreDefined?: boolean;
   isRequired: boolean;
   isUnique: boolean;
-  isWhereEnable: boolean;
-  linkEnable: boolean;
-  maxLength: number;
+  isWhereEnable?: boolean;
+  linkEnable?: boolean;
+  maxLength?: number;
   name: string;
-  orderByEnable: boolean;
-  preDefined: boolean;
-  precision: number;
-  required: boolean;
-  scale: number;
-  schemaName: string;
+  orderByEnable?: boolean;
+  preDefined?: boolean;
+  precision?: number;
+  required?: boolean;
+  scale?: number;
+  schemaName?: string;
   type: number;
-  unique: boolean;
-  whereEnable: boolean;
+  unique?: boolean;
+  whereEnable?: boolean;
+  langName?: string;
 }
 
+/**
+ * 表结构
+ */
 export interface ViewTable {
   aliasName: string;
   creationTime: string;
@@ -44,15 +51,110 @@ export interface ViewTable {
   name: string;
   parentBusinessTableId: string;
   schemaName: string;
-  tableExtInfoDto: {
-    groupType: string;
-    height: number;
-    id: string;
-    style: Record<string, any>;
-    width: number;
-    x: number;
-    y: number;
-  };
   type: number;
   updateTime: string;
+  cn_name: string;
+  en_name: string;
+}
+
+/**
+ * 字段关系
+ */
+export interface ColumnRelation {
+  id: string;
+  // 名称
+  name: string;
+  // 主键
+  primaryKey: string;
+  // 主键表
+  primaryTable: string;
+  // 外键
+  foreignKey: string;
+  // 外键表
+  foreignTable: string;
+  // 关系类型 1 一对一 2 一对多 3 多对多
+  relationType: 1 | 2 | 3;
+  // 连线样式
+  style: Record<string, any>;
+}
+
+/**
+ * 主题区域信息
+ */
+export interface TopicAreaInfo {
+  // 主题区域id
+  id: string;
+  // 主题区域名称
+  name: string;
+  // 主题区域样式
+  style: Record<string, any>;
+}
+
+/**
+ * 注释信息
+ */
+export interface RemarkInfo {
+  id: string;
+  // 名称
+  name: string;
+  // 内容
+  text: string;
+  // 样式
+  style: Record<string, any>;
+}
+
+/**
+ * 表数据
+ */
+export type TableItemType = {
+  table: ViewTable & {openSync: boolean; style: Record<string, any>},
+  tableColumnList: ColumnItem[]
+};
+
+/**
+ * 数据模型项目信息
+ */
+export interface ProjectInfo {
+  id: string;
+  // 名称
+  name: string;
+  // 文件夹ID
+  folderId: string;
+  // 创建时间
+  creationTime: string;
+  // 创建者
+  creatorUser: string;
+  // 更新时间
+  lastModificationTime: string;
+  // 模型说明
+  description?: string;
+  // 是否模版
+  isTemplate: boolean;
+  // 所属行业
+  industry?: string;
+  // 发布状态
+  publishStatus: string;
+  // 数据表 openSync 开启同步
+  tables: TableItemType[];
+  // 关系
+  relations: ColumnRelation[];
+  // 主题区域
+  TopicAreas: TopicAreaInfo[];
+  // 注释节点
+  remarks: RemarkInfo[];
+  // 操作记录
+  history: any[];
+  // 画布设置
+  setting: {
+    // 展示菜单栏
+    showMenu: boolean;
+    // 展示侧边栏
+    showSidebar: boolean;
+    // 展示字段详情
+    showColumnDetail: boolean;
+    // 展示网格
+    showGrid: boolean;
+    // 展示关系
+    showRelation: boolean;
+  }
 }

+ 10 - 0
apps/er-designer/src/utils/index.ts

@@ -0,0 +1,10 @@
+/**
+ * 创建uuid
+ * */ 
+export function uuid() {
+  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
+    var r = (Math.random() * 16) | 0,
+      v = c === "x" ? r : (r & 0x3) | 0x8;
+    return v.toString(16);
+  });
+}