Przeglądaj źródła

feat: 新增excel模版导入,excel数据解析

liaojiaxing 3 miesięcy temu
rodzic
commit
2f2b30328c

+ 163 - 0
apps/er-designer/src/components/ImportResultModal.tsx

@@ -0,0 +1,163 @@
+import { forwardRef, useImperativeHandle, useRef, useState } from "react";
+import { message, Modal, Table, Tooltip } from "antd";
+import { ColumnItem } from "@/type";
+import { TableProps } from "antd/lib";
+import { createColumn } from "@/utils";
+
+export default forwardRef(function ImportResultModal(props: { tableId?: string}, ref) {
+  const [open, setOpen] = useState(false);
+  const [dataSource, setDataSource] = useState<any[]>([]);
+  const originData = useRef<ColumnItem[]>([]);
+  const [modal, contextHolder] = Modal.useModal();
+  useImperativeHandle(ref, () => {
+    return {
+      open: (data: any[], columnList: ColumnItem[]) => {
+        originData.current = columnList;
+        setDataSource(data);
+        setOpen(true);
+      },
+    };
+  });
+
+  // 校验字段名
+  const validateSchemaName = (val: string) => {
+    const regex = /^[a-z][a-z0-9_]*$/;
+    let msg = "";
+    if (!val) {
+      msg = "编码不能为空!";
+    }
+    if (!regex.test(val)) {
+      msg = "编码只能包含字母和数字, 必须小写字母开头!";
+    }
+    if (val?.length > 50) {
+      msg += "!编码长度不能超过50";
+    }
+
+    return msg;
+  };
+
+  // 校验类型值
+  const validateTypeValue = (val: string) => {
+    return !["Int", "Nvarchar", "Real", "Bit", "Decimal", "DateTime"].includes(
+      val
+    )
+      ? "类型值错误!"
+      : "";
+  };
+
+  // 长度校验
+  const validateLength = (val: string | number, type: string) => {
+    let msg = "";
+    if (type === "Int" || type === "DateTime") {
+      if (parseInt(val + "") > 0) {
+        msg = "Int和DateTime类型长度为0!";
+      }
+    }
+    if (type === "Nvarchar") {
+      if (typeof val === "string" && val.length > 4000) {
+        msg = "长度不能超过4000!";
+      }
+    }
+    return msg;
+  };
+
+  const handleSubmit = () => {
+    let valid = true;
+    dataSource.forEach((item) => {
+      if (
+        validateSchemaName(item["字段名"]) ||
+        validateTypeValue(item["类型"]) ||
+        validateLength(item["长度"], item["类型"])
+      ) {
+        valid = false;
+      }
+    });
+    if(!valid) {
+      message.error("数据校验失败,请检查数据修改后重新导入!");
+      return;
+    }
+
+    modal.confirm({
+      title: "确认导入",
+      content: "导入会覆盖原有的内容, 是否继续?",
+      okText: "确认",
+      cancelText: "取消",
+      onOk: () => {
+        const addList = dataSource.map((item, i) => {
+          const old = originData.current.find((obj) => obj.schemaName === item?.['字段名']);
+          const newColumn = createColumn(props?.tableId, old?.displayOrder || originData.current.length + i + 1);
+          return newColumn;
+        });
+        setOpen(false);
+      },
+      onCancel: () => {
+        setOpen(false);
+      },
+    });
+  };
+
+  const columns: TableProps["columns"] = [
+    {
+      title: "字段名",
+      dataIndex: "字段名",
+      render: (text) => {
+        const msg = validateSchemaName(text);
+        return (
+          <Tooltip title={msg}>
+            <span style={{ color: msg ? "red" : "black" }}>
+              {text || "字段名为空"}
+            </span>
+          </Tooltip>
+        );
+      },
+    },
+    {
+      title: "类型",
+      dataIndex: "类型",
+      render: (text) => {
+        const msg = validateTypeValue(text);
+        return (
+          <Tooltip title={msg}>
+            <span style={{ color: msg ? "red" : "black" }}>{text}</span>
+          </Tooltip>
+        );
+      },
+    },
+    {
+      title: "长度",
+      dataIndex: "长度",
+      render: (text, record) => {
+        const msg = validateLength(text, record?.["类型"]);
+        return (
+          <Tooltip title={msg}>
+            <span style={{ color: msg ? "red" : "black" }}>{text}</span>
+          </Tooltip>
+        );
+      },
+    },
+    { title: "中文名", dataIndex: "中文名" },
+    { title: "英文名", dataIndex: "英文名" },
+    { title: "是否必填", dataIndex: "是否必填" },
+    { title: "描述", dataIndex: "描述" },
+    { title: "默认值", dataIndex: "默认值" },
+  ];
+
+  return (
+    <Modal
+      title="导入结果"
+      open={open}
+      width="80%"
+      okText="导入"
+      onOk={handleSubmit}
+      onCancel={() => setOpen(false)}
+    >
+      {contextHolder}
+      <Table
+        key={"字段名"}
+        scroll={{ y: 400 }}
+        columns={columns}
+        dataSource={dataSource}
+      />
+    </Modal>
+  );
+});

