Преглед на файлове

feat: 添加数据表预定义字段

liaojiaxing преди 3 месеца
родител
ревизия
45f692222f

+ 12 - 5
apps/er-designer/src/components/LangInput.tsx

@@ -11,15 +11,17 @@ interface LanItem {
 export default function LangInput({
   value,
   onChange,
+  disabled
 }: {
-  value: string;
-  onChange: (value: any) => void;
+  value: Record<string, string>[];
+  onChange: (value: Record<string, string>[], key?: string) => void;
+  disabled?: boolean;
 }) {
   const [lanOptions, setLanOptions] = React.useState<any[]>([]);
   const [formModel, setFormModel] = React.useState({
     langText: "",
-    en: "",
-    ["zh-CN"]: "",
+    en: value?.find(item => item.name = 'en')?.value || "",
+    ["zh-CN"]: value?.find(item => item.name = 'zh-CN')?.value || "",
   });
   const { run } = useRequest(ListLangBySearchKey, {
     manual: true,
@@ -64,7 +66,10 @@ export default function LangInput({
   };
 
   useEffect(() => {
-    onChange(formModel);
+    onChange([
+      { name: 'en', value: formModel.en},
+      { name: 'zh-CN', value: formModel["zh-CN"]},
+    ], formModel.langText);
   }, [formModel]);
 
   return (
@@ -78,6 +83,7 @@ export default function LangInput({
           value={formModel["zh-CN"]}
           onChange={(val) => handleChange("zh-CN", val)}
           onSelect={(_val, opt) => handleSelectLang(opt)}
+          disabled={disabled}
         />
       </div>
       <div>
@@ -90,6 +96,7 @@ export default function LangInput({
           value={formModel.en}
           onChange={(val) => handleChange("en", val)}
           onSelect={(_val, opt) => handleSelectLang(opt)}
+          disabled={disabled}
         />
       </div>
     </div>

+ 8 - 5
apps/er-designer/src/components/LangInputTextarea.tsx

@@ -12,14 +12,14 @@ export default function LangInputTextarea({
   value,
   onChange,
 }: {
-  value: string;
-  onChange: (value: any) => void;
+  value: Record<string, string>[];
+  onChange: (value: Record<string, string>[], key?: string) => void;
 }) {
   const [lanOptions, setLanOptions] = React.useState<any[]>([]);
   const [formModel, setFormModel] = React.useState({
     langText: "",
-    en: "",
-    ["zh-CN"]: "",
+    en: value?.find(item => item.name = 'en')?.value || "",
+    ["zh-CN"]: value?.find(item => item.name = 'zh-CN')?.value || "",
   });
   const { run } = useRequest(ListLangBySearchKey, {
     manual: true,
@@ -64,7 +64,10 @@ export default function LangInputTextarea({
   };
 
   useEffect(() => {
-    onChange(formModel);
+    onChange([
+      { name: 'en', value: formModel.en},
+      { name: 'zh-CN', value: formModel["zh-CN"]},
+    ], formModel.langText);
   }, [formModel]);
 
   return (

+ 171 - 49
apps/er-designer/src/components/TableEdit.tsx

@@ -1,73 +1,195 @@
-import React from 'react'
-import { EditableProTable, ProColumns } from '@ant-design/pro-components'
-
-export default function TableEdit() {
+import React from "react";
+import { EditableProTable, ProColumns } from "@ant-design/pro-components";
+import type { ColumnItem } from "@/type";
+import { createColumn, uuid } from "@/utils";
+import { DataType } from "@/enum";
+import { DATA_TYPE_OPTIONS } from "@/constants";
+import { Input, InputNumber } from "antd";
+import LangInput from "./LangInput";
+export default function TableEdit(props: { tableId?: string; data: any[] }) {
+  const [editableKeys, setEditableRowKeys] = React.useState<React.Key[]>([]);
+  const [dataSource, setDataSource] = React.useState<readonly ColumnItem[]>(
+    props.data
+  );
   const columns: ProColumns[] = [
     {
-      title: '代码',
-      dataIndex: 'code',
-      key: 'name',
-      valueType: 'text',
+      title: "字段代码",
+      dataIndex: "schemaName",
+      valueType: "text",
+      width: 80,
+    },
+    {
+      title: "字段名称",
+      dataIndex: "alignment",
+      valueType: "text",
+      width: 80,
+      renderFormItem: (_schema, config, form) => {
+        const model = config.record;
+        return (
+          <LangInput
+            value={model.langNameList}
+            onChange={(langValue) =>
+              form.setFieldValue("langNameList", langValue)
+            }
+          />
+        );
+      },
     },
     {
-      title: '名称',
-      dataIndex: 'name',
-      key: 'name',
-      valueType: 'text',
+      title: "类型",
+      dataIndex: "type",
+      valueType: "select",
+      width: 120,
+      fieldProps: {
+        options: DATA_TYPE_OPTIONS,
+      },
     },
     {
-      title: '类型',
-      dataIndex: 'type',
-      key: 'name',
-      valueType: 'text',
+      title: "长度",
+      dataIndex: "maxLength",
+      valueType: "digit",
+      width: 120,
+      fieldProps: {
+        precision: 0,
+      },
+      render: (text, record, index) => {
+        return record.type === DataType.Decimal
+          ? `${record.precision},${record.scale}`
+          : text;
+      },
+      renderFormItem: (schema, config, form) => {
+        const model = config.record;
+        return model.type === DataType.Nvarchar ? (
+          <InputNumber
+            min={0}
+            max={255}
+            value={model.maxLength}
+            onChange={(value) => form.setFieldValue("maxLength", value)}
+          />
+        ) : model.type === DataType.Decimal ? (
+          <span className="flex gap-2px">
+            <InputNumber
+              min={0}
+              max={20}
+              placeholder="总长度"
+              value={model.precision}
+              onChange={(value) => form.setFieldValue("precision", value)}
+            />
+            <InputNumber
+              min={0}
+              max={19}
+              placeholder="小数位数"
+              value={model.scale}
+              onChange={(value) => form.setFieldValue("scale", value)}
+            />
+          </span>
+        ) : (
+          <>-</>
+        );
+      },
     },
     {
-      title: '长度',
-      dataIndex: 'desc',
-      key: 'name',
-      valueType: 'text',
+      title: "必填",
+      dataIndex: "isRequired",
+      valueType: "switch",
+      width: 80,
     },
     {
-      title: '描述',
-      dataIndex: 'desc',
-      key: 'name',
-      valueType: 'text',
+      title: "唯一",
+      dataIndex: "isUnique",
+      valueType: "switch",
+      width: 80,
     },
     {
-      title: '必填',
-      dataIndex: 'desc',
-      key: 'name',
-      valueType: 'switch',
+      title: "默认值",
+      dataIndex: "defaultValue",
+      valueType: "text",
+      width: 120,
     },
     {
-      title: '唯一',
-      dataIndex: 'desc',
-      key: 'name',
-      valueType: 'switch',
+      title: "字符集",
+      dataIndex: "chartset",
+      valueType: "text",
+      width: 120,
+      renderFormItem: (_schema, config) => {
+        return config.record.type === DataType.Nvarchar ? (
+          <Input placeholder="请输入" />
+        ) : null;
+      },
     },
     {
-      title: '默认值',
-      dataIndex: 'desc',
-      key: 'name',
-      valueType: 'text',
+      title: "内容",
+      dataIndex: "whereInputContent",
+      valueType: "text",
+      width: 120,
+      renderFormItem: (_schema, config) => {
+        return config.record.type === DataType.Nvarchar ? (
+          <Input.TextArea placeholder="请输入" />
+        ) : null;
+      },
     },
     {
-      title: '字符集',
-      dataIndex: 'desc',
-      key: 'name',
-      valueType: 'text',
+      title: "描述",
+      dataIndex: "memo",
+      valueType: "textarea",
+      width: 120,
+      renderFormItem: () => {
+        return <Input.TextArea placeholder="请输入" />;
+      },
     },
     {
-      title: '内容',
-      dataIndex: 'desc',
-      key: 'name',
-      valueType: 'text',
+      title: "操作",
+      valueType: "option",
+      width: 120,
+      render: (text, record, _, action) =>
+        record.isPreDefined
+          ? []
+          : [
+              <a
+                key="editable"
+                onClick={() => {
+                  action?.startEditable?.(record.id);
+                }}
+              >
+                编辑
+              </a>,
+              <a
+                key="delete"
+                onClick={() => {
+                  setDataSource(
+                    dataSource.filter((item) => item.id !== record.id)
+                  );
+                }}
+              >
+                删除
+              </a>,
+            ],
     },
-    
-  ]
+  ];
+
+  const handleAdd = () => {
+    return createColumn(props?.tableId);
+  };
   return (
-    <div className='w-full h-full'>
-      <EditableProTable columns={columns}/>
+    <div className="w-full h-full">
+      <EditableProTable
+        columns={columns}
+        rowKey="id"
+        scroll={{ x: 960 }}
+        value={dataSource}
+        onChange={setDataSource}
+        recordCreatorProps={{
+          record: handleAdd,
+        }}
+        editable={{
+          type: "multiple",
+          editableKeys,
+          onSave: async (rowKey, data, row) => {
+            console.log(rowKey, data, row);
+          },
+          onChange: setEditableRowKeys,
+        }}
+      />
     </div>
-  )
+  );
 }

+ 3 - 3
apps/er-designer/src/constants/index.ts

@@ -1,4 +1,4 @@
-import { DataType } from "@/enum";
+import { DataType, TableType } from "@/enum";
 
 export interface DataTypeOption {
   label: string;
@@ -22,8 +22,8 @@ export const DATA_TYPE_OPTIONS: DataTypeOption[] = [
  */
 export const TABLE_TYPE_OPTIONS = [
   // {label: "系统表", value: 1},
-  {label: "流程表", value: 2},
-  {label: "业务表", value: 3},
+  {label: "流程表", value: TableType.FlowTable},
+  {label: "业务表", value: TableType.BusinessTable},
   // {label: "视图", value: 4},
 ];
 

+ 6 - 1
apps/er-designer/src/enum/index.ts

@@ -12,7 +12,6 @@ export enum RelationType {
   OneToOne = 1,
   OneToMany = 2,
   ManyToOne = 3,
-  ManyToMany = 4,
 }
 
 export enum RelationLineType {
@@ -20,4 +19,10 @@ export enum RelationLineType {
   Dash,
   Solid,
   Dotted,
+}
+
+export enum TableType {
+  SystemTable = 1,
+  FlowTable = 2,
+  BusinessTable = 3,
 }

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

@@ -25,4 +25,8 @@ body {
 
 .x6-widget-selection-selected {
   z-index: 0;
+}
+
+.x6-widget-transform {
+  z-index: 2;
 }

+ 104 - 84
apps/er-designer/src/models/erModel.tsx

@@ -8,6 +8,10 @@ import { Export } from "@antv/x6-plugin-export";
 import { Selection } from "@antv/x6-plugin-selection";
 import { GetAllDesignTables } from "@/api";
 import { useRequest } from "umi";
+import { useFullscreen } from "ahooks";
+import { throttle } from "lodash-es";
+import { createTable } from "@/utils";
+
 import type {
   ColumnItem,
   ColumnRelation,
@@ -16,7 +20,7 @@ import type {
   TableItemType,
   TopicAreaInfo,
 } from "@/type";
-import { uuid } from "@/utils";
+import { createColumn, uuid } from "@/utils";
 import { DataType, RelationLineType, RelationType } from "@/enum";
 import { render } from "./renderer";
 import { useSessionStorageState } from "ahooks";
@@ -31,6 +35,8 @@ export default function erModel() {
   const [graph, setGraph] = useState<Graph>();
   const historyRef = useRef<ProjectInfo[]>([]);
   const activeIndex = useRef(0);
+  const [_isFullscreen, { enterFullscreen, exitFullscreen }] = useFullscreen(document.body);
+  const [playModeEnable, setPlayModeEnable] = useState(false);
   const [project, setProjectInfo] = useState<ProjectInfo>({
     id: "1",
     name: "项目1",
@@ -44,7 +50,6 @@ export default function erModel() {
     relations: [],
     topicAreas: [],
     remarkInfos: [],
-    history: [],
     todos: [],
     setting: {
       showMenu: true,
@@ -121,7 +126,6 @@ export default function erModel() {
    * @param container
    */
   const initGraph = (container: HTMLElement, width?: number, height?: number) => {
-    console.log(container.clientWidth, container.clientHeight);
     const instance = new Graph({
       container,
       width: width || document.documentElement.clientWidth,
@@ -238,6 +242,45 @@ export default function erModel() {
       }
     );
 
+    instance.on("node:change:position", throttle((args) => {
+      console.log(args);
+      const data = args.node.data;
+      const current = args.current;
+      if(data.isTable) {
+        updateTable({
+          ...data,
+          table: {
+            ...data.table,
+            style: {
+              ...data.table.style,
+              x: current.x,
+              y: current.y,
+            }
+          }
+        })
+      }
+      if(data.isTopicArea) {
+        updateTopicArea({
+          ...data,
+          style: {
+            ...data.style,
+            x: current.x,
+            y: current.y,
+          }
+        })
+      }
+      if(data.isRemark) {
+        updateRemark({
+          ...data,
+          style: {
+            ...data.style,
+            x: current.x,
+            y: current.y,
+          }
+        })
+      }
+    }, 500));
+
     instance.bindKey("ctrl+z", onUndo);
     instance.bindKey("ctrl+y", onRedo);
     instance.bindKey("ctrl+c", onCopy);
@@ -291,62 +334,13 @@ export default function erModel() {
    * 添加表
    */
   const addTable = (parentId?: string) => {
-    const tableId = uuid();
-    const columnId = uuid();
-    const newTable: TableItemType = {
-      isTable: true,
-      table: {
-        aliasName: "newtable",
-        creationTime: "",
-        creatorUserId: "",
-        displayOrder: true,
-        id: tableId,
-        isDeleted: false,
-        langDescription: "",
-        langName: "",
-        parentBusinessTableId: parentId || "",
-        schemaName: "new_table",
-        type: 1,
-        updateTime: "",
-        openSync: false,
-        style: {
-          // 随机颜色
-          color: "#" + Math.floor(Math.random() * 0x666666).toString(16),
-          x: 300,
-          y: 300,
-        },
-      },
-      tableColumnList: [
-        {
-          id: columnId,
-          schemaName: "",
-          type: DataType.Nvarchar,
-          maxLength: 100,
-          precision: 0,
-          scale: 0,
-          isRequired: false,
-          isUnique: false,
-          isPreDefined: false,
-          defaultValue: "",
-          displayOrder: false,
-          businessTableId: tableId,
-          memo: '',
-          alignment: '',
-          isDisplayEnable: true,
-          isLinkEnable: false,
-          isWhereEnable: false,
-          isOrderByEnable: false,
-          isGroupByEnable: false,
-          isAggregateEnable: false,
-          whereInputType: '',
-          whereInputContent: '',
-          alterId: '',
-          temp_rename: '',
-          charset: '',
-          orderChar: ''
-        },
-      ],
-    };
+    const area = graphRef.current?.getGraphArea();
+    const x = area?.center.x || 300;
+    const y = area?.center.y || 300;
+    // todo 获取表类型
+    const newTable = createTable(3, parentId);
+    newTable.table.style.x = x;
+    newTable.table.style.y = y;
 
     // 子表插入到父表后面
     const list = [...project.tables];
@@ -367,14 +361,16 @@ export default function erModel() {
    * @param table
    */
   const updateTable = (table: TableItemType) => {
-    setProject({
-      ...project,
-      tables: project.tables.map((item) => {
-        if (item.table.id === table.table.id) {
-          return table;
-        }
-        return item;
-      }),
+    setProject((project) => {
+      return {
+        ...project,
+        tables: project.tables.map((item) => {
+          if (item.table.id === table.table.id) {
+            return table;
+          }
+          return item;
+        }),
+      }
     });
   };
 
@@ -429,14 +425,16 @@ export default function erModel() {
    * 修改主题域
    */
   const updateTopicArea = (topicArea: TopicAreaInfo) => {
-    setProject({
-      ...project,
-      topicAreas: project.topicAreas.map((item) => {
-        if (item.id === topicArea.id) {
-          return topicArea;
-        }
-        return item;
-      }),
+    setProject((project) => {
+      return {
+        ...project,
+        topicAreas: project.topicAreas.map((item) => {
+          if (item.id === topicArea.id) {
+            return topicArea;
+          }
+          return item;
+        }),
+      }
     });
   };
 
@@ -479,14 +477,10 @@ export default function erModel() {
    * 修改备注
    */
   const updateRemark = (remark: RemarkInfo) => {
+    console.log(remark)
     setProject((state) => ({
       ...(state || {}),
-      remarks: state.remarkInfos.map((item) => {
-        if (item.id === remark.id) {
-          return remark;
-        }
-        return item;
-      }),
+      remarkInfos: state.remarkInfos.map((item) => item.id === remark.id ? remark : item),
     }));
   };
 
@@ -640,10 +634,11 @@ export default function erModel() {
    */
   const onCut = () => {
     const cells = graphRef.current?.getSelectedCells();
-    if (cells?.[0]?.isNode) {
+    if (cells?.[0]?.isNode()) {
       const cell = cells[0];
       const data = cell.data;
       setClipboardCache(data);
+      // 表
       if (data?.isTable) {
         const childTableIds = project.tables
           .filter((item) => item.table.parentBusinessTableId === cell.id)
@@ -664,12 +659,14 @@ export default function erModel() {
           ),
         });
       }
+      // 主题区域
       if (data?.isTopicArea) {
         setProject({
           ...project,
           topicAreas: project.topicAreas.filter((item) => item.id !== cell.id),
         });
       }
+      // 备注
       if (data?.isRemark) {
         setProject({
           ...project,
@@ -684,7 +681,7 @@ export default function erModel() {
    */
   const onCopy = () => {
     const cells = graphRef.current?.getSelectedCells();
-    if (cells?.[0]?.isNode) {
+    if (cells?.[0]?.isNode()) {
       const cell = cells[0];
       const data = cell.data;
       setClipboardCache(data);
@@ -766,7 +763,7 @@ export default function erModel() {
    */
   const onDelete = () => {
     const cell = graphRef.current?.getSelectedCells();
-    if (cell?.[0]?.isNode) {
+    if (cell?.[0]?.isNode()) {
       const data = cell[0].data;
       if (data?.isTable) {
         setProject({
@@ -791,6 +788,25 @@ export default function erModel() {
     }
   };
 
+  /**
+   * 演示模式
+   */
+  const enterPlayMode = () => {
+    enterFullscreen();
+    setPlayModeEnable(true);
+    setTimeout(() => {
+      graphRef.current?.centerContent();
+    }, 100);
+  }; 
+
+  /**
+   * 退出演示模式
+   */
+  const exitPlayMode = () => {
+    exitFullscreen();
+    setPlayModeEnable(false);
+  };
+
   return {
     initGraph,
     graph,
@@ -818,5 +834,9 @@ export default function erModel() {
     onCopy,
     onPaste,
     onDelete,
+    enterPlayMode,
+    playModeEnable,
+    setPlayModeEnable,
+    exitPlayMode
   };
 }

+ 2 - 4
apps/er-designer/src/models/renderer.ts

@@ -154,8 +154,7 @@ export const render = (graph: Graph, project: ProjectInfo) => {
       relationEdge?.appendLabel({
         attrs: {
           txt: {
-            text: relation.relationType === RelationType.OneToMany ||
-            relation.relationType === RelationType.ManyToMany
+            text: relation.relationType === RelationType.OneToMany
               ? "n"
               : "1",
           },
@@ -244,8 +243,7 @@ export const render = (graph: Graph, project: ProjectInfo) => {
             attrs: {
               txt: {
                 text:
-                  relation.relationType === RelationType.OneToMany ||
-                  relation.relationType === RelationType.ManyToMany
+                  relation.relationType === RelationType.OneToMany
                     ? "n"
                     : "1",
               },

+ 69 - 18
apps/er-designer/src/pages/detail/index.tsx

@@ -8,6 +8,7 @@ import ER from "./components/ER";
 const { Content, Header } = Layout;
 export default function index() {
   const [active, setActive] = useState(0);
+  const [showNavigator, setShowNavigator] = useState(false);
   const descItems: DescriptionsProps["items"] = [
     {
       key: "1",
@@ -78,10 +79,22 @@ export default function index() {
 
   const extra = (
     <div className="flex gap-12px">
-      <a><i className="iconfont icon-tongbu text-12px"/>一键同步</a>
-      <a><i className="iconfont icon-bianji text-12px"/>修改</a>
-      <a><i className="iconfont icon-bianji text-12px"/>进入编辑</a>
-      <a><i className="iconfont icon-moban text-14px"/>保存为模板</a>
+      <a>
+        <i className="iconfont icon-tongbu text-12px" />
+        一键同步
+      </a>
+      <a>
+        <i className="iconfont icon-bianji text-12px" />
+        修改
+      </a>
+      <a>
+        <i className="iconfont icon-bianji text-12px" />
+        进入编辑
+      </a>
+      <a>
+        <i className="iconfont icon-moban text-14px" />
+        保存为模板
+      </a>
     </div>
   );
   return (
@@ -112,7 +125,7 @@ export default function index() {
       </Header>
 
       <Content className="flex-1 flex gap-12px">
-        <div className="left w-300px h-full shadow-sm bg-#fff rounded-8px">
+        <div className="left w-300px shrink-0 h-full shadow-sm bg-#fff rounded-8px">
           <div
             className="
               flex 
@@ -134,9 +147,18 @@ export default function index() {
               <span>数据表</span>
             </div>
             <div>
-              <Input size="small" placeholder="搜索" className="w-100px m-r-4px" suffix={<SearchOutlined/>}/>
+              <Input
+                size="small"
+                placeholder="搜索"
+                className="w-100px m-r-4px"
+                suffix={<SearchOutlined />}
+              />
               <Tooltip title="添加数据表">
-                <Button size="small" type="primary" icon={<PlusOutlined/>}></Button>
+                <Button
+                  size="small"
+                  type="primary"
+                  icon={<PlusOutlined />}
+                ></Button>
               </Tooltip>
             </div>
           </div>
@@ -145,23 +167,52 @@ export default function index() {
         <div className="right flex-1 h-full shadow-sm bg-#fff rounded-8px p-12px flex flex-col">
           <div className="head flex justify-between">
             <div className="left flex gap-8px">
-              <Button type={active === 0 ? 'primary' : 'default'} onClick={() => setActive(0)}>ER图</Button>
-              <Button type={active === 1 ? 'primary' : 'default'} onClick={() => setActive(1)}>实体</Button>
+              <Button
+                type={active === 0 ? "primary" : "default"}
+                onClick={() => setActive(0)}
+              >
+                ER图
+              </Button>
+              <Button
+                type={active === 1 ? "primary" : "default"}
+                onClick={() => setActive(1)}
+              >
+                实体
+              </Button>
             </div>
             <div className="right flex gap-8px m-b-12px">
-              <Input placeholder="搜索" suffix={<SearchOutlined/>}/>
-              <Button type="primary" icon={<i className="iconfont icon-xiaoditu text-14px"/>}>导航</Button>
-              <Button type="primary" icon={<i className="iconfont icon-quanping_o text-14px"/>}>全屏</Button>
+              {active === 0 ? (
+                <>
+                  <Input placeholder="搜索" suffix={<SearchOutlined />} />
+                  <Button
+                    type={showNavigator ? "primary" : "default"}
+                    onClick={() => setShowNavigator(!showNavigator)}
+                    icon={<i className="iconfont icon-xiaoditu text-14px" />}
+                  >
+                    导航
+                  </Button>
+                  <Button
+                    type="primary"
+                    icon={<i className="iconfont icon-quanping_o text-14px" />}
+                  >
+                    全屏
+                  </Button>
+                </>
+              ) : (
+                <>
+                  <Input placeholder="搜索" suffix={<SearchOutlined />} />
+                </>
+              )}
             </div>
           </div>
           <div className="content w-full flex-1">
-            {
-              active === 0
-              ? <div className="er w-full h-full bg-#ccc">
-                <ER/>
+            {active === 0 ? (
+              <div className="er w-full h-full bg-#ccc">
+                <ER showNavigator={showNavigator} />
               </div>
-              : <TableEdit/>
-            }
+            ) : (
+              <TableEdit  data={[]}/>
+            )}
           </div>
         </div>
       </Content>

+ 43 - 19
apps/er-designer/src/pages/er/components/ColumnItem.tsx

@@ -16,7 +16,6 @@ import { DATA_TYPE_OPTIONS } from "@/constants";
 import { useSortable } from "@dnd-kit/sortable";
 import { CSS } from "@dnd-kit/utilities";
 import LangInput from "@/components/LangInput";
-import LangInputTextarea from "@/components/LangInputTextarea";
 
 export default function ColumnItem({
   column,
@@ -27,7 +26,6 @@ export default function ColumnItem({
   onChange: (key: string, value: any) => void;
   onDelete: (id: string) => void;
 }) {
-  console.log(column);
   const { setNodeRef, attributes, listeners, transform, transition } =
     useSortable({
       id: column.id,
@@ -37,10 +35,10 @@ export default function ColumnItem({
       },
     });
 
-    const styles = {
-      transform: CSS.Transform.toString(transform),
-      transition,
-    };
+  const styles = {
+    transform: CSS.Transform.toString(transform),
+    transition,
+  };
 
   return (
     <div
@@ -51,12 +49,17 @@ export default function ColumnItem({
       {...attributes}
       data-cypress="draggable-item"
     >
-      <HolderOutlined className="cursor-move" data-cypress="draggable-handle" {...listeners}/>
+      <HolderOutlined
+        className="cursor-move"
+        data-cypress="draggable-handle"
+        {...listeners}
+      />
       <Tooltip title="字段编码">
         <Input
           placeholder="编码"
           defaultValue={column.schemaName}
           className="flex-1"
+          disabled={column.isPreDefined}
           onChange={(e) => onChange("schemaName", e.target.value)}
         />
       </Tooltip>
@@ -66,6 +69,7 @@ export default function ColumnItem({
           className="w-80px"
           options={DATA_TYPE_OPTIONS}
           value={column.type}
+          disabled={column.isPreDefined}
           onChange={(value) => onChange("type", value)}
           dropdownStyle={{ width: 120 }}
         />
@@ -86,7 +90,9 @@ export default function ColumnItem({
           style={
             column.isRequired ? { background: "#1677ff", color: "#fff" } : {}
           }
-          onClick={() => onChange("isRequired", !column.isRequired)}
+          onClick={() =>
+            !column.isPreDefined && onChange("isRequired", !column.isRequired)
+          }
         >
           !
         </div>
@@ -107,7 +113,9 @@ export default function ColumnItem({
           style={
             column.isUnique ? { background: "#1677ff", color: "#fff" } : {}
           }
-          onClick={() => onChange("isUnique", !column.isUnique)}
+          onClick={() =>
+            !column.isPreDefined && onChange("isUnique", !column.isUnique)
+          }
         >
           1
         </div>
@@ -124,7 +132,13 @@ export default function ColumnItem({
               <Row gutter={8}>
                 <Col span={12}>
                   <Form.Item label="字段名称">
-                    <LangInput value="" onChange={() => {}}/>
+                    <LangInput
+                      disabled={column.isPreDefined}
+                      value={column.langNameList}
+                      onChange={(val) => {
+                        onChange("langNameList", val);
+                      }}
+                    />
                   </Form.Item>
                 </Col>
                 <Col span={12}>
@@ -132,6 +146,7 @@ export default function ColumnItem({
                     <Input
                       className="w-full"
                       placeholder="默认值"
+                      disabled={column.isPreDefined}
                       value={column.defaultValue}
                       onChange={(e) => onChange("defaultValue", e.target.value)}
                     />
@@ -145,6 +160,7 @@ export default function ColumnItem({
                       placeholder="请输入"
                       min={0}
                       className="w-full"
+                      disabled={column.isPreDefined}
                       value={column.maxLength}
                       onChange={(num) => onChange("maxLength", num)}
                     />
@@ -156,6 +172,7 @@ export default function ColumnItem({
                       placeholder="请输入"
                       min={0}
                       className="w-full"
+                      disabled={column.isPreDefined}
                       value={column.precision}
                       onChange={(num) => onChange("precision", num)}
                     />
@@ -165,19 +182,26 @@ export default function ColumnItem({
               <Row>
                 <Col span={24}>
                   <Form.Item label="描述">
-                    <LangInputTextarea value="" onChange={() => {}}/>
+                    <Input.TextArea
+                      placeholder="请输入"
+                      disabled={column.isPreDefined}
+                      value={column.memo}
+                      onChange={(e) => onChange("memo", e.target.value)}
+                    />
                   </Form.Item>
                 </Col>
               </Row>
             </Form>
-            <Button
-              type="default"
-              danger
-              className="w-full"
-              onClick={() => onDelete(column.id)}
-            >
-              删除
-            </Button>
+            {!column.isPreDefined && (
+              <Button
+                type="default"
+                danger
+                className="w-full"
+                onClick={() => onDelete(column.id)}
+              >
+                删除
+              </Button>
+            )}
           </div>
         }
       >

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

@@ -17,6 +17,7 @@ export default function Menu() {
     onCopy,
     onPaste,
     onDelete,
+    enterPlayMode,
   } = useModel("erModel");
   const [modal, contextHolder] = Modal.useModal();
   const [isFullscreen, { toggleFullscreen }] = useFullscreen(document.body);
@@ -190,7 +191,7 @@ export default function Menu() {
             </span>
           ),
         },
-        { key: "1-3", label: "演示模式" },
+        { key: "1-3", label: "演示模式", onClick: () => enterPlayMode() },
         {
           key: "1-4",
           label: (

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

@@ -1,13 +1,16 @@
 import React, { useEffect } from "react";
 import { useModel } from "umi";
 import { MiniMap } from "@antv/x6-plugin-minimap";
+import { useSessionStorageState } from "ahooks";
 export default function Navigator() {
   const { graph } = useModel("erModel");
   const mapRef = React.useRef<HTMLDivElement>(null);
-  const [show, setShow] = React.useState(true);
+  const [show, setShow] = useSessionStorageState('show-navigator', {
+    defaultValue: true,
+    listenStorageChange: true,
+  });
 
   useEffect(() => {
-    console.log("graph", graph);
     if (graph && mapRef.current) {
       graph.use(
         new MiniMap({

+ 3 - 31
apps/er-designer/src/pages/er/components/TableItem.tsx

@@ -14,9 +14,8 @@ import {
 import React, { useEffect, useState } from "react";
 import CustomColorPicker from "@/components/CustomColorPicker";
 import { ColumnItem as ColumnItemType, TableItemType } from "@/type";
-import { TABLE_TYPE_OPTIONS, DATA_TYPE_OPTIONS } from "@/constants";
-import { uuid } from "@/utils";
-import { DataType } from "@/enum";
+import { TABLE_TYPE_OPTIONS } from "@/constants";
+import { createColumn } from "@/utils";
 import { useModel } from "umi";
 import { DndContext } from "@dnd-kit/core";
 import type { DragEndEvent } from "@dnd-kit/core";
@@ -78,34 +77,7 @@ export default function TableItem({
 
   // 添加字段
   const handleAddColumn = () => {
-    const newColumn: ColumnItemType = {
-      id: uuid(),
-      schemaName: "",
-          type: DataType.Nvarchar,
-          maxLength: 100,
-          precision: 0,
-          scale: 0,
-          isRequired: false,
-          isUnique: false,
-          isPreDefined: false,
-          defaultValue: "",
-          displayOrder: false,
-          businessTableId: table.id,
-          memo: '',
-          alignment: '',
-          isDisplayEnable: true,
-          isLinkEnable: false,
-          isWhereEnable: false,
-          isOrderByEnable: false,
-          isGroupByEnable: false,
-          isAggregateEnable: false,
-          whereInputType: '',
-          whereInputContent: '',
-          alterId: '',
-          temp_rename: '',
-          charset: '',
-          orderChar: ''
-    };
+    const newColumn: ColumnItemType = createColumn(table.id);
     onChange({
       table,
       tableColumnList: [...tableColumnList, newColumn],

+ 106 - 37
apps/er-designer/src/pages/er/index.tsx

@@ -11,16 +11,22 @@ import Menu from "./components/Menu";
 import { useModel } from "umi";
 import "./index.less";
 import { useSessionStorageState } from "ahooks";
+import { EnvironmentOutlined, FullscreenExitOutlined, UnorderedListOutlined } from "@ant-design/icons";
 
 const { Header, Content, Sider } = Layout;
 
 const App: React.FC = () => {
   const containerRef = React.useRef(null);
-  const { initGraph, project } = useModel("erModel");
-  const [tabActiveKey, setTabActiveKey] = useSessionStorageState('tabs-active-key', {
-    defaultValue: "1",
-    listenStorageChange: true
-  });
+  const { initGraph, project, playModeEnable, exitPlayMode } =
+    useModel("erModel");
+  const [tabActiveKey, setTabActiveKey] = useSessionStorageState(
+    "tabs-active-key",
+    {
+      defaultValue: "1",
+      listenStorageChange: true,
+    }
+  );
+  const [show, setShow] = useSessionStorageState('show-navigator');
 
   useEffect(() => {
     if (containerRef.current) {
@@ -53,47 +59,110 @@ const App: React.FC = () => {
 
   return (
     <Layout className="h-100vh">
-      <Header
-        className="bg-white h-100px border-b-1px border-b-solid border-b-gray-200 p-x-0 flex flex-col"
-        style={{
-          height: project.setting.showMenu ? "100px" : "32px",
-          transition: "all 0.3s ease-in-out",
-        }}
-      >
-        <div
-          className="grid"
-          style={{ 
-            gridTemplateRows: project.setting.showMenu ? "1fr" : "0fr",
-            transition: 'all 0.3s'
+      {!playModeEnable && (
+        <Header
+          className="bg-white h-100px border-b-1px border-b-solid border-b-gray-200 p-x-0 flex flex-col"
+          style={{
+            height: project.setting.showMenu ? "100px" : "32px",
+            transition: "all 0.3s ease-in-out",
           }}
         >
-          <div className="overflow-hidden">
-            <Menu />
+          <div
+            className="grid"
+            style={{
+              gridTemplateRows: project.setting.showMenu ? "1fr" : "0fr",
+              transition: "all 0.3s",
+            }}
+          >
+            <div className="overflow-hidden">
+              <Menu />
+            </div>
           </div>
-        </div>
-        <Toolbar />
-      </Header>
+          <Toolbar />
+        </Header>
+      )}
       <Layout>
-        <Sider
-          width={project.setting.showSidebar ? 360 : 0}
-          style={{ background: "#fff", borderRight: "1px solid #eee" }}
-        >
-          <ConfigProvider
-            theme={{
-              components: {
-                Tabs: {
-                  colorPrimary: "#000",
-                },
-              },
-            }}
+        {!playModeEnable && (
+          <Sider
+            width={project.setting.showSidebar ? 360 : 0}
+            style={{ background: "#fff", borderRight: "1px solid #eee" }}
           >
-            <Tabs animated activeKey={tabActiveKey} onChange={setTabActiveKey} items={tabItems} centered />
-          </ConfigProvider>
-        </Sider>
+            <ConfigProvider
+              theme={{
+                components: {
+                  Tabs: {
+                    colorPrimary: "#000",
+                  },
+                },
+              }}
+            >
+              <Tabs
+                animated
+                activeKey={tabActiveKey}
+                onChange={setTabActiveKey}
+                items={tabItems}
+                centered
+              />
+            </ConfigProvider>
+          </Sider>
+        )}
         <Layout>
           <Content>
             <div id="graph-container" ref={containerRef}></div>
             <Navigator />
+            {
+              playModeEnable && <div className="absolute top-32px right-32px z-2">
+              <div className="left bg-#fff shadow-md p-x-4px p-y-4px flex items-center gap-8px">
+                <div
+                  className="
+                  rounded-4px 
+                  cus-btn 
+                  w-32px 
+                  h-32px
+                  bg-#eee 
+                  flex-none 
+                  text-center 
+                  leading-32px 
+                  cursor-pointer 
+                  hover:bg-#ddd"
+                >
+                  <UnorderedListOutlined />
+                </div>
+                <div
+                  className="
+                  rounded-4px 
+                  cus-btn 
+                  w-32px 
+                  h-32px
+                  bg-#eee 
+                  flex-none 
+                  text-center 
+                  leading-32px 
+                  cursor-pointer 
+                  hover:bg-#ddd"
+
+                >
+                  <EnvironmentOutlined onClick={() => setShow(!show)}/>
+                </div>
+                <div
+                  className="
+                  rounded-4px 
+                  cus-btn 
+                  w-32px 
+                  h-32px
+                  bg-#eee 
+                  flex-none 
+                  text-center 
+                  leading-32px 
+                  cursor-pointer 
+                  hover:bg-#ddd"
+                  onClick={() => exitPlayMode()}
+                >
+                  <FullscreenExitOutlined />
+                </div>
+              </div>
+            </div>
+            }
           </Content>
         </Layout>
       </Layout>

+ 41 - 29
apps/er-designer/src/type.d.ts

@@ -3,32 +3,41 @@
  */
 export interface ColumnItem {
   langName?: string;
-  schemaName: string;
+  schemaName?: string;
   type: number;
-  maxLength: number;
-  isRequired: boolean;
-  isUnique: boolean;
-  displayOrder: boolean;
-  businessTableId: string;
-  memo: string;
-  alignment: string;
-  isDisplayEnable: boolean;
-  isLinkEnable: boolean;
-  isWhereEnable: boolean;
-  isOrderByEnable: boolean;
-  isGroupByEnable: boolean;
-  isAggregateEnable: boolean;
-  whereInputType: string;
-  whereInputContent: string;
-  precision: number;
-  scale: number;
-  isPreDefined: boolean;
-  defaultValue: string;
-  alterId: string;
-  temp_rename: string;
+  maxLength?: number;
+  // 必填
+  isRequired?: boolean;
+  // 唯一
+  isUnique?: boolean;
+  displayOrder?: number;
+  // 表id
+  businessTableId?: string;
+  // 描述
+  memo?: string;
+  alignment?: string;
+  isDisplayEnable?: boolean;
+  isLinkEnable?: boolean;
+  isWhereEnable?: boolean;
+  isOrderByEnable?: boolean;
+  isGroupByEnable?: boolean;
+  isAggregateEnable?: boolean;
+  whereInputType?: string;
+  // 内容
+  whereInputContent?: string;
+  precision?: number;
+  scale?: number;
+  isPreDefined?: boolean;
+  // 默认值
+  defaultValue?: string;
+  alterId?: string;
+  temp_rename?: string;
   id: string;
-  charset: string;
-  orderChar: string;
+  charset?: string;
+  orderChar?: string;
+  langNameList: Record<string, string>[];
+  // 字符集
+  chartset?: string;
 }
 
 /**
@@ -45,9 +54,11 @@ export interface ViewTable {
   langName?: string;
   parentBusinessTableId: string;
   schemaName: string;
-  type: number; // 1 基础数据 2 流程 3 系统表
+  type: number;
   updateTime: string;
   langName: string;
+  langNameList?: Record<string, string>[];
+  langDescriptionList?: Record<string, string>[];
 }
 
 /**
@@ -65,8 +76,8 @@ export interface ColumnRelation {
   foreignKey: string;
   // 外键表
   foreignTable: string;
-  // 关系类型 1 一对一 2 一对多 3 多对一 4 多对多
-  relationType: 1 | 2 | 3 | 4;
+  // 关系类型 1 一对一 2 一对多 3 多对一
+  relationType: 1 | 2 | 3;
   // 连线样式
   style: Record<string, any>;
 }
@@ -79,7 +90,7 @@ export interface TodoItem {
   name: string;
   text: string;
   isDone: boolean;
-  level: 0 | 1 | 2 | 3;
+  level: 0 | 1 | 2 | 3; // 0 默认 1 一般 2 紧急 3 最高
 }
 
 /**
@@ -92,6 +103,7 @@ export interface TopicAreaInfo {
   name: string;
   // 主题区域样式
   style: Record<string, any>;
+  // 主题区域
   isTopicArea: boolean;
 }
 
@@ -160,7 +172,7 @@ export interface ProjectInfo {
   // 注释节点
   remarkInfos: RemarkInfo[];
   // 操作记录
-  history: any[];
+  // history: any[];
   // 待办事项
   todos: TodoItem[];
   // 画布设置

+ 304 - 2
apps/er-designer/src/utils/index.ts

@@ -1,10 +1,312 @@
+import { DataType, TableType } from "@/enum";
+import { ColumnItem, TableItemType } from "@/type";
+
 /**
  * 创建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);
   });
-}
+}
+
+export const createTable = (tableType: TableType, parentId?: string): TableItemType => {
+  const tableId = uuid();
+  const tableColumnList: ColumnItem[] = [];
+  if(tableType === TableType.FlowTable) {
+    // 创建流程表预定义字段
+    const list = createFlowPredefinedFields(tableId, !!parentId);
+    tableColumnList.push(...list);
+  } else {
+    // 创建业务表预定义字段
+    tableColumnList.push(...createBasePredefinedField(tableId));
+  }
+  return {
+    isTable: true,
+    table: {
+      aliasName: "newtable",
+      creationTime: "",
+      creatorUserId: "",
+      displayOrder: true,
+      id: tableId,
+      isDeleted: false,
+      langDescription: "",
+      langName: "",
+      parentBusinessTableId: parentId || "",
+      schemaName: "new_table",
+      type: 1,
+      updateTime: "",
+      openSync: false,
+      style: {
+        // 随机颜色
+        color: "#" + Math.floor(Math.random() * 0x666666).toString(16),
+      },
+    },
+    tableColumnList,
+  };
+};
+
+/**
+ * 创建字段
+ * @param tableId 表id
+ * @returns
+ */
+export const createColumn = (tableId?: string): ColumnItem => {
+  return {
+    id: uuid(),
+    schemaName: "",
+    type: DataType.Nvarchar,
+    maxLength: 100,
+    precision: 0,
+    scale: 0,
+    isRequired: false,
+    isUnique: false,
+    isPreDefined: false,
+    defaultValue: "",
+    displayOrder: 0,
+    businessTableId: tableId || "",
+    memo: "",
+    whereInputType: "",
+    whereInputContent: "",
+    charset: "",
+    langNameList: [
+      { name: "zh-CN", value: "" },
+      { name: "en", value: "" },
+    ],
+  };
+};
+
+/**
+ * 创建基础表预定义字段
+ */
+export const createBasePredefinedField = (tableId: string): ColumnItem[] => {
+  return [
+    // id
+    {
+      id: uuid(),
+      schemaName: "id",
+      type: DataType.Nvarchar,
+      displayOrder: 1,
+      maxLength: 50,
+      businessTableId: tableId,
+      isRequired: true,
+      isPreDefined: true,
+      langNameList: [
+        { name: "zh-CN", value: "Id" },
+        { name: "en", value: "Id" },
+      ],
+    },
+    // 创建时间
+    {
+      id: uuid(),
+      schemaName: "creation_time",
+      type: DataType.DateTime,
+      displayOrder: 9001,
+      maxLength: 0,
+      businessTableId: tableId,
+      isRequired: true,
+      isPreDefined: true,
+      defaultValue: "<%SystemDate%>",
+      langNameList: [
+        { name: "zh-CN", value: "创建时间" },
+        { name: "en", value: "CreationTime" },
+      ],
+    },
+    // 创建者
+    {
+      id: uuid(),
+      schemaName: "creation_user",
+      type: DataType.Nvarchar,
+      displayOrder: 9002,
+      maxLength: 50,
+      businessTableId: tableId,
+      isRequired: true,
+      isPreDefined: true,
+      defaultValue: "<%UserId%>",
+      langNameList: [
+        { name: "zh-CN", value: "创建人" },
+        { name: "en", value: "CreationUser" },
+      ],
+    },
+    // 更新时间
+    {
+      id: uuid(),
+      schemaName: "update_time",
+      type: DataType.DateTime,
+      displayOrder: 9003,
+      maxLength: 0,
+      businessTableId: tableId,
+      isRequired: false,
+      isPreDefined: true,
+      defaultValue: "<%SystemDate%>",
+      langNameList: [
+        { name: "zh-CN", value: "更新时间" },
+        { name: "en", value: "UpdateTime" },
+      ],
+    },
+    // 更新者
+    {
+      id: uuid(),
+      schemaName: "update_user",
+      type: DataType.Nvarchar,
+      displayOrder: 9004,
+      maxLength: 50,
+      businessTableId: tableId,
+      isRequired: false,
+      isPreDefined: true,
+      defaultValue: "<%UserId%>",
+      langNameList: [
+        { name: "zh-CN", value: "更新人" },
+        { name: "en", value: "UpdateUser" },
+      ],
+    },
+    // 删除时间
+    {
+      id: uuid(),
+      schemaName: "delete_time",
+      type: DataType.DateTime,
+      displayOrder: 9005,
+      maxLength: 0,
+      businessTableId: tableId,
+      isRequired: false,
+      isPreDefined: true,
+      langNameList: [
+        { name: "zh-CN", value: "删除时间" },
+        { name: "en", value: "DeleteTime" },
+      ],
+    },
+    // 删除人
+    {
+      id: uuid(),
+      schemaName: "delete_user",
+      type: DataType.Nvarchar,
+      displayOrder: 9006,
+      maxLength: 50,
+      businessTableId: tableId,
+      isRequired: false,
+      isPreDefined: true,
+      defaultValue: "<%UserId%>",
+      langNameList: [
+        { name: "zh-CN", value: "删除人" },
+        { name: "en", value: "DeleteUser" },
+      ],
+    },
+    // 删除状态
+    {
+      id: uuid(),
+      schemaName: "is_deleted",
+      type: DataType.Bit,
+      displayOrder: 9007,
+      maxLength: 0,
+      businessTableId: tableId,
+      isRequired: false,
+      isPreDefined: true,
+      langNameList: [
+        { name: "zh-CN", value: "删除状态" },
+        { name: "en", value: "IsDeleted" },
+      ],
+    },
+  ];
+};
+
+/**
+ * 创建流程表预定义字段
+ */
+export const createFlowPredefinedFields = (
+  tableId: string,
+  isMainTable: boolean
+) => {
+  return [
+    // id
+    {
+      id: uuid(),
+      schemaName: "id",
+      type: DataType.Nvarchar,
+      displayOrder: 1,
+      maxLength: 50,
+      businessTableId: tableId,
+      isRequired: true,
+      isPreDefined: true,
+      langNameList: [
+        { name: "zh-CN", value: "Id" },
+        { name: "en", value: "Id" },
+      ],
+    },
+    // 任务id
+    {
+      id: uuid(),
+      schemaName: "TaskId",
+      type: DataType.Nvarchar,
+      displayOrder: 2,
+      maxLength: 50,
+      businessTableId: tableId,
+      isRequired: true,
+      isPreDefined: true,
+      langNameList: [
+        { name: "zh-CN", value: "任务Id" },
+        { name: "en", value: "TaskId" },
+      ],
+    },
+    ...(isMainTable
+      ? [
+          {
+            id: uuid(),
+            schemaName: "snNumber",
+            type: DataType.Nvarchar,
+            displayOrder: 3,
+            maxLength: 50,
+            businessTableId: tableId,
+            isRequired: true,
+            isPreDefined: true,
+            langNameList: [
+              { name: "zh-CN", value: "单号" },
+              { name: "en", value: "SnNumber" },
+            ],
+          },
+          {
+            id: uuid(),
+            schemaName: "taskStatus",
+            type: DataType.Int,
+            displayOrder: 4,
+            maxLength: 0,
+            businessTableId: tableId,
+            isRequired: false,
+            isPreDefined: true,
+            langNameList: [
+              { name: "zh-CN", value: "任务状态" },
+              { name: "en", value: "TaskStatus" },
+            ],
+          },
+          {
+            id: uuid(),
+            schemaName: "creationTime",
+            type: DataType.DateTime,
+            displayOrder: 5,
+            businessTableId: tableId,
+            isRequired: true,
+            isPreDefined: true,
+            langNameList: [
+              { name: "zh-CN", value: "创建时间" },
+              { name: "en", value: "CreationTime" },
+            ],
+          },
+        ]
+      : [
+          {
+            id: uuid(),
+            schemaName: "gridOrder",
+            type: DataType.Int,
+            displayOrder: 3,
+            businessTableId: tableId,
+            isRequired: false,
+            isPreDefined: true,
+            langNameList: [
+              { name: "zh-CN", value: "序号" },
+              { name: "en", value: "GridOrder" },
+            ],
+          },
+        ]),
+  ];
+};