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

- 完成项目初始化
- 实现用户注册、登录功能
- 完成用户管理与权限管理模块
- 开发后端 Tracker 服务器项目管理接口
- 实现日志管理接口
Change-Id: Ia4bde1c9ff600352a7ff0caca0cc50b02cad1af7
diff --git a/react-ui/src/pages/Monitor/Cache/List.tsx b/react-ui/src/pages/Monitor/Cache/List.tsx
new file mode 100644
index 0000000..af6147a
--- /dev/null
+++ b/react-ui/src/pages/Monitor/Cache/List.tsx
@@ -0,0 +1,240 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { clearCacheAll, clearCacheKey, clearCacheName, getCacheValue, listCacheKey, listCacheName } from '@/services/monitor/cachelist';
+import { Button, Card, Col, Form, FormInstance, Input, message, Row, Table } from 'antd';
+import styles from './index.less';
+import { FormattedMessage } from '@umijs/max';
+import { ReloadOutlined } from '@ant-design/icons';
+import { ProForm } from '@ant-design/pro-components';
+
+const { TextArea } = Input;
+
+
+/* *
+ *
+ * @author whiteshader@163.com
+ * @datetime  2022/06/27
+ *
+ * */
+
+
+
+const CacheList: React.FC = () => {
+  const [cacheNames, setCacheNames] = useState<any>([]);
+  const [currentCacheName, setCurrentCacheName] = useState<any>([]);
+  const [cacheKeys, setCacheKeys] = useState<any>([]);
+  const [form] = Form.useForm();
+
+  const getCacheNames = () => {
+    listCacheName().then((res) => {
+      if (res.code === 200) {
+        setCacheNames(res.data);
+      }
+    });
+  }
+
+  useEffect(() => {
+    getCacheNames();
+  }, []);
+
+  const getCacheKeys = (cacheName: string) => {
+    listCacheKey(cacheName).then(res => {
+      if (res.code === 200) {
+        let index = 0;
+        const keysData = res.data.map((item: any) => {
+          return {
+            index: index++,
+            cacheKey: item
+          }
+        })
+        setCacheKeys(keysData);
+      }
+    });
+  };
+
+  const onClearAll = async () => {
+    clearCacheAll().then(res => {
+      if(res.code === 200) {
+        message.success("清理全部缓存成功");
+      }
+    });
+  };
+
+  const onClearAllFailed = (errorInfo: any) => {
+    message.error('Failed:', errorInfo);
+  };
+
+  const refreshCacheNames = () => {
+    getCacheNames();
+    message.success("刷新缓存列表成功");
+  };
+
+  const refreshCacheKeys = () => {
+    getCacheKeys(currentCacheName);
+    message.success("刷新键名列表成功");
+  };
+
+  const columns = [
+    {
+      title: '缓存名称',
+      dataIndex: 'cacheName',
+      key: 'cacheName',
+      render: (_: any, record: any) => {
+        return record.cacheName.replace(":", "");
+      }
+    },
+    {
+      title: '备注',
+      dataIndex: 'remark',
+      key: 'remark',
+    },
+    {
+      title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />,
+      dataIndex: 'option',
+      width: '40px',
+      valueType: 'option',
+      render: (_: any, record: API.Monitor.CacheContent) => [
+        <Button
+          type="link"
+          size="small"
+          key="remove"
+          danger
+          onClick={() => {
+            clearCacheName(record.cacheName).then(res => {
+              if(res.code === 200) {
+                message.success("清理缓存名称[" + record.cacheName + "]成功");
+                getCacheKeys(currentCacheName);
+              }
+            });
+          }}
+        >
+          <FormattedMessage id="pages.searchTable.delete" defaultMessage="删除" />
+        </Button>,
+      ]
+    }
+  ];
+
+  const cacheKeysColumns = [
+    {
+      title: '序号',
+      dataIndex: 'index',
+      key: 'index'
+    },
+    {
+      title: '缓存键名',
+      dataIndex: 'cacheKey',
+      key: 'cacheKey',
+      render: (_: any, record: any) => {
+        return record.cacheKey.replace(currentCacheName, "");
+      }
+    },
+    {
+      title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />,
+      dataIndex: 'option',
+      width: '40px',
+      valueType: 'option',
+      render: (_: any, record: API.Monitor.CacheContent) => [
+        <Button
+          type="link"
+          size="small"
+          key="remove"
+          danger
+          onClick={() => {
+            console.log(record)
+            clearCacheKey(record.cacheKey).then(res => {
+              if(res.code === 200) {
+                message.success("清理缓存键名[" + record.cacheKey + "]成功");
+                getCacheKeys(currentCacheName);
+              }
+            });
+          }}
+        >
+          <FormattedMessage id="pages.searchTable.delete" defaultMessage="删除" />
+        </Button>,
+      ]
+    }
+  ];
+
+  return (
+    <div>
+      <Row gutter={[24, 24]}>
+        <Col span={8}>
+          <Card title="缓存列表" extra={<Button icon={<ReloadOutlined />} onClick={()=>{ refreshCacheNames()}} type="link" />} className={styles.card}>
+            <Table
+              rowKey="cacheName"
+              dataSource={cacheNames}
+              columns={columns}
+              onRow={(record: API.Monitor.CacheContent) => {
+                return {
+                  onClick: () => {
+                    setCurrentCacheName(record.cacheName);
+                    getCacheKeys(record.cacheName);
+                  },
+                };
+              }}
+            />
+          </Card>
+        </Col>
+        <Col span={8}>
+          <Card title="键名列表" extra={<Button icon={<ReloadOutlined />} onClick={()=>{ refreshCacheKeys()}} type="link" />} className={styles.card}>
+            <Table
+              rowKey="index"
+              dataSource={cacheKeys}
+              columns={cacheKeysColumns}
+              onRow={(record: any) => {
+                return {
+                  onClick: () => {
+                    getCacheValue(currentCacheName, record.cacheKey).then(res => {
+                      if (res.code === 200) {
+                        form.resetFields();
+                        form.setFieldsValue({
+                          cacheName: res.data.cacheName,
+                          cacheKey: res.data.cacheKey,
+                          cacheValue: res.data.cacheValue,
+                          remark: res.data.remark,
+                        });
+                      }
+                    });
+                  },
+                };
+              }}
+            />
+          </Card>
+        </Col>
+        <Col span={8}>
+          <Card title="缓存内容" extra={<Button icon={<ReloadOutlined />} onClick={()=>{ onClearAll()}} type="link" >清理全部</Button>} className={styles.card}>
+            <ProForm
+              name="basic"
+              form={form}
+              labelCol={{ span: 8 }}
+              wrapperCol={{ span: 16 }}
+              onFinish={onClearAll}
+              onFinishFailed={onClearAllFailed}
+              autoComplete="off"
+            >
+              <Form.Item
+                label="缓存名称"
+                name="cacheName"
+              >
+                <Input />
+              </Form.Item>
+              <Form.Item
+                label="缓存键名"
+                name="cacheKey"
+              >
+                <Input />
+              </Form.Item>
+              <Form.Item
+                label="缓存内容"
+                name="cacheValue"
+              >
+                <TextArea autoSize={{ minRows: 2 }} />
+              </Form.Item>
+            </ProForm>
+          </Card>
+        </Col>
+      </Row>
+    </div>
+  );
+};
+
+export default CacheList;
diff --git a/react-ui/src/pages/Monitor/Cache/List/index.less b/react-ui/src/pages/Monitor/Cache/List/index.less
new file mode 100644
index 0000000..0c5aace
--- /dev/null
+++ b/react-ui/src/pages/Monitor/Cache/List/index.less
@@ -0,0 +1,10 @@
+
+
+
+/* *
+ *
+ * @author whiteshader@163.com
+ * @datetime  2021/09/16
+ *
+ * */
+
diff --git a/react-ui/src/pages/Monitor/Cache/index.less b/react-ui/src/pages/Monitor/Cache/index.less
new file mode 100644
index 0000000..7112c50
--- /dev/null
+++ b/react-ui/src/pages/Monitor/Cache/index.less
@@ -0,0 +1,33 @@
+
+/* *
+ *
+ * @author whiteshader@163.com
+ * @datetime  2021/09/16
+ * 
+ * */
+
+ 
+.card {
+  margin-bottom: 12px;
+}
+
+
+.miniChart {
+  position: relative;
+  width: 100%;
+  .chartContent {
+    position: absolute;
+    bottom: -28px;
+    width: 100%;
+    > div {
+      margin: 0 -5px;
+      overflow: hidden;
+    }
+  }
+  .chartLoading {
+    position: absolute;
+    top: 16px;
+    left: 50%;
+    margin-left: -7px;
+  }
+}
diff --git a/react-ui/src/pages/Monitor/Cache/index.tsx b/react-ui/src/pages/Monitor/Cache/index.tsx
new file mode 100644
index 0000000..4c247ee
--- /dev/null
+++ b/react-ui/src/pages/Monitor/Cache/index.tsx
@@ -0,0 +1,201 @@
+import React, { useEffect, useState } from 'react';
+import { Card, Col, Row, Table } from 'antd';
+import { DataItem } from '@antv/g2plot/esm/interface/config';
+import { Gauge, Pie } from '@ant-design/plots';
+import styles from './index.less';
+import { getCacheInfo } from '@/services/monitor/cache';
+
+
+/* *
+ *
+ * @author whiteshader@163.com
+ * @datetime  2021/09/16
+ *
+ * */
+
+
+const columns = [
+  {
+    title: 'col1',
+    dataIndex: 'col1',
+    key: 'col1',
+  },
+  {
+    title: 'col2',
+    dataIndex: 'col2',
+    key: 'col2',
+  },
+  {
+    title: 'col3',
+    dataIndex: 'col3',
+    key: 'col3',
+  },
+  {
+    title: 'col4',
+    dataIndex: 'col4',
+    key: 'col4',
+  },
+  {
+    title: 'col5',
+    dataIndex: 'col5',
+    key: 'col5',
+  },
+  {
+    title: 'col6',
+    dataIndex: 'col6',
+    key: 'col6',
+  },
+  {
+    title: 'col7',
+    dataIndex: 'col7',
+    key: 'col7',
+  },
+  {
+    title: 'col8',
+    dataIndex: 'col8',
+    key: 'col8',
+  },
+];
+
+const usageFormatter = (val: string): string => {
+  switch (val) {
+    case '10':
+      return '100%';
+    case '8':
+      return '80%';
+    case '6':
+      return '60%';
+    case '4':
+      return '40%';
+    case '2':
+      return '20%';
+    case '0':
+      return '0%';
+    default:
+      return '';
+  }
+};
+
+const CacheInfo: React.FC = () => {
+  const [baseInfoData, setBaseInfoData] = useState<any>([]);
+  const [memUsage, setMemUsage] = useState<Number>(0);
+  const [memUsageTitle, setMemUsageTitle] = useState<any>([]);
+  const [cmdInfoData, setCmdInfoData] = useState<DataItem[]>([]);
+  useEffect(() => {
+    getCacheInfo().then((res) => {
+      if (res.code === 200) {
+        const baseinfo = [];
+        baseinfo.push({
+          col1: 'Redis版本',
+          col2: res.data.info.redis_version,
+          col3: '运行模式',
+          col4: res.data.info.redis_mode === 'standalone' ? '单机' : '集群',
+          col5: '端口',
+          col6: res.data.info.tcp_port,
+          col7: '客户端数',
+          col8: res.data.info.connected_clients,
+        });
+        baseinfo.push({
+          col1: '运行时间(天)',
+          col2: res.data.info.uptime_in_days,
+          col3: '使用内存',
+          col4: res.data.info.used_memory_human,
+          col5: '使用CPU',
+          col6: `${res.data.info.used_cpu_user_children}%`,
+          col7: '内存配置',
+          col8: res.data.info.maxmemory_human,
+        });
+        baseinfo.push({
+          col1: 'AOF是否开启',
+          col2: res.data.info.aof_enabled === '0' ? '否' : '是',
+          col3: 'RDB是否成功',
+          col4: res.data.info.rdb_last_bgsave_status,
+          col5: 'Key数量',
+          col6: res.data.dbSize,
+          col7: '网络入口/出口',
+          col8: `${res.data.info.instantaneous_input_kbps}/${res.data.info.instantaneous_output_kbps}kps`,
+        });
+        setBaseInfoData(baseinfo);
+
+        const data: VisitDataType[] = res.data.commandStats.map((item) => {
+          return {
+            x: item.name,
+            y: Number(item.value),
+          };
+        });
+
+        setCmdInfoData(data);
+        setMemUsageTitle(res.data.info.used_memory_human);
+        setMemUsage(res.data.info.used_memory / res.data.info.total_system_memory);
+      }
+    });
+  }, []);
+
+  return (
+    <div>
+      <Row gutter={[24, 24]}>
+        <Col span={24}>
+          <Card title="基本信息" className={styles.card}>
+            <Table
+              rowKey="col1"
+              pagination={false}
+              showHeader={false}
+              dataSource={baseInfoData}
+              columns={columns}
+            />
+          </Card>
+        </Col>
+      </Row>
+      <Row gutter={[24, 24]}>
+        <Col span={12}>
+          <Card title="命令统计" className={styles.card}>
+            <Pie
+              height={320}
+              radius={0.8}
+              innerRadius={0.5}
+              angleField="y"
+              colorField="x"
+              data={cmdInfoData as any}
+              legend={false}
+              label={{
+                position: 'spider',
+                text: (item: { x: number; y: number }) => {
+                  return `${item.x}: ${item.y}`;
+                },
+              }}
+            />
+          </Card>
+        </Col>
+        <Col span={12}>
+          <Card title="内存信息" className={styles.card}>
+            <Gauge
+              title={memUsageTitle}
+              height={320}
+              percent={memUsage}
+              formatter={usageFormatter}
+              padding={-16}
+              style={{
+                textContent: () => { return memUsage.toFixed(2).toString() + '%'},
+              }}
+              data={
+                {
+                  target: memUsage,
+                  total: 100,
+                  name: 'score',
+                  thresholds: [20, 40, 60, 80, 100],
+                } as any
+              }
+              meta={{
+                color: {
+                  range: ['#C3F71F', '#B5E61D', '#FFC90E', '#FF7F27', '#FF2518'],
+                },
+              }}
+            />
+          </Card>
+        </Col>
+      </Row>
+    </div>
+  );
+};
+
+export default CacheInfo;