+ 94 - 6
apps/er-designer/src/components/TableEdit.tsx

@@ -4,11 +4,27 @@ import type { ColumnItem } from "@/type";
 import { createColumn } from "@/utils";
 import { DataType } from "@/enum";
 import { DATA_TYPE_OPTIONS } from "@/constants";
-import { Button, Input, InputNumber, Switch } from "antd";
+import {
+  Button,
+  Input,
+  InputNumber,
+  message,
+  Switch,
+  Tooltip,
+  Upload,
+  UploadFile,
+} from "antd";
 import LangInput from "./LangInput";
 import { validateColumnCode } from "@/utils/validator";
 import VariableModal from "./VariableModal";
 import { FormInstance } from "antd/lib";
+import {
+  DownloadOutlined,
+  InfoCircleOutlined,
+  UploadOutlined,
+} from "@ant-design/icons";
+import { parseExcel } from "@/utils/parseExcel";
+import ImportResultModal from "./ImportResultModal";
 export default function TableEdit(props: {
   tableId?: string;
   data: any[];
@@ -20,6 +36,7 @@ export default function TableEdit(props: {
     props.data
   );
   const boxRef = React.useRef<HTMLDivElement>(null);
+  const importResultRef = React.useRef<{open: (data: any[], columns: readonly ColumnItem[]) => void}>();
 
   useEffect(() => {
     props.onChange?.(dataSource);
@@ -131,7 +148,7 @@ export default function TableEdit(props: {
       renderFormItem: (_schema, config, form) => {
         const model = config.record;
         const rowKey = config.recordKey;
-        console.log(model)
+        console.log(model);
         return (
           <span>
             <LangInput
@@ -187,7 +204,7 @@ export default function TableEdit(props: {
         const model = config.record;
         const rowKey = config.recordKey;
         return model.type === DataType.Nvarchar ? (
-          <InputNumber min={0} placeholder="字符长度" />
+          <InputNumber min={0} max={4000} placeholder="字符长度" />
         ) : model.type === DataType.Decimal ? (
           <LengthComp model={model} form={form} rowKey={rowKey} />
         ) : (
@@ -253,6 +270,13 @@ export default function TableEdit(props: {
         return <Input.TextArea placeholder="描述..." />;
       },
     },
+    {
+      title: "预定义字段",
+      dataIndex: "isPreDefined",
+      renderText: (text, record) => {
+        return record.isPreDefined ? "是" : "否";
+      },
+    },
     {
       title: "操作",
       valueType: "option",
@@ -287,8 +311,31 @@ export default function TableEdit(props: {
     return createColumn(props?.tableId, dataSource.length + 1);
   };
 
+  // 上传字段模版文件
+  const handleUpload = (file: UploadFile) => {
+    message.loading("正在解析文件...", 0);
+    parseExcel<any>(file)
+      .then((res) => {
+        console.log("加载数据:", res);
+        const list = res?.["表单字段"];
+        if (!list || !list.length) {
+          message.warning("当前文件无字段数据,请检查");
+        } else {
+          importResultRef.current?.open(list, dataSource);
+        }
+      })
+      .catch((err) => {
+        console.error("加载数据失败:", err);
+        message.error("文件解析失败");
+      })
+      .finally(() => {
+        message.destroy();
+      });
+  };
+
   return (
     <div className="w-full h-full overflow-auto" ref={boxRef}>
+      <ImportResultModal ref={importResultRef} tableId={props?.tableId} />
       <EditableProTable
         columns={columns}
         rowKey="id"
@@ -300,11 +347,52 @@ export default function TableEdit(props: {
         editable={{
           type: "multiple",
           editableKeys,
-          // onSave: async (rowKey, data, row) => {
-          //   console.log(rowKey, data, row, dataSource);
-          // },
           onChange: setEditableRowKeys,
         }}
+        toolBarRender={() => [
+          <Tooltip
+            key="info"
+            title={
+              <div>
+                <p>填写规范:</p>
+                <p>
+                  1、字段名第一位必须为大写字母,之后的对象可以字母大小写,数字或下划线;
+                </p>
+                <p>2、Nvarchar类型长度最大不可以超过4000;</p>
+                <p>
+                  3、Decimal类型精度的填写方式:总精度,有效小数位,eg: 18,6;
+                </p>
+                <p>4、Int、Datetime等类型长度需填写为0;</p>
+              </div>
+            }
+          >
+            <InfoCircleOutlined />
+          </Tooltip>,
+          <Button key="download" icon={<DownloadOutlined />}>
+            <a
+              download={"BusinessTableColumnsImportTemplate.xlsx"}
+              href="/Content/Template/BusinessTableColumnsImportTemplate.xlsx"
+            >
+              下载模版
+            </a>
+          </Button>,
+          <Upload
+            key="upload"
+            accept=".xlsx"
+            showUploadList={false}
+            beforeUpload={(file) => handleUpload(file)}
+          >
+            <Button
+              type="primary"
+              icon={<UploadOutlined />}
+              onClick={() => {
+                handleAdd();
+              }}
+            >
+              导入字段
+            </Button>
+          </Upload>,
+        ]}
       />
     </div>
   );

+ 2 - 2
apps/er-designer/src/layouts/index.less

@@ -20,11 +20,11 @@ body {
 }
 
 .x6-graph-svg {
-  z-index: 1;
+  z-index: 2;
 }
 
 .x6-widget-selection-selected {
-  z-index: 0;
+  z-index: 1;
 }
 
 .x6-widget-transform {

+ 13 - 0
apps/er-designer/src/models/erModel.tsx

@@ -68,6 +68,10 @@ export default function erModel() {
     useSessionStorageState("tabs-active-key");
   const [_relationActive, setRelationActive] =
     useSessionStorageState("relation-active");
+  const [tableActive, setTableActive] = useSessionStorageState<string>('table-active', {
+      defaultValue: "",
+      listenStorageChange: true
+    });
 
   const timer = useRef<any>();
   const saveData = (info: ProjectInfo) => {
@@ -321,6 +325,7 @@ export default function erModel() {
     instance.on("node:resized", (args) => {
       console.log("node:resized", args);
       const size = args.node.getSize();
+      const position = args.node.position();
       const data = args.node.data;
       if (data.isTopicArea) {
         updateTopicArea({
@@ -329,6 +334,8 @@ export default function erModel() {
             ...data.style,
             width: size.width,
             height: size.height,
+            x: position.x,
+            y: position.y,
           },
         });
       }
@@ -339,6 +346,8 @@ export default function erModel() {
             ...data.style,
             width: size.width,
             height: size.height,
+            x: position.x,
+            y: position.y,
           },
         });
       }
@@ -418,6 +427,8 @@ export default function erModel() {
       tables: list,
     });
     setTabActiveKey("1");
+    setTableActive(newTable.table.id);
+    graphRef.current?.select(graphRef.current?.getCellById(newTable.table.id));
   };
 
   /**
@@ -965,5 +976,7 @@ export default function erModel() {
     exitPlayMode,
     saveTime,
     onSave,
+    tableActive,
+    setTableActive
   };
 }

+ 1 - 0
apps/er-designer/src/pages/detail/components/AddTable.tsx

@@ -152,6 +152,7 @@ export default forwardRef(function AddTable(
         setOpen(false);
       });
     }
+    // 引入
   };
 
   return (

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

@@ -12,7 +12,6 @@ export default function Navigator() {
 
   useEffect(() => {
     if (graph && mapRef.current) {
-      console.log('mini map');
       graph.use(
         new MiniMap({
           container: mapRef.current,

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

@@ -3,19 +3,13 @@ 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";
 import noData from "@/assets/no-data.png";
-import { useSessionStorageState } from "ahooks";
 export default function TablePanel() {
-  const { project, updateTable, addTable } = useModel("erModel");
+  const { project, updateTable, addTable, tableActive, setTableActive } = useModel("erModel");
   const contentRef = React.useRef<HTMLDivElement>(null);
   const [contentStyle, setContentStyle] = React.useState<React.CSSProperties>(
     {}
   );
-  const [active, setActive] = useSessionStorageState<string>('table-active', {
-    defaultValue: "",
-    listenStorageChange: true
-  });
 
   useEffect(() => {
     // 计算高度
@@ -43,8 +37,8 @@ export default function TablePanel() {
             data={item}
             onChange={updateTable}
             key={item.table.id}
-            active={active}
-            setActive={setActive}
+            active={tableActive}
+            setActive={setTableActive}
           />
         );
       })}

+ 48 - 0
apps/er-designer/src/utils/parseExcel.ts

@@ -0,0 +1,48 @@
+import * as XLSX from "xlsx";
+
+/**
+ * 解析Excel文件
+ * @param file
+ * @returns
+ */
+export const parseExcel = <T>(file: any): Promise<Record<string, T>> => {
+  return new Promise((resolve, reject) => {
+    const reader = new FileReader();
+    reader.onload = (e) => {
+      const data = e.target?.result;
+      const workbook = XLSX.read(data, { type: "binary" });
+      const result: Record<string, any> = {};
+      // 处理sheet并转换sheet数据 第一行为key 后面数据为value
+      workbook.SheetNames.forEach((sheetName) => {
+        const worksheet = workbook.Sheets[sheetName];
+        const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
+        if (jsonData.length === 0) {
+          result[sheetName] = jsonData;
+        } else {
+          const keys = jsonData[0] as string[];
+          const list = [];
+          for (let i = 1; i < jsonData.length; i++) {
+            const row = jsonData[i] as any[];
+            if(row.length === 0) {
+              continue;
+            }
+            const obj: Record<string, any> = {};
+            for (let j = 0; j < keys.length; j++) {
+              obj[keys[j]] = row[j];
+            }
+            list.push(obj);
+          }
+          result[sheetName] = list;
+        }
+      });
+
+      resolve(result);
+    };
+
+    reader.onerror = (e) => {
+      reject(e);
+    };
+
+    reader.readAsArrayBuffer(new Blob([file], { type: file.type }));
+  });
+};

+ 2 - 1
package.json

@@ -64,6 +64,7 @@
     "react-draggable": "^4.4.6",
     "thememirror": "^2.0.1",
     "umi": "^4.3.18",
-    "unocss": "^0.62.3"
+    "unocss": "^0.62.3",
+    "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
   }
 }

+ 11 - 0
pnpm-lock.yaml

@@ -149,6 +149,9 @@ importers:
       unocss:
         specifier: ^0.62.3
         version: 0.62.3(postcss@8.4.45)(vite@5.4.3)
+      xlsx:
+        specifier: https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz
+        version: '@cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz'
     devDependencies:
       '@umijs/plugins':
         specifier: ^4.3.19
@@ -14993,3 +14996,11 @@ packages:
   /zod@3.23.8:
     resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
     dev: false
+
+  '@cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz':
+    resolution: {tarball: https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz}
+    name: xlsx
+    version: 0.20.3
+    engines: {node: '>=0.8'}
+    hasBin: true
+    dev: false