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

- 完成项目初始化
- 实现用户注册、登录功能
- 完成用户管理与权限管理模块
- 开发后端 Tracker 服务器项目管理接口
- 实现日志管理接口
Change-Id: Ia4bde1c9ff600352a7ff0caca0cc50b02cad1af7
diff --git a/react-ui/src/pages/User/Center/Center.less b/react-ui/src/pages/User/Center/Center.less
new file mode 100644
index 0000000..430d88e
--- /dev/null
+++ b/react-ui/src/pages/User/Center/Center.less
@@ -0,0 +1,61 @@
+
+.avatarHolder {
+  margin-bottom: 16px;
+  text-align: center;
+  position: relative;
+  display: inline-block;
+  height: 120px;
+  
+  & > img {
+    width: 120px;
+    height: 120px;
+    margin-bottom: 20px;
+    border-radius: 50%;
+  }
+  &:hover:after {
+    position: absolute;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+    color: #eee;
+    font-size: 24px;
+    font-style: normal;
+    line-height: 110px;
+    background: rgba(0, 0, 0, 0.5);
+    border-radius: 50%;
+    cursor: pointer;
+    content: '+';
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+  }
+}
+
+.teamTitle {
+  margin-bottom: 12px;
+  color: @heading-color;
+  font-weight: 500;
+}
+
+.team {
+  :global {
+    .ant-avatar {
+      margin-right: 12px;
+    }
+  }
+
+  a {
+    display: block;
+    margin-bottom: 24px;
+    overflow: hidden;
+    color: @text-color;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    word-break: break-all;
+    transition: color 0.3s;
+
+    &:hover {
+      color: @primary-color;
+    }
+  }
+}
diff --git a/react-ui/src/pages/User/Center/components/AvatarCropper/cropper.css b/react-ui/src/pages/User/Center/components/AvatarCropper/cropper.css
new file mode 100644
index 0000000..7f2f350
--- /dev/null
+++ b/react-ui/src/pages/User/Center/components/AvatarCropper/cropper.css
@@ -0,0 +1,309 @@
+/*!
+ * Cropper.js v1.5.13
+ * https://fengyuanchen.github.io/cropperjs
+ *
+ * Copyright 2015-present Chen Fengyuan
+ * Released under the MIT license
+ *
+ * Date: 2022-11-20T05:30:43.444Z
+ */
+
+.cropper-container {
+  direction: ltr;
+  font-size: 0;
+  line-height: 0;
+  position: relative;
+  -ms-touch-action: none;
+      touch-action: none;
+  -webkit-user-select: none;
+     -moz-user-select: none;
+      -ms-user-select: none;
+          user-select: none;
+}
+
+.cropper-container img {
+    -webkit-backface-visibility: hidden;
+            backface-visibility: hidden;
+    display: block;
+    height: 100%;
+    image-orientation: 0deg;
+    max-height: none !important;
+    max-width: none !important;
+    min-height: 0 !important;
+    min-width: 0 !important;
+    width: 100%;
+  }
+
+.cropper-wrap-box,
+.cropper-canvas,
+.cropper-drag-box,
+.cropper-crop-box,
+.cropper-modal {
+  bottom: 0;
+  left: 0;
+  position: absolute;
+  right: 0;
+  top: 0;
+}
+
+.cropper-wrap-box,
+.cropper-canvas {
+  overflow: hidden;
+}
+
+.cropper-drag-box {
+  background-color: #fff;
+  opacity: 0;
+}
+
+.cropper-modal {
+  background-color: #000;
+  opacity: 0.5;
+}
+
+.cropper-view-box {
+  display: block;
+  height: 100%;
+  outline: 1px solid #39f;
+  outline-color: rgba(51, 153, 255, 75%);
+  overflow: hidden;
+  width: 100%;
+}
+
+.cropper-dashed {
+  border: 0 dashed #eee;
+  display: block;
+  opacity: 0.5;
+  position: absolute;
+}
+
+.cropper-dashed.dashed-h {
+    border-bottom-width: 1px;
+    border-top-width: 1px;
+    height: calc(100% / 3);
+    left: 0;
+    top: calc(100% / 3);
+    width: 100%;
+  }
+
+.cropper-dashed.dashed-v {
+    border-left-width: 1px;
+    border-right-width: 1px;
+    height: 100%;
+    left: calc(100% / 3);
+    top: 0;
+    width: calc(100% / 3);
+  }
+
+.cropper-center {
+  display: block;
+  height: 0;
+  left: 50%;
+  opacity: 0.75;
+  position: absolute;
+  top: 50%;
+  width: 0;
+}
+
+.cropper-center::before,
+  .cropper-center::after {
+    background-color: #eee;
+    content: " ";
+    display: block;
+    position: absolute;
+  }
+
+.cropper-center::before {
+    height: 1px;
+    left: -3px;
+    top: 0;
+    width: 7px;
+  }
+
+.cropper-center::after {
+    height: 7px;
+    left: 0;
+    top: -3px;
+    width: 1px;
+  }
+
+.cropper-face,
+.cropper-line,
+.cropper-point {
+  display: block;
+  height: 100%;
+  opacity: 0.1;
+  position: absolute;
+  width: 100%;
+}
+
+.cropper-face {
+  background-color: #fff;
+  left: 0;
+  top: 0;
+}
+
+.cropper-line {
+  background-color: #39f;
+}
+
+.cropper-line.line-e {
+    cursor: ew-resize;
+    right: -3px;
+    top: 0;
+    width: 5px;
+  }
+
+.cropper-line.line-n {
+    cursor: ns-resize;
+    height: 5px;
+    left: 0;
+    top: -3px;
+  }
+
+.cropper-line.line-w {
+    cursor: ew-resize;
+    left: -3px;
+    top: 0;
+    width: 5px;
+  }
+
+.cropper-line.line-s {
+    bottom: -3px;
+    cursor: ns-resize;
+    height: 5px;
+    left: 0;
+  }
+
+.cropper-point {
+  background-color: #39f;
+  height: 5px;
+  opacity: 0.75;
+  width: 5px;
+}
+
+.cropper-point.point-e {
+    cursor: ew-resize;
+    margin-top: -3px;
+    right: -3px;
+    top: 50%;
+  }
+
+.cropper-point.point-n {
+    cursor: ns-resize;
+    left: 50%;
+    margin-left: -3px;
+    top: -3px;
+  }
+
+.cropper-point.point-w {
+    cursor: ew-resize;
+    left: -3px;
+    margin-top: -3px;
+    top: 50%;
+  }
+
+.cropper-point.point-s {
+    bottom: -3px;
+    cursor: s-resize;
+    left: 50%;
+    margin-left: -3px;
+  }
+
+.cropper-point.point-ne {
+    cursor: nesw-resize;
+    right: -3px;
+    top: -3px;
+  }
+
+.cropper-point.point-nw {
+    cursor: nwse-resize;
+    left: -3px;
+    top: -3px;
+  }
+
+.cropper-point.point-sw {
+    bottom: -3px;
+    cursor: nesw-resize;
+    left: -3px;
+  }
+
+.cropper-point.point-se {
+    bottom: -3px;
+    cursor: nwse-resize;
+    height: 20px;
+    opacity: 1;
+    right: -3px;
+    width: 20px;
+  }
+
+@media (min-width: 768px) {
+
+.cropper-point.point-se {
+      height: 15px;
+      width: 15px;
+  }
+    }
+
+@media (min-width: 992px) {
+
+.cropper-point.point-se {
+      height: 10px;
+      width: 10px;
+  }
+    }
+
+@media (min-width: 1200px) {
+
+.cropper-point.point-se {
+      height: 5px;
+      opacity: 0.75;
+      width: 5px;
+  }
+    }
+
+.cropper-point.point-se::before {
+    background-color: #39f;
+    bottom: -50%;
+    content: " ";
+    display: block;
+    height: 200%;
+    opacity: 0;
+    position: absolute;
+    right: -50%;
+    width: 200%;
+  }
+
+.cropper-invisible {
+  opacity: 0;
+}
+
+.cropper-bg {
+  background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC");
+}
+
+.cropper-hide {
+  display: block;
+  height: 0;
+  position: absolute;
+  width: 0;
+}
+
+.cropper-hidden {
+  display: none !important;
+}
+
+.cropper-move {
+  cursor: move;
+}
+
+.cropper-crop {
+  cursor: crosshair;
+}
+
+.cropper-disabled .cropper-drag-box,
+.cropper-disabled .cropper-face,
+.cropper-disabled .cropper-line,
+.cropper-disabled .cropper-point {
+  cursor: not-allowed;
+}
diff --git a/react-ui/src/pages/User/Center/components/AvatarCropper/images/bg.png b/react-ui/src/pages/User/Center/components/AvatarCropper/images/bg.png
new file mode 100644
index 0000000..3c7056b
--- /dev/null
+++ b/react-ui/src/pages/User/Center/components/AvatarCropper/images/bg.png
Binary files differ
diff --git a/react-ui/src/pages/User/Center/components/AvatarCropper/index.less b/react-ui/src/pages/User/Center/components/AvatarCropper/index.less
new file mode 100644
index 0000000..dc3fecf
--- /dev/null
+++ b/react-ui/src/pages/User/Center/components/AvatarCropper/index.less
@@ -0,0 +1,10 @@
+.avatarPreview {
+	position: absolute;
+	top: 50%;
+	transform: translate(50%, -50%);
+	width: 200px;
+	height: 200px;
+	border-radius: 50%;
+	box-shadow: 0 0 4px #ccc;
+	overflow: hidden; 
+}
\ No newline at end of file
diff --git a/react-ui/src/pages/User/Center/components/AvatarCropper/index.tsx b/react-ui/src/pages/User/Center/components/AvatarCropper/index.tsx
new file mode 100644
index 0000000..83d0bcf
--- /dev/null
+++ b/react-ui/src/pages/User/Center/components/AvatarCropper/index.tsx
@@ -0,0 +1,144 @@
+import React, { useEffect, useRef, useState } from 'react';
+import { Modal, Row, Col, Button, Space, Upload, message } from 'antd';
+import { useIntl } from '@umijs/max';
+import { uploadAvatar } from '@/services/system/user';
+import { Cropper } from 'react-cropper';
+import './cropper.css';
+import styles from './index.less';
+import {
+  MinusOutlined,
+  PlusOutlined,
+  RedoOutlined,
+  UndoOutlined,
+  UploadOutlined,
+} from '@ant-design/icons';
+
+/* *
+ *
+ * @author whiteshader@163.com
+ * @datetime  2022/02/24
+ *
+ * */
+
+export type AvatarCropperProps = {
+  onFinished: (isSuccess: boolean) => void;
+  open: boolean;
+  data: any;
+};
+
+const AvatarCropperForm: React.FC<AvatarCropperProps> = (props) => {
+  const cropperRef = useRef<HTMLImageElement>(null);
+  const [avatarData, setAvatarData] = useState<any>();
+  const [previewData, setPreviewData] = useState();
+
+  useEffect(() => {
+    setAvatarData(props.data);
+  }, [props]);
+
+  const intl = useIntl();
+  const handleOk = () => {
+    const imageElement: any = cropperRef?.current;
+    const cropper: any = imageElement?.cropper;
+    cropper.getCroppedCanvas().toBlob((blob: Blob) => {
+      const formData = new FormData();
+      formData.append('avatarfile', blob);
+      uploadAvatar(formData).then((res) => {
+        if (res.code === 200) {
+          message.success(res.msg);          
+          props.onFinished(true);
+        } else {
+          message.warning(res.msg);
+        }
+      });
+    }, 'image/png');
+  };
+  const handleCancel = () => {
+    props.onFinished(false);
+  };
+  const onCrop = () => {
+    const imageElement: any = cropperRef?.current;
+    const cropper: any = imageElement?.cropper;
+    setPreviewData(cropper.getCroppedCanvas().toDataURL());
+  };
+  const onRotateRight = () => {
+    const imageElement: any = cropperRef?.current;
+    const cropper: any = imageElement?.cropper;
+    cropper.rotate(90);
+  };
+  const onRotateLeft = () => {
+    const imageElement: any = cropperRef?.current;
+    const cropper: any = imageElement?.cropper;
+    cropper.rotate(-90);
+  };
+  const onZoomIn = () => {
+    const imageElement: any = cropperRef?.current;
+    const cropper: any = imageElement?.cropper;
+    cropper.zoom(0.1);
+  };
+  const onZoomOut = () => {
+    const imageElement: any = cropperRef?.current;
+    const cropper: any = imageElement?.cropper;
+    cropper.zoom(-0.1);
+  };
+  const beforeUpload = (file: any) => {
+    const reader = new FileReader();
+    reader.readAsDataURL(file);
+    reader.onload = () => {
+      setAvatarData(reader.result);
+    };
+  };
+  return (
+    <Modal
+      width={800}
+      title={intl.formatMessage({
+        id: 'system.user.modify_avatar',
+        defaultMessage: '修改头像',
+      })}
+      open={props.open}
+      destroyOnClose
+      onOk={handleOk}
+      onCancel={handleCancel}
+    >
+      <Row gutter={[16, 16]}>
+        <Col span={12} order={1}>
+          <Cropper
+            ref={cropperRef}
+            src={avatarData}
+            style={{ height: 350, width: '100%', marginBottom: '16px' }}
+            initialAspectRatio={1}
+            guides={false}
+            crop={onCrop}
+            zoomable={true}
+            zoomOnWheel={true}
+            rotatable={true}
+          />
+        </Col>
+        <Col span={12} order={2}>
+          <div className={styles.avatarPreview}>
+            <img src={previewData} style={{ height: '100%', width: '100%' }} />
+          </div>
+        </Col>
+      </Row>
+      <Row gutter={[16, 16]}>
+        <Col span={6}>
+          <Upload beforeUpload={beforeUpload} maxCount={1}>
+            <Button>
+              <UploadOutlined />
+              上传
+            </Button>
+          </Upload>
+        </Col>
+        <Col>
+          <Space>
+            <Button icon={<RedoOutlined />} onClick={onRotateRight} />
+            <Button icon={<UndoOutlined />} onClick={onRotateLeft} />
+            <Button icon={<PlusOutlined />} onClick={onZoomIn} />
+            <Button icon={<MinusOutlined />} onClick={onZoomOut} />
+          </Space>
+        </Col>
+      </Row>
+    </Modal>
+  );
+};
+
+export default AvatarCropperForm;
diff --git a/react-ui/src/pages/User/Center/components/BaseInfo/index.tsx b/react-ui/src/pages/User/Center/components/BaseInfo/index.tsx
new file mode 100644
index 0000000..5cdca5f
--- /dev/null
+++ b/react-ui/src/pages/User/Center/components/BaseInfo/index.tsx
@@ -0,0 +1,119 @@
+import React from 'react';
+import { Form, message, Row } from 'antd';
+import { FormattedMessage, useIntl } from '@umijs/max';
+import { ProForm, ProFormRadio, ProFormText } from '@ant-design/pro-components';
+import { updateUserProfile } from '@/services/system/user';
+
+
+export type BaseInfoProps = {
+  values: Partial<API.CurrentUser> | undefined;
+};
+
+const BaseInfo: React.FC<BaseInfoProps> = (props) => {
+  const [form] = Form.useForm();
+  const intl = useIntl();
+
+  const handleFinish = async (values: Record<string, any>) => {
+    const data = { ...props.values, ...values } as API.CurrentUser;
+    const resp = await updateUserProfile(data);
+    if (resp.code === 200) {
+      message.success('修改成功');
+    } else {
+      message.warning(resp.msg);
+    }
+  };
+
+  return (
+    <>
+      <ProForm form={form} onFinish={handleFinish} initialValues={props.values}>
+        <Row>
+          <ProFormText
+            name="nickName"
+            label={intl.formatMessage({
+              id: 'system.user.nick_name',
+              defaultMessage: '用户昵称',
+            })}
+            width="xl"
+            placeholder="请输入用户昵称"
+            rules={[
+              {
+                required: true,
+                message: (
+                  <FormattedMessage id="请输入用户昵称!" defaultMessage="请输入用户昵称!" />
+                ),
+              },
+            ]}
+          />
+        </Row>
+        <Row>
+          <ProFormText
+            name="phonenumber"
+            label={intl.formatMessage({
+              id: 'system.user.phonenumber',
+              defaultMessage: '手机号码',
+            })}
+            width="xl"
+            placeholder="请输入手机号码"
+            rules={[
+              {
+                required: false,
+                message: (
+                  <FormattedMessage id="请输入手机号码!" defaultMessage="请输入手机号码!" />
+                ),
+              },
+            ]}
+          />
+        </Row>
+        <Row>
+          <ProFormText
+            name="email"
+            label={intl.formatMessage({
+              id: 'system.user.email',
+              defaultMessage: '邮箱',
+            })}
+            width="xl"
+            placeholder="请输入邮箱"
+            rules={[
+              {
+                type: 'email',
+                message: '无效的邮箱地址!',
+              },
+              {
+                required: false,
+                message: <FormattedMessage id="请输入邮箱!" defaultMessage="请输入邮箱!" />,
+              },
+            ]}
+          />
+        </Row>
+        <Row>
+          <ProFormRadio.Group
+            options={[
+              {
+                label: '男',
+                value: '0',
+              },
+              {
+                label: '女',
+                value: '1',
+              },
+            ]}
+            name="sex"
+            label={intl.formatMessage({
+              id: 'system.user.sex',
+              defaultMessage: 'sex',
+            })}
+            width="xl"
+            rules={[
+              {
+                required: false,
+                message: <FormattedMessage id="请输入性别!" defaultMessage="请输入性别!" />,
+              },
+            ]}
+          />
+        </Row>
+      </ProForm>
+    </>
+  );
+};
+
+export default BaseInfo;
diff --git a/react-ui/src/pages/User/Center/components/ResetPassword/index.tsx b/react-ui/src/pages/User/Center/components/ResetPassword/index.tsx
new file mode 100644
index 0000000..26825d4
--- /dev/null
+++ b/react-ui/src/pages/User/Center/components/ResetPassword/index.tsx
@@ -0,0 +1,84 @@
+import React from 'react';
+import { Form, message } from 'antd';
+import { FormattedMessage, useIntl } from '@umijs/max';
+import { updateUserPwd } from '@/services/system/user';
+import { ProForm, ProFormText } from '@ant-design/pro-components';
+
+const ResetPassword: React.FC = () => {
+  const [form] = Form.useForm();
+  const intl = useIntl();
+
+  const handleFinish = async (values: Record<string, any>) => {
+    const resp = await updateUserPwd(values.oldPassword, values.newPassword);
+    if (resp.code === 200) {
+      message.success('密码重置成功。');
+    } else {
+      message.warning(resp.msg);
+    }
+  };
+
+  const checkPassword = (rule: any, value: string) => {
+    const login_password = form.getFieldValue('newPassword');
+    if (value === login_password) {
+      return Promise.resolve();
+    }
+    return Promise.reject(new Error('两次密码输入不一致'));
+  };
+
+  return (
+    <>
+      <ProForm form={form} onFinish={handleFinish}>       
+          <ProFormText.Password
+            name="oldPassword"
+            label={intl.formatMessage({
+              id: 'system.user.old_password',
+              defaultMessage: '旧密码',
+            })}
+            width="xl"
+            placeholder="请输入旧密码"
+            rules={[
+              {
+                required: true,
+                message: <FormattedMessage id="请输入旧密码!" defaultMessage="请输入旧密码!" />,
+              },
+            ]}
+          />
+          <ProFormText.Password
+            name="newPassword"
+            label={intl.formatMessage({
+              id: 'system.user.new_password',
+              defaultMessage: '新密码',
+            })}
+            width="xl"
+            placeholder="请输入新密码"
+            rules={[
+              {
+                required: true,
+                message: <FormattedMessage id="请输入新密码!" defaultMessage="请输入新密码!" />,
+              },
+            ]}
+          />
+          <ProFormText.Password
+            name="confirmPassword"
+            label={intl.formatMessage({
+              id: 'system.user.confirm_password',
+              defaultMessage: '确认密码',
+            })}
+            width="xl"
+            placeholder="请输入确认密码"
+            rules={[
+              {
+                required: true,
+                message: (
+                  <FormattedMessage id="请输入确认密码!" defaultMessage="请输入确认密码!" />
+                ),
+              },
+              { validator: checkPassword },
+            ]}
+          />
+      </ProForm>
+    </>
+  );
+};
+
+export default ResetPassword;
diff --git a/react-ui/src/pages/User/Center/index.tsx b/react-ui/src/pages/User/Center/index.tsx
new file mode 100644
index 0000000..2ce308c
--- /dev/null
+++ b/react-ui/src/pages/User/Center/index.tsx
@@ -0,0 +1,200 @@
+import {
+  ClusterOutlined,
+  MailOutlined,
+  TeamOutlined,
+  UserOutlined,
+  MobileOutlined,
+  ManOutlined,
+} from '@ant-design/icons';
+import { Card, Col, Divider, List, Row } from 'antd';
+import React, { useState } from 'react';
+import styles from './Center.less';
+import BaseInfo from './components/BaseInfo';
+import ResetPassword from './components/ResetPassword';
+import AvatarCropper from './components/AvatarCropper';
+import { useRequest } from '@umijs/max';
+import { getUserInfo } from '@/services/session';
+import { PageLoading } from '@ant-design/pro-components';
+
+const operationTabList = [
+  {
+    key: 'base',
+    tab: (
+      <span>
+        基本资料
+      </span>
+    ),
+  },
+  {
+    key: 'password',
+    tab: (
+      <span>
+        重置密码
+      </span>
+    ),
+  },
+];
+
+export type tabKeyType = 'base' | 'password';
+
+const Center: React.FC = () => {
+  
+  const [tabKey, setTabKey] = useState<tabKeyType>('base');
+  
+  const [cropperModalOpen, setCropperModalOpen] = useState<boolean>(false);
+  
+  //  获取用户信息
+  const { data: userInfo, loading } = useRequest(async () => {
+    return { data: await getUserInfo()};
+  });
+  if (loading) {
+    return <div>loading...</div>;
+  }
+
+  const currentUser = userInfo?.user;
+
+  //  渲染用户信息
+  const renderUserInfo = ({
+    userName,
+    phonenumber,
+    email,
+    sex,
+    dept,
+  }: Partial<API.CurrentUser>) => {
+    return (
+      <List>
+        <List.Item>
+          <div>
+            <UserOutlined
+              style={{
+                marginRight: 8,
+              }}
+            />
+            用户名
+          </div>
+          <div>{userName}</div>
+        </List.Item>
+        <List.Item>
+          <div>
+            <ManOutlined
+              style={{
+                marginRight: 8,
+              }}
+            />
+            性别
+          </div>
+          <div>{sex === '1' ? '女' : '男'}</div>
+        </List.Item>
+        <List.Item>
+          <div>
+            <MobileOutlined
+              style={{
+                marginRight: 8,
+              }}
+            />
+            电话
+          </div>
+          <div>{phonenumber}</div>
+        </List.Item>
+        <List.Item>
+          <div>
+            <MailOutlined
+              style={{
+                marginRight: 8,
+              }}
+            />
+            邮箱
+          </div>
+          <div>{email}</div>
+        </List.Item>
+        <List.Item>
+          <div>
+            <ClusterOutlined
+              style={{
+                marginRight: 8,
+              }}
+            />
+            部门
+          </div>
+          <div>{dept?.deptName}</div>
+        </List.Item>
+      </List>
+    );
+  };
+
+  // 渲染tab切换
+  const renderChildrenByTabKey = (tabValue: tabKeyType) => {
+    if (tabValue === 'base') {
+      return <BaseInfo values={currentUser} />;
+    }
+    if (tabValue === 'password') {
+      return <ResetPassword />;
+    }
+    return null;
+  };
+
+  if (!currentUser) {
+    return <PageLoading />;
+  }
+
+  return (
+    <div>
+      <Row gutter={[16, 24]}>
+        <Col lg={8} md={24}>
+          <Card
+            title="个人信息"
+            bordered={false}
+            loading={loading}
+          >
+            {!loading && (
+              <div style={{ textAlign: "center"}}>
+                <div className={styles.avatarHolder} onClick={()=>{setCropperModalOpen(true)}}>
+                  <img alt="" src={currentUser.avatar} />
+                </div>
+                {renderUserInfo(currentUser)}
+                <Divider dashed />
+                <div className={styles.team}>
+                  <div className={styles.teamTitle}>角色</div>
+                  <Row gutter={36}>
+                    {currentUser.roles &&
+                      currentUser.roles.map((item: any) => (
+                        <Col key={item.roleId} lg={24} xl={12}>
+                          <TeamOutlined
+                            style={{
+                              marginRight: 8,
+                            }}
+                          />
+                          {item.roleName}
+                        </Col>
+                      ))}
+                  </Row>
+                </div>
+              </div>
+            )}
+          </Card>
+        </Col>
+        <Col lg={16} md={24}>
+          <Card
+            bordered={false}
+            tabList={operationTabList}
+            activeTabKey={tabKey}
+            onTabChange={(_tabKey: string) => {
+              setTabKey(_tabKey as tabKeyType);
+            }}
+          >
+            {renderChildrenByTabKey(tabKey)}
+          </Card>
+        </Col>
+      </Row>
+      <AvatarCropper
+        onFinished={() => {
+          setCropperModalOpen(false);     
+        }}
+        open={cropperModalOpen}
+        data={currentUser.avatar}
+      />
+    </div>
+  );
+};
+
+export default Center;
diff --git a/react-ui/src/pages/User/Login/index.tsx b/react-ui/src/pages/User/Login/index.tsx
new file mode 100644
index 0000000..a18f785
--- /dev/null
+++ b/react-ui/src/pages/User/Login/index.tsx
@@ -0,0 +1,436 @@
+import Footer from '@/components/Footer';
+import { getCaptchaImg, login } from '@/services/system/auth';
+import { getFakeCaptcha } from '@/services/ant-design-pro/login';
+import {
+  AlipayCircleOutlined,
+  LockOutlined,
+  MobileOutlined,
+  TaobaoCircleOutlined,
+  UserOutlined,
+  WeiboCircleOutlined,
+} from '@ant-design/icons';
+import {
+  LoginForm,
+  ProFormCaptcha,
+  ProFormCheckbox,
+  ProFormText,
+} from '@ant-design/pro-components';
+import { useEmotionCss } from '@ant-design/use-emotion-css';
+import { FormattedMessage, history, SelectLang, useIntl, useModel, Helmet } from '@umijs/max';
+import { Alert, Col, message, Row, Tabs, Image } from 'antd';
+import Settings from '../../../../config/defaultSettings';
+import React, { useEffect, useState } from 'react';
+import { flushSync } from 'react-dom';
+import { clearSessionToken, setSessionToken } from '@/access';
+
+const ActionIcons = () => {
+  const langClassName = useEmotionCss(({ token }) => {
+    return {
+      marginLeft: '8px',
+      color: 'rgba(0, 0, 0, 0.2)',
+      fontSize: '24px',
+      verticalAlign: 'middle',
+      cursor: 'pointer',
+      transition: 'color 0.3s',
+      '&:hover': {
+        color: token.colorPrimaryActive,
+      },
+    };
+  });
+
+  return (
+    <>
+      <AlipayCircleOutlined key="AlipayCircleOutlined" className={langClassName} />
+      <TaobaoCircleOutlined key="TaobaoCircleOutlined" className={langClassName} />
+      <WeiboCircleOutlined key="WeiboCircleOutlined" className={langClassName} />
+    </>
+  );
+};
+
+const Lang = () => {
+  const langClassName = useEmotionCss(({ token }) => {
+    return {
+      width: 42,
+      height: 42,
+      lineHeight: '42px',
+      position: 'fixed',
+      right: 16,
+      borderRadius: token.borderRadius,
+      ':hover': {
+        backgroundColor: token.colorBgTextHover,
+      },
+    };
+  });
+
+  return (
+    <div className={langClassName} data-lang>
+      {SelectLang && <SelectLang />}
+    </div>
+  );
+};
+
+const LoginMessage: React.FC<{
+  content: string;
+}> = ({ content }) => {
+  return (
+    <Alert
+      style={{
+        marginBottom: 24,
+      }}
+      message={content}
+      type="error"
+      showIcon
+    />
+  );
+};
+
+const Login: React.FC = () => {
+  const [userLoginState, setUserLoginState] = useState<API.LoginResult>({code: 200});
+  const [type, setType] = useState<string>('account');
+  const { initialState, setInitialState } = useModel('@@initialState');
+  const [captchaCode, setCaptchaCode] = useState<string>('');
+  const [uuid, setUuid] = useState<string>('');
+
+  const containerClassName = useEmotionCss(() => {
+    return {
+      display: 'flex',
+      flexDirection: 'column',
+      height: '100vh',
+      overflow: 'auto',
+      backgroundImage:
+        "url('https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/V-_oS6r-i7wAAAAAAAAAAAAAFl94AQBr')",
+      backgroundSize: '100% 100%',
+    };
+  });
+
+  const intl = useIntl();
+
+  const getCaptchaCode = async () => {
+    const response = await getCaptchaImg();
+    const imgdata = `data:image/png;base64,${response.img}`;
+    setCaptchaCode(imgdata);
+    setUuid(response.uuid);
+  };
+
+  const fetchUserInfo = async () => {
+    const userInfo = await initialState?.fetchUserInfo?.();
+    if (userInfo) {
+      flushSync(() => {
+        setInitialState((s) => ({
+          ...s,
+          currentUser: userInfo,
+        }));
+      });
+    }
+  };
+
+  const handleSubmit = async (values: API.LoginParams) => {
+    try {
+      // 登录
+      const response = await login({ ...values, uuid });
+      if (response.code === 200) {
+        const defaultLoginSuccessMessage = intl.formatMessage({
+          id: 'pages.login.success',
+          defaultMessage: '登录成功!',
+        });
+        const current = new Date();
+        const expireTime = current.setTime(current.getTime() + 1000 * 12 * 60 * 60);
+        console.log('login response: ', response);
+        setSessionToken(response?.token, response?.token, expireTime);
+        message.success(defaultLoginSuccessMessage);
+        await fetchUserInfo();
+        console.log('login ok');
+        const urlParams = new URL(window.location.href).searchParams;
+        history.push(urlParams.get('redirect') || '/');
+        return;
+      } else {
+        console.log(response.msg);
+        clearSessionToken();
+        // 如果失败去设置用户错误信息
+        setUserLoginState({ ...response, type });
+        getCaptchaCode();
+      }
+    } catch (error) {
+      const defaultLoginFailureMessage = intl.formatMessage({
+        id: 'pages.login.failure',
+        defaultMessage: '登录失败,请重试!',
+      });
+      console.log(error);
+      message.error(defaultLoginFailureMessage);
+    }
+  };
+  const { code } = userLoginState;
+  const loginType = type;
+
+  useEffect(() => {
+    getCaptchaCode();
+  }, []);
+
+  return (
+    <div className={containerClassName}>
+      <Helmet>
+        <title>
+          {intl.formatMessage({
+            id: 'menu.login',
+            defaultMessage: '登录页',
+          })}
+          - {Settings.title}
+        </title>
+      </Helmet>
+      <Lang />
+      <div
+        style={{
+          flex: '1',
+          padding: '32px 0',
+        }}
+      >
+        <LoginForm
+          contentStyle={{
+            minWidth: 280,
+            maxWidth: '75vw',
+          }}
+          logo={<img alt="logo" src="/logo.svg" />}
+          title="Ant Design"
+          subTitle={intl.formatMessage({ id: 'pages.layouts.userLayout.title' })}
+          initialValues={{
+            autoLogin: true,
+          }}
+          actions={[
+            <FormattedMessage
+              key="loginWith"
+              id="pages.login.loginWith"
+              defaultMessage="其他登录方式"
+            />,
+            <ActionIcons key="icons" />,
+          ]}
+          onFinish={async (values) => {
+            await handleSubmit(values as API.LoginParams);
+          }}
+        >
+          <Tabs
+            activeKey={type}
+            onChange={setType}
+            centered
+            items={[
+              {
+                key: 'account',
+                label: intl.formatMessage({
+                  id: 'pages.login.accountLogin.tab',
+                  defaultMessage: '账户密码登录',
+                }),
+              },
+              {
+                key: 'mobile',
+                label: intl.formatMessage({
+                  id: 'pages.login.phoneLogin.tab',
+                  defaultMessage: '手机号登录',
+                }),
+              },
+            ]}
+          />
+
+          {code !== 200 && loginType === 'account' && (
+            <LoginMessage
+              content={intl.formatMessage({
+                id: 'pages.login.accountLogin.errorMessage',
+                defaultMessage: '账户或密码错误(admin/admin123)',
+              })}
+            />
+          )}
+          {type === 'account' && (
+            <>
+              <ProFormText
+                name="username"
+                initialValue="admin"
+                fieldProps={{
+                  size: 'large',
+                  prefix: <UserOutlined />,
+                }}
+                placeholder={intl.formatMessage({
+                  id: 'pages.login.username.placeholder',
+                  defaultMessage: '用户名: admin',
+                })}
+                rules={[
+                  {
+                    required: true,
+                    message: (
+                      <FormattedMessage
+                        id="pages.login.username.required"
+                        defaultMessage="请输入用户名!"
+                      />
+                    ),
+                  },
+                ]}
+              />
+              <ProFormText.Password
+                name="password"
+                initialValue="admin123"
+                fieldProps={{
+                  size: 'large',
+                  prefix: <LockOutlined />,
+                }}
+                placeholder={intl.formatMessage({
+                  id: 'pages.login.password.placeholder',
+                  defaultMessage: '密码: admin123',
+                })}
+                rules={[
+                  {
+                    required: true,
+                    message: (
+                      <FormattedMessage
+                        id="pages.login.password.required"
+                        defaultMessage="请输入密码!"
+                      />
+                    ),
+                  },
+                ]}
+              />
+              <Row>
+                <Col flex={3}>
+                  <ProFormText
+                    style={{
+                      float: 'right',
+                    }}
+                    name="code"
+                    placeholder={intl.formatMessage({
+                      id: 'pages.login.captcha.placeholder',
+                      defaultMessage: '请输入验证',
+                    })}
+                    rules={[
+                      {
+                        required: true,
+                        message: (
+                          <FormattedMessage
+                            id="pages.searchTable.updateForm.ruleName.nameRules"
+                            defaultMessage="请输入验证啊"
+                          />
+                        ),
+                      },
+                    ]}
+                  />
+                </Col>
+                <Col flex={2}>
+                  <Image
+                    src={captchaCode}
+                    alt="验证码"
+                    style={{
+                      display: 'inline-block',
+                      verticalAlign: 'top',
+                      cursor: 'pointer',
+                      paddingLeft: '10px',
+                      width: '100px',
+                    }}
+                    preview={false}
+                    onClick={() => getCaptchaCode()}
+                  />
+                </Col>
+              </Row>
+            </>
+          )}
+
+          {code !== 200 && loginType === 'mobile' && <LoginMessage content="验证码错误" />}
+          {type === 'mobile' && (
+            <>
+              <ProFormText
+                fieldProps={{
+                  size: 'large',
+                  prefix: <MobileOutlined />,
+                }}
+                name="mobile"
+                placeholder={intl.formatMessage({
+                  id: 'pages.login.phoneNumber.placeholder',
+                  defaultMessage: '手机号',
+                })}
+                rules={[
+                  {
+                    required: true,
+                    message: (
+                      <FormattedMessage
+                        id="pages.login.phoneNumber.required"
+                        defaultMessage="请输入手机号!"
+                      />
+                    ),
+                  },
+                  {
+                    pattern: /^1\d{10}$/,
+                    message: (
+                      <FormattedMessage
+                        id="pages.login.phoneNumber.invalid"
+                        defaultMessage="手机号格式错误!"
+                      />
+                    ),
+                  },
+                ]}
+              />
+              <ProFormCaptcha
+                fieldProps={{
+                  size: 'large',
+                  prefix: <LockOutlined />,
+                }}
+                captchaProps={{
+                  size: 'large',
+                }}
+                placeholder={intl.formatMessage({
+                  id: 'pages.login.captcha.placeholder',
+                  defaultMessage: '请输入验证码',
+                })}
+                captchaTextRender={(timing, count) => {
+                  if (timing) {
+                    return `${count} ${intl.formatMessage({
+                      id: 'pages.getCaptchaSecondText',
+                      defaultMessage: '获取验证码',
+                    })}`;
+                  }
+                  return intl.formatMessage({
+                    id: 'pages.login.phoneLogin.getVerificationCode',
+                    defaultMessage: '获取验证码',
+                  });
+                }}
+                name="captcha"
+                rules={[
+                  {
+                    required: true,
+                    message: (
+                      <FormattedMessage
+                        id="pages.login.captcha.required"
+                        defaultMessage="请输入验证码!"
+                      />
+                    ),
+                  },
+                ]}
+                onGetCaptcha={async (phone) => {
+                  const result = await getFakeCaptcha({
+                    phone,
+                  });
+                  if (!result) {
+                    return;
+                  }
+                  message.success('获取验证码成功!验证码为:1234');
+                }}
+              />
+            </>
+          )}
+          <div
+            style={{
+              marginBottom: 24,
+            }}
+          >
+            <ProFormCheckbox noStyle name="autoLogin">
+              <FormattedMessage id="pages.login.rememberMe" defaultMessage="自动登录" />
+            </ProFormCheckbox>
+            <a
+              style={{
+                float: 'right',
+              }}
+            >
+              <FormattedMessage id="pages.login.forgotPassword" defaultMessage="忘记密码" />
+            </a>
+          </div>
+        </LoginForm>
+      </div>
+      <Footer />
+    </div>
+  );
+};
+
+export default Login;
diff --git a/react-ui/src/pages/User/Settings/index.tsx b/react-ui/src/pages/User/Settings/index.tsx
new file mode 100644
index 0000000..f29d0b9
--- /dev/null
+++ b/react-ui/src/pages/User/Settings/index.tsx
@@ -0,0 +1,20 @@
+import { PageContainer } from '@ant-design/pro-components';
+import { Card } from 'antd';
+import React from 'react';
+
+/**
+ *
+ * @author whiteshader@163.com
+ *
+ * */
+
+
+const Settings: React.FC = () => {
+  return (
+    <PageContainer>
+      <Card title="Developing" />
+    </PageContainer>
+  );
+};
+
+export default Settings;