Jelajahi Sumber

feat: 添加历史记录

liaojiaxing 5 bulan lalu
induk
melakukan
80236e666c

+ 7 - 0
apps/designer/.umirc.ts

@@ -27,6 +27,13 @@ export default defineConfig({
   request: {
     dataField: '',
   },
+  proxy: {
+    '/api': {
+      'target': 'http://ab.dev.jbpm.shalu.com/',
+      'changeOrigin': true,
+      'pathRewrite': { '^/api' : '' },
+    },
+  },
   model: {},
   unocss: {
     watch: ['src/**/*.tsx']

+ 31 - 0
apps/designer/src/api/index.ts

@@ -132,3 +132,34 @@ export const GetAllTablesAndViews = () => {
     method: "POST",
   });
 };
+
+/**
+ * 上传文件
+ * @param file 
+ * @returns
+ */
+export const UploadFile = (data: FormData) => {
+  return request<{
+    filePath: string;
+  }>("/fileApi/File/UploadFiles", {
+    method: "POST",
+    headers: {
+      "Content-Type": "multipart/form-data",
+    },
+    data,
+  });
+};
+
+/**
+ * 获取文件
+ * @param fileId 文件id 
+ * @returns
+ */
+export const GetFile = (data: {fileId: string}) => {
+  return request<{
+    filePath: string;
+  }>("/File/GetImage", {
+    method: "GET",
+    params: data
+  });
+};

+ 33 - 0
apps/designer/src/api/systemDesigner.ts

@@ -261,4 +261,37 @@ export const RecentFile = () => {
   return request("/api/flowchartMindMap/recentFile", {
     method: "POST",
   });
+};
+
+/**
+ * 历史记录列表
+ * @param
+ */
+export const HistoryList = (data: {id: string}) => {
+  return request("/api/flowchartMindMap/history/list", {
+    method: "POST",
+    data
+  });
+};
+
+/**
+ * 删除历史记录
+ * @param
+ */
+export const DeleteHistory = (data: {id: string}) => {
+  return request("/api/flowchartMindMap/history/delete", {
+    method: "POST",
+    data
+  });
+};
+
+/**
+ * 恢复历史记录
+ * @param
+ */
+export const RevertHistory = (data: {id: string}) => {
+  return request("/api/flowchartMindMap/history/revert", {
+    method: "POST",
+    data
+  });
 };

+ 1 - 1
apps/designer/src/app.ts

@@ -12,7 +12,7 @@ export const request: RequestConfig = {
   },
   requestInterceptors: [
     (url, options) => {
-      const baseUrl = process.env.NODE_ENV === 'production' ? '' : 'http://ab.dev.jbpm.shalu.com' // https://edesign.shalu.com'
+      const baseUrl = process.env.NODE_ENV === 'production' ? '' : '/api'//'http://ab.dev.jbpm.shalu.com' // https://edesign.shalu.com'
       const enterpriseCode = sessionStorage.getItem('enterpriseCode');
       const token = localStorage.getItem('token_' + enterpriseCode);
  

+ 80 - 13
apps/designer/src/components/HistoryPanel.tsx

@@ -1,15 +1,76 @@
-import { Button } from "antd";
-import React, { useState, forwardRef, useImperativeHandle } from "react";
+import { Button, Spin, Modal } from "antd";
+import React, { useState, forwardRef, useEffect } from "react";
 import { useModel } from "umi";
+import {
+  HistoryList,
+  RevertHistory,
+  DeleteHistory,
+} from "@/api/systemDesigner";
+import { useRequest } from "umi";
 
-export default forwardRef(function HistoryPanel(props, ref) {
-  const {
-    showHistory,
-    setShowHistory
-  } = useModel("appModel");
+type HistoryItem = {
+  id: string;
+  title: string;
+  createdTime: string;
+  json: string;
+};
 
+export default forwardRef(function HistoryPanel(
+  props: {
+    graphId: string;
+    onRevert: () => void;
+  },
+  ref
+) {
+  const { showHistory, setShowHistory } = useModel("appModel");
+  const [data, setData] = useState<HistoryItem[]>();
+  const { confirm } = Modal;
+  const { run, loading } = useRequest(HistoryList, {
+    manual: true,
+    defaultParams: [{ id: props.graphId }],
+    onSuccess(res) {
+      setData(res?.result);
+    },
+  });
 
-  const ItemComponent = () => {
+  useEffect(() => {
+    if (showHistory && props.graphId) {
+      run({
+        id: props.graphId,
+      });
+    }
+  }, [props.graphId, showHistory]);
+
+  const handleDeleteHistory = (id: string) => {
+    confirm({
+      title: "确定要删除该记录吗?",
+      content: "删除后不可恢复",
+      onOk() {
+        DeleteHistory({ id }).then(() => {
+          run({
+            id: props.graphId,
+          });
+        });
+      },
+    });
+  };
+
+  const handleRevertHistory = (id: string) => {
+    confirm({
+      title: "确定要恢复该记录吗?",
+      content: "",
+      onOk() {
+        RevertHistory({ id }).then(() => {
+          run({
+            id: props.graphId,
+          });
+          props.onRevert?.();
+        });
+      },
+    });
+  };
+
+  const ItemComponent = ({ history }: { history: HistoryItem }) => {
     const [showBtn, setShowBtn] = useState(false);
 
     return (
@@ -19,8 +80,8 @@ export default forwardRef(function HistoryPanel(props, ref) {
         onMouseOut={() => setShowBtn(false)}
       >
         <div className="flex-1">
-          <div className="text-sm text-#333">标题xxxxxxxxxxxxxx</div>
-          <div className="text-xs text-#999">2024-12-12 20:08:30</div>
+          <div className="text-sm text-#333">手动保存</div>
+          <div className="text-xs text-#999">{history.createdTime}</div>
         </div>
         {showBtn && (
           <div>
@@ -28,11 +89,15 @@ export default forwardRef(function HistoryPanel(props, ref) {
               type="text"
               size="small"
               icon={<i className="iconfont icon-lishijilu" />}
+              onClick={() => {
+                handleRevertHistory(history.id);
+              }}
             />
             <Button
               type="text"
               size="small"
               icon={<i className="iconfont icon-shanchu" />}
+              onClick={() => handleDeleteHistory(history.id)}
             />
           </div>
         )}
@@ -55,9 +120,11 @@ export default forwardRef(function HistoryPanel(props, ref) {
         </div>
       </div>
       <div className="flex-1 overflow-y-auto">
-        {new Array(100).fill(0).map((_, i) => (
-          <ItemComponent key={i} />
-        ))}
+        <Spin spinning={loading}>
+          {data?.map((item: HistoryItem, i: number) => (
+            <ItemComponent history={item} key={i} />
+          ))}
+        </Spin>
       </div>
     </div>
   );

+ 13 - 0
apps/designer/src/models/graphModel.ts

@@ -22,6 +22,7 @@ export default function GraphModel() {
 
   const pageNodeRef = useRef<Node>();
   const dndRef = useRef(dnd);
+  const [updateKey, setUpdateKey] = useState(0);
   // 画布
   const graphRef = useRef<Graph>();
   const { pageState } = useModel("appModel");
@@ -45,6 +46,16 @@ export default function GraphModel() {
       });
   };
 
+  useEffect(() => {
+    // 用于恢复历史后画布更新
+    if(updateKey) {
+      graphRef.current?.off("cell:added");
+      graphRef.current?.off("cell:change:*");
+      graphRef.current?.off("cell:removed");
+      pageNodeRef?.current && graphRef.current?.resetCells([pageNodeRef?.current])
+    }
+  }, [updateKey]);
+
   const addPageNode = () => {
     const graph = graphRef.current;
     pageNodeRef.current = graph?.addNode({
@@ -277,5 +288,7 @@ export default function GraphModel() {
     onUndo,
     onRedo,
     initCells,
+    updateKey,
+    setUpdateKey
   };
 }

+ 2 - 2
apps/designer/src/pages/flow/components/Content/index.tsx

@@ -13,7 +13,7 @@ import { ConnectorType } from "@/enum";
 import HistoryPanel from "@/components/HistoryPanel";
 export default function Content() {
   const stageRef = useRef<HTMLDivElement | null>(null);
-  const { initGraph } = useModel("graphModel");
+  const { initGraph, updateKey, setUpdateKey } = useModel("graphModel");
   const { onChangePageSettings, leftPanelActiveKey, setLeftPanelActiveKey } = useModel("appModel");
   const { projectInfo } = useModel("projectModel");
 
@@ -159,7 +159,7 @@ export default function Content() {
             },
           ]}
         />
-        <HistoryPanel />
+        <HistoryPanel graphId={projectInfo?.graph?.id} onRevert={() => setUpdateKey(updateKey + 1)}/>
       </div>
       <div className="w-12px drag-line"></div>
       <div

+ 29 - 15
apps/designer/src/pages/flow/components/MenuBar/index.tsx

@@ -29,6 +29,8 @@ import FindReplaceModal from "@/components/FindReplaceModal";
 import { useFindReplace } from "@/hooks/useFindReplace";
 import { GraphType } from "@/enum";
 import { EditGraph, SaveAll } from "@/api/systemDesigner";
+import { UploadFile } from "@/api";
+import { base64ToFile } from "@/utils";
 
 InsertCss(`
   .custom-color-picker-popover {
@@ -123,17 +125,29 @@ export default function MenuBar() {
   const handlePreview = () => {};
 
   // 保存
-  const handleSave = async () => {
+  const handleSave = () => {
     const elements = graph?.toJSON()?.cells;
-    await SaveAll({
-      graph: {
-        ...(projectInfo?.graph || {}),
-        ...pageState,
-        type: GraphType.flowchart
-      },
-      elements: elements?.filter(item => !item.data?.isPage)
+    graph?.toPNG(async (dataUri) => {
+      const file = base64ToFile(dataUri, projectInfo?.graph.id || '封面图', "image/png");
+
+      const formData = new FormData();
+      formData.append("file", file);
+      const res = UploadFile(formData);
+
+      await SaveAll({
+        graph: {
+          ...(projectInfo?.graph || {}),
+          ...pageState,
+          type: GraphType.flowchart
+        },
+        elements: elements?.filter(item => !item.data?.isPage)
+      });
+      message.success("保存成功");
+    }, {
+      width: 300,
+      height: 150,
+      quality: 0.2
     });
-    message.success("保存成功");
   };
 
   // 克隆 todo
@@ -292,12 +306,12 @@ export default function MenuBar() {
           key: "1-8",
           type: "divider",
         },
-        // {
-        //   key: "1-9",
-        //   label: "历史记录",
-        //   icon: <i className="w-20px iconfont icon-lishijilu" />,
-        //   onClick: handleHistory,
-        // },
+        {
+          key: "1-9",
+          label: "历史记录",
+          icon: <i className="w-20px iconfont icon-lishijilu" />,
+          onClick: handleHistory,
+        },
         {
           key: "1-0",
           label: (

+ 2 - 2
apps/designer/src/pages/flow/index.tsx

@@ -11,7 +11,7 @@ import { FlowchartMindMapInfo } from "@/api/systemDesigner";
 export default function HomePage() {
   const { showRightPanel, pageState, setPageState } = useModel("appModel");
   const { setProjectInfo } = useModel("projectModel");
-  const { initCells } = useModel("graphModel");
+  const { initCells, updateKey } = useModel("graphModel");
 
   // 获取项目详情
   const { loading, run, data } = useRequest(FlowchartMindMapInfo, {
@@ -45,7 +45,7 @@ export default function HomePage() {
       run({ id: params.id });
       sessionStorage.setItem("projectId", params.id);
     }
-  }, []);
+  }, [updateKey]);
 
   useEffect(() => {
     document.addEventListener(

+ 1 - 0
apps/designer/src/pages/home/ProjectCard.tsx

@@ -4,6 +4,7 @@ import { GraphType } from "@/enum";
 import defaultFlowImg from "@/assets/image/flow.png";
 import defaultMindImg from "@/assets/image/mindmap.png";
 import { DeleteGraph, EditGraph } from "@/api/systemDesigner";
+import { GetFile } from "@/api";
 
 export default function ProjectCard({
   record,

+ 31 - 1
apps/designer/src/utils/index.ts

@@ -87,4 +87,34 @@ export const listToTree = (list: TreeNode[], parent: TreeNode): TreeNode => {
     }
   });
   return parent;
-};
+};
+
+/**
+ * base64 转 file
+ * @param base64String 
+ * @param fileName 
+ * @param fileType 
+ * @returns 
+ */
+export function base64ToFile(base64String: string, fileName: string, fileType: string): File {
+  // 移除Base64字符串中的前缀(如"data:image/png;base64,")
+  const base64Data = base64String.split(',')[1];
+  
+  // 解码Base64字符串
+  const byteCharacters = atob(base64Data);
+  
+  // 创建一个Uint8Array来存储二进制数据
+  const byteArrays = new Uint8Array(byteCharacters.length);
+  
+  for (let i = 0; i < byteCharacters.length; i++) {
+    byteArrays[i] = byteCharacters.charCodeAt(i);
+  }
+  
+  // 创建Blob对象
+  const blob = new Blob([byteArrays], { type: fileType });
+  
+  // 创建File对象
+  const file = new File([blob], fileName, { type: fileType });
+  
+  return file;
+}

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


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

@@ -0,0 +1,82 @@
+import React from 'react';
+import { LaptopOutlined, NotificationOutlined, UserOutlined } from '@ant-design/icons';
+import type { MenuProps } from 'antd';
+import { Breadcrumb, Layout, Menu, theme } from 'antd';
+
+const { Header, Content, Sider } = Layout;
+
+const items1: MenuProps['items'] = ['1', '2', '3'].map((key) => ({
+  key,
+  label: `nav ${key}`,
+}));
+
+const items2: MenuProps['items'] = [UserOutlined, LaptopOutlined, NotificationOutlined].map(
+  (icon, index) => {
+    const key = String(index + 1);
+
+    return {
+      key: `sub${key}`,
+      icon: React.createElement(icon),
+      label: `subnav ${key}`,
+
+      children: new Array(4).fill(null).map((_, j) => {
+        const subKey = index * 4 + j + 1;
+        return {
+          key: subKey,
+          label: `option${subKey}`,
+        };
+      }),
+    };
+  },
+);
+
+const App: React.FC = () => {
+  const {
+    token: { colorBgContainer, borderRadiusLG },
+  } = theme.useToken();
+
+  return (
+    <Layout>
+      <Header style={{ display: 'flex', alignItems: 'center' }}>
+        <div className="demo-logo" />
+        <Menu
+          theme="dark"
+          mode="horizontal"
+          defaultSelectedKeys={['2']}
+          items={items1}
+          style={{ flex: 1, minWidth: 0 }}
+        />
+      </Header>
+      <Layout>
+        <Sider width={200} style={{ background: colorBgContainer }}>
+          <Menu
+            mode="inline"
+            defaultSelectedKeys={['1']}
+            defaultOpenKeys={['sub1']}
+            style={{ height: '100%', borderRight: 0 }}
+            items={items2}
+          />
+        </Sider>
+        <Layout style={{ padding: '0 24px 24px' }}>
+          <Breadcrumb
+            items={[{ title: 'Home' }, { title: 'List' }, { title: 'App' }]}
+            style={{ margin: '16px 0' }}
+          />
+          <Content
+            style={{
+              padding: 24,
+              margin: 0,
+              minHeight: 280,
+              background: colorBgContainer,
+              borderRadius: borderRadiusLG,
+            }}
+          >
+            Content
+          </Content>
+        </Layout>
+      </Layout>
+    </Layout>
+  );
+};
+
+export default App;

+ 32 - 1
pnpm-lock.yaml

@@ -216,6 +216,37 @@ importers:
         specifier: ^5
         version: 5.5.4
 
+  apps/er-designer:
+    dependencies:
+      '@repo/ui':
+        specifier: workspace:*
+        version: link:../../packages/ui
+      '@repo/x6-plugin-selection':
+        specifier: workspace:*
+        version: link:../../packages/x6-plugin-selection
+      '@unocss/cli':
+        specifier: ^0.62.3
+        version: 0.62.3
+      umi:
+        specifier: ^4.3.18
+        version: 4.3.19(@babel/core@7.25.2)(@types/react@18.3.5)(eslint@8.57.0)(prettier@3.3.3)(react-dom@18.3.1)(react@18.3.1)(stylelint@14.16.1)(typescript@5.5.4)(webpack@5.94.0)
+      unocss:
+        specifier: ^0.62.3
+        version: 0.62.3(postcss@8.4.45)(vite@5.4.3)
+    devDependencies:
+      '@iconify-json/ant-design':
+        specifier: 1.2.1
+        version: 1.2.1
+      '@types/react':
+        specifier: ^18.0.33
+        version: 18.3.5
+      '@types/react-dom':
+        specifier: ^18.0.11
+        version: 18.3.0
+      typescript:
+        specifier: ^5.0.3
+        version: 5.5.4
+
   packages/eslint-config:
     devDependencies:
       '@typescript-eslint/eslint-plugin':
@@ -3486,7 +3517,7 @@ packages:
       postcss: '>=7.0.0'
       postcss-syntax: '>=0.36.2'
     dependencies:
-      '@babel/core': 7.23.6
+      '@babel/core': 7.25.2
       postcss: 8.4.45
       postcss-syntax: 0.36.2(postcss@8.4.45)
     transitivePeerDependencies: