feat: 初始化项目并完成基础功能开发

- 完成项目初始化
- 实现用户注册、登录功能
- 完成用户管理与权限管理模块
- 开发后端 Tracker 服务器项目管理接口
- 实现日志管理接口
Change-Id: Ia4bde1c9ff600352a7ff0caca0cc50b02cad1af7
diff --git a/react-ui/src/app.tsx b/react-ui/src/app.tsx
new file mode 100644
index 0000000..38f76b4
--- /dev/null
+++ b/react-ui/src/app.tsx
@@ -0,0 +1,233 @@
+import { Footer, Question, SelectLang, AvatarDropdown, AvatarName } from '@/components';
+import { LinkOutlined } from '@ant-design/icons';
+import type { Settings as LayoutSettings } from '@ant-design/pro-components';
+import { SettingDrawer } from '@ant-design/pro-components';
+import type { RunTimeLayoutConfig } from '@umijs/max';
+import { history, Link } from '@umijs/max';
+import defaultSettings from '../config/defaultSettings';
+import { errorConfig } from './requestErrorConfig';
+import { clearSessionToken, getAccessToken, getRefreshToken, getTokenExpireTime } from './access';
+import { getRemoteMenu, getRoutersInfo, getUserInfo, patchRouteWithRemoteMenus, setRemoteMenu } from './services/session';
+import { PageEnum } from './enums/pagesEnums';
+
+
+const isDev = process.env.NODE_ENV === 'development';
+
+
+
+/**
+ * @see  https://umijs.org/zh-CN/plugins/plugin-initial-state
+ * */
+export async function getInitialState(): Promise<{
+  settings?: Partial<LayoutSettings>;
+  currentUser?: API.CurrentUser;
+  loading?: boolean;
+  fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
+}> {
+  const fetchUserInfo = async () => {
+    try {
+      const response = await getUserInfo({
+        skipErrorHandler: true,
+      });
+      if (response.user.avatar === '') {
+        response.user.avatar =
+          'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png';
+      }
+      return {
+        ...response.user,
+        permissions: response.permissions,
+        roles: response.roles,
+      } as API.CurrentUser;
+    } catch (error) {
+      console.log(error);
+      history.push(PageEnum.LOGIN);
+    }
+    return undefined;
+  };
+  // 如果不是登录页面,执行
+  const { location } = history;
+  if (location.pathname !== PageEnum.LOGIN) {
+    const currentUser = await fetchUserInfo();
+    return {
+      fetchUserInfo,
+      currentUser,
+      settings: defaultSettings as Partial<LayoutSettings>,
+    };
+  }
+  return {
+    fetchUserInfo,
+    settings: defaultSettings as Partial<LayoutSettings>,
+  };
+}
+
+// ProLayout 支持的api https://procomponents.ant.design/components/layout
+export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) => {
+  return {
+    actionsRender: () => [<Question key="doc" />, <SelectLang key="SelectLang" />],
+    avatarProps: {
+      src: initialState?.currentUser?.avatar,
+      title: <AvatarName />,
+      render: (_, avatarChildren) => {
+        return <AvatarDropdown menu="True">{avatarChildren}</AvatarDropdown>;
+      },
+    },
+    waterMarkProps: {
+      // content: initialState?.currentUser?.nickName,
+    },
+    menu: {
+      locale: false,
+      // 每当 initialState?.currentUser?.userid 发生修改时重新执行 request
+      params: {
+        userId: initialState?.currentUser?.userId,
+      },
+      request: async () => {
+        if (!initialState?.currentUser?.userId) {
+          return [];
+        }
+        return getRemoteMenu();
+      },
+    },
+    footerRender: () => <Footer />,
+    onPageChange: () => {
+      const { location } = history;
+      // 如果没有登录,重定向到 login
+      if (!initialState?.currentUser && location.pathname !== PageEnum.LOGIN) {
+        history.push(PageEnum.LOGIN);
+      }
+    },
+    layoutBgImgList: [
+      {
+        src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/D2LWSqNny4sAAAAAAAAAAAAAFl94AQBr',
+        left: 85,
+        bottom: 100,
+        height: '303px',
+      },
+      {
+        src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/C2TWRpJpiC0AAAAAAAAAAAAAFl94AQBr',
+        bottom: -68,
+        right: -45,
+        height: '303px',
+      },
+      {
+        src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/F6vSTbj8KpYAAAAAAAAAAAAAFl94AQBr',
+        bottom: 0,
+        left: 0,
+        width: '331px',
+      },
+    ],
+    links: isDev
+      ? [
+        <Link key="openapi" to="/umi/plugin/openapi" target="_blank">
+          <LinkOutlined />
+          <span>OpenAPI 文档</span>
+        </Link>,
+      ]
+      : [],
+    menuHeaderRender: undefined,
+    // 自定义 403 页面
+    // unAccessible: <div>unAccessible</div>,
+    // 增加一个 loading 的状态
+    childrenRender: (children) => {
+      // if (initialState?.loading) return <PageLoading />;
+      return (
+        <>
+          {children}
+          <SettingDrawer
+            disableUrlParams
+            enableDarkTheme
+            settings={initialState?.settings}
+            onSettingChange={(settings) => {
+              setInitialState((preInitialState) => ({
+                ...preInitialState,
+                settings,
+              }));
+            }}
+          />
+        </>
+      );
+    },
+    ...initialState?.settings,
+  };
+};
+
+export async function onRouteChange({ clientRoutes, location }) {
+  const menus = getRemoteMenu();
+ // console.log('onRouteChange', clientRoutes, location, menus);
+  if(menus === null && location.pathname !== PageEnum.LOGIN) {
+    console.log('refresh')
+    history.go(0);
+  }
+}
+
+// export function patchRoutes({ routes, routeComponents }) {
+//   console.log('patchRoutes', routes, routeComponents);
+// }
+
+
+export async function patchClientRoutes({ routes }) {
+  // console.log('patchClientRoutes', routes);
+  patchRouteWithRemoteMenus(routes);
+}
+
+export function render(oldRender: () => void) {
+  // console.log('render get routers', oldRender)
+  const token = getAccessToken();
+  if(!token || token?.length === 0) {
+    oldRender();
+    return;
+  }
+  getRoutersInfo().then(res => {
+    setRemoteMenu(res);
+    oldRender()
+  });
+}
+
+/**
+ * @name request 配置,可以配置错误处理
+ * 它基于 axios 和 ahooks 的 useRequest 提供了一套统一的网络请求和错误处理方案。
+ * @doc https://umijs.org/docs/max/request#配置
+ */
+const checkRegion = 5 * 60 * 1000;
+
+export const request = {
+  ...errorConfig,
+  requestInterceptors: [
+    (url: any, options: { headers: any }) => {
+      const headers = options.headers ? options.headers : [];
+      console.log('request ====>:', url);
+      const authHeader = headers['Authorization'];
+      const isToken = headers['isToken'];
+      if (!authHeader && isToken !== false) {
+        const expireTime = getTokenExpireTime();
+        if (expireTime) {
+          const left = Number(expireTime) - new Date().getTime();
+          const refreshToken = getRefreshToken();
+          if (left < checkRegion && refreshToken) {
+            if (left < 0) {
+              clearSessionToken();
+            }
+          } else {
+            const accessToken = getAccessToken();
+            if (accessToken) {
+              headers['Authorization'] = `Bearer ${accessToken}`;
+            }
+          }
+        } else {
+          clearSessionToken();
+        }
+      }
+      return { url, options };
+    },
+  ],
+  responseInterceptors: [
+    // (response) =>
+    // {
+    //   // // 不再需要异步处理读取返回体内容,可直接在data中读出,部分字段可在 config 中找到
+    //   // const { data = {} as any, config } = response;
+    //   // // do something
+    //   // console.log('data: ', data)
+    //   // console.log('config: ', config)
+    //   return response
+    // },
+  ],
+};