Browse Source

初始化

mentoswzq 4 years ago
parent
commit
527c4af205
100 changed files with 5331 additions and 0 deletions
  1. 16 0
      .editorconfig
  2. 8 0
      .eslintignore
  3. 8 0
      .eslintrc.js
  4. 40 0
      .gitignore
  5. 23 0
      .prettierignore
  6. 5 0
      .prettierrc.js
  7. 5 0
      .stylelintrc.js
  8. 58 0
      config/config.ts
  9. 21 0
      config/defaultSettings.ts
  10. 30 0
      config/proxy.ts
  11. 76 0
      config/routes.ts
  12. 10 0
      jest.config.js
  13. 10 0
      jsconfig.json
  14. 171 0
      mock/listTableList.ts
  15. 107 0
      mock/notices.ts
  16. 5 0
      mock/route.ts
  17. 200 0
      mock/user.ts
  18. 107 0
      package.json
  19. 1 0
      public/CNAME
  20. BIN
      public/favicon.ico
  21. BIN
      public/home_bg.png
  22. BIN
      public/icons/icon-128x128.png
  23. BIN
      public/icons/icon-192x192.png
  24. BIN
      public/icons/icon-512x512.png
  25. 1 0
      public/logo.svg
  26. 5 0
      public/pro_icon.svg
  27. 7 0
      src/access.ts
  28. 146 0
      src/app.tsx
  29. 29 0
      src/components/Footer/index.tsx
  30. 16 0
      src/components/HeaderDropdown/index.less
  31. 19 0
      src/components/HeaderDropdown/index.tsx
  32. 25 0
      src/components/HeaderSearch/index.less
  33. 101 0
      src/components/HeaderSearch/index.tsx
  34. 125 0
      src/components/NoticeIcon/NoticeIcon.tsx
  35. 103 0
      src/components/NoticeIcon/NoticeList.less
  36. 111 0
      src/components/NoticeIcon/NoticeList.tsx
  37. 35 0
      src/components/NoticeIcon/index.less
  38. 158 0
      src/components/NoticeIcon/index.tsx
  39. 107 0
      src/components/RightContent/AvatarDropdown.tsx
  40. 62 0
      src/components/RightContent/index.less
  41. 90 0
      src/components/RightContent/index.tsx
  42. 170 0
      src/components/common/DataTable/index.tsx
  43. 270 0
      src/components/index.md
  44. 57 0
      src/e2e/baseLayout.e2e.js
  45. 54 0
      src/global.less
  46. 85 0
      src/global.tsx
  47. 24 0
      src/locales/en-US.ts
  48. 5 0
      src/locales/en-US/component.ts
  49. 17 0
      src/locales/en-US/globalHeader.ts
  50. 52 0
      src/locales/en-US/menu.ts
  51. 68 0
      src/locales/en-US/pages.ts
  52. 6 0
      src/locales/en-US/pwa.ts
  53. 31 0
      src/locales/en-US/settingDrawer.ts
  54. 60 0
      src/locales/en-US/settings.ts
  55. 23 0
      src/locales/id-ID.ts
  56. 5 0
      src/locales/id-ID/component.ts
  57. 17 0
      src/locales/id-ID/globalHeader.ts
  58. 52 0
      src/locales/id-ID/menu.ts
  59. 7 0
      src/locales/id-ID/pwa.ts
  60. 32 0
      src/locales/id-ID/settingDrawer.ts
  61. 60 0
      src/locales/id-ID/settings.ts
  62. 20 0
      src/locales/pt-BR.ts
  63. 5 0
      src/locales/pt-BR/component.ts
  64. 18 0
      src/locales/pt-BR/globalHeader.ts
  65. 52 0
      src/locales/pt-BR/menu.ts
  66. 7 0
      src/locales/pt-BR/pwa.ts
  67. 32 0
      src/locales/pt-BR/settingDrawer.ts
  68. 60 0
      src/locales/pt-BR/settings.ts
  69. 24 0
      src/locales/zh-CN.ts
  70. 5 0
      src/locales/zh-CN/component.ts
  71. 17 0
      src/locales/zh-CN/globalHeader.ts
  72. 52 0
      src/locales/zh-CN/menu.ts
  73. 65 0
      src/locales/zh-CN/pages.ts
  74. 6 0
      src/locales/zh-CN/pwa.ts
  75. 31 0
      src/locales/zh-CN/settingDrawer.ts
  76. 55 0
      src/locales/zh-CN/settings.ts
  77. 20 0
      src/locales/zh-TW.ts
  78. 5 0
      src/locales/zh-TW/component.ts
  79. 17 0
      src/locales/zh-TW/globalHeader.ts
  80. 52 0
      src/locales/zh-TW/menu.ts
  81. 6 0
      src/locales/zh-TW/pwa.ts
  82. 31 0
      src/locales/zh-TW/settingDrawer.ts
  83. 55 0
      src/locales/zh-TW/settings.ts
  84. 22 0
      src/manifest.json
  85. 18 0
      src/pages/404.tsx
  86. 43 0
      src/pages/Admin.tsx
  87. 30 0
      src/pages/ListTableList/components/CreateForm.tsx
  88. 209 0
      src/pages/ListTableList/components/UpdateForm.tsx
  89. 36 0
      src/pages/ListTableList/data.d.ts
  90. 323 0
      src/pages/ListTableList/index.tsx
  91. 38 0
      src/pages/ListTableList/service.ts
  92. 0 0
      src/pages/Order/Billing/index.module.less
  93. 65 0
      src/pages/Order/Billing/index.tsx
  94. 8 0
      src/pages/Welcome.less
  95. 63 0
      src/pages/Welcome.tsx
  96. 213 0
      src/pages/document.ejs
  97. 114 0
      src/pages/user/login/index.less
  98. 308 0
      src/pages/user/login/index.tsx
  99. 70 0
      src/service-worker.js
  100. 0 0
      src/services/API.d.ts

+ 16 - 0
.editorconfig

@@ -0,0 +1,16 @@
+# http://editorconfig.org
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false
+
+[Makefile]
+indent_style = tab

+ 8 - 0
.eslintignore

@@ -0,0 +1,8 @@
+/lambda/
+/scripts
+/config
+.history
+public
+dist
+.umi
+mock

+ 8 - 0
.eslintrc.js

@@ -0,0 +1,8 @@
+module.exports = {
+  extends: [require.resolve('@umijs/fabric/dist/eslint')],
+  globals: {
+    ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true,
+    page: true,
+    REACT_APP_ENV: true,
+  },
+};

+ 40 - 0
.gitignore

@@ -0,0 +1,40 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+**/node_modules
+# roadhog-api-doc ignore
+/src/utils/request-temp.js
+_roadhog-api-doc
+
+# production
+/dist
+/.vscode
+
+# misc
+.DS_Store
+npm-debug.log*
+yarn-error.log
+
+/coverage
+.idea
+yarn.lock
+package-lock.json
+*bak
+.vscode
+
+# visual studio code
+.history
+*.log
+functions/*
+.temp/**
+
+# umi
+.umi
+.umi-production
+
+# screenshot
+screenshot
+.firebase
+.eslintcache
+
+build

+ 23 - 0
.prettierignore

@@ -0,0 +1,23 @@
+**/*.svg
+package.json
+.umi
+.umi-production
+/dist
+.dockerignore
+.DS_Store
+.eslintignore
+*.png
+*.toml
+docker
+.editorconfig
+Dockerfile*
+.gitignore
+.prettierignore
+LICENSE
+.eslintcache
+*.lock
+yarn-error.log
+.history
+CNAME
+/build
+/public

+ 5 - 0
.prettierrc.js

@@ -0,0 +1,5 @@
+const fabric = require('@umijs/fabric');
+
+module.exports = {
+  ...fabric.prettier,
+};

+ 5 - 0
.stylelintrc.js

@@ -0,0 +1,5 @@
+const fabric = require('@umijs/fabric');
+
+module.exports = {
+  ...fabric.stylelint,
+};

+ 58 - 0
config/config.ts

@@ -0,0 +1,58 @@
+// https://umijs.org/config/
+import { defineConfig } from 'umi';
+import defaultSettings from './defaultSettings';
+import proxy from './proxy';
+import routes from './routes';
+
+const { REACT_APP_ENV } = process.env;
+
+export default defineConfig({
+  hash: true,
+  antd: {},
+  dva: {
+    hmr: true,
+  },
+  layout: {
+    name: 'Ant Design Pro',
+    locale: true,
+    siderWidth: 208,
+    ...defaultSettings,
+  },
+  locale: {
+    // default zh-CN
+    default: 'zh-CN',
+    antd: true,
+    // default true, when it is true, will use `navigator.language` overwrite default
+    baseNavigator: true,
+  },
+  dynamicImport: {
+    loading: '@ant-design/pro-layout/es/PageLoading',
+  },
+  targets: {
+    ie: 11,
+  },
+  // umi routes: https://umijs.org/docs/routing
+  routes,
+  // Theme for antd: https://ant.design/docs/react/customize-theme-cn
+  theme: {
+    'primary-color': defaultSettings.primaryColor,
+  },
+  esbuild: {},
+  title: false,
+  ignoreMomentLocale: true,
+  proxy: proxy[REACT_APP_ENV || 'dev'],
+  manifest: {
+    basePath: '/',
+  },
+  // https://github.com/zthxxx/react-dev-inspector
+  plugins: ['react-dev-inspector/plugins/umi/react-inspector'],
+  inspectorConfig: {
+    // loader options type and docs see below
+    exclude: [],
+    babelPlugins: [],
+    babelOptions: {},
+  },
+  resolve: {
+    includes: ['src/components'],
+  },
+});

+ 21 - 0
config/defaultSettings.ts

@@ -0,0 +1,21 @@
+import { Settings as LayoutSettings } from '@ant-design/pro-layout';
+
+const Settings: LayoutSettings & {
+  pwa?: boolean;
+  logo?: string;
+} = {
+  navTheme: 'light',
+  // 拂晓蓝
+  primaryColor: '#1890ff',
+  layout: 'mix',
+  contentWidth: 'Fluid',
+  fixedHeader: false,
+  fixSiderbar: true,
+  colorWeak: false,
+  title: 'Ant Design Pro',
+  pwa: false,
+  logo: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg',
+  iconfontUrl: '',
+};
+
+export default Settings;

+ 30 - 0
config/proxy.ts

@@ -0,0 +1,30 @@
+/**
+ * 在生产环境 代理是无法生效的,所以这里没有生产环境的配置
+ * The agent cannot take effect in the production environment
+ * so there is no configuration of the production environment
+ * For details, please see
+ * https://pro.ant.design/docs/deploy
+ */
+export default {
+  dev: {
+    '/api/': {
+      target: 'http://uat.jishutao.com',
+      changeOrigin: true,
+      pathRewrite: { '/api/login': '' },
+    },
+  },
+  test: {
+    '/api/': {
+      target: 'https://preview.pro.ant.design',
+      changeOrigin: true,
+      pathRewrite: { '/api/login': '' },
+    },
+  },
+  pre: {
+    '/api/': {
+      target: 'your pre url',
+      changeOrigin: true,
+      pathRewrite: { '/api/login': '' },
+    },
+  },
+};

+ 76 - 0
config/routes.ts

@@ -0,0 +1,76 @@
+export default [
+  {
+    path: '/user',
+    layout: false,
+    routes: [
+      {
+        path: '/user',
+        routes: [
+          {
+            name: 'login',
+            path: '/user/login',
+            component: './user/login',
+          },
+        ],
+      },
+    ],
+  },
+  {
+    path: '/welcome',
+    name: 'welcome',
+    icon: 'smile',
+    component: './Welcome',
+  },
+  {
+    path: '/admin',
+    name: 'admin',
+    icon: 'crown',
+    access: 'canAdmin',
+    component: './Admin',
+    routes: [
+      {
+        path: '/admin/sub-page',
+        name: 'sub-page',
+        icon: 'smile',
+        component: './Welcome',
+      },
+    ],
+  },
+  // {
+  //   name: 'list.table-list',
+  //   icon: 'table',
+  //   path: '/',
+  //   routes: [
+  //     {
+  //       name: 'list.table-list',
+  //       path: '/',
+  //       routes: [
+  //         {
+  //           name: 'list.table-list',
+  //           path: '/order',
+  //           component: './Order/Billing',
+  //         }
+  //       ]
+  //     }
+  //   ],
+  // },
+  {
+    name: 'list.table-list',
+    icon: 'table',
+    path: '/order',
+    component: './Order/Billing',
+  },
+  // {
+  //   name: 'list.table-list',
+  //   icon: 'table',
+  //   path: '/list',
+  //   component: './ListTableList',
+  // },
+  {
+    path: '/',
+    redirect: '/welcome',
+  },
+  {
+    component: './404',
+  },
+];

+ 10 - 0
jest.config.js

@@ -0,0 +1,10 @@
+module.exports = {
+  testURL: 'http://localhost:8000',
+  testEnvironment: './tests/PuppeteerEnvironment',
+  verbose: false,
+  extraSetupFiles: ['./tests/setupTests.js'],
+  globals: {
+    ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: false,
+    localStorage: null,
+  },
+};

+ 10 - 0
jsconfig.json

@@ -0,0 +1,10 @@
+{
+  "compilerOptions": {
+    "emitDecoratorMetadata": true,
+    "experimentalDecorators": true,
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["./src/*"]
+    }
+  }
+}

+ 171 - 0
mock/listTableList.ts

@@ -0,0 +1,171 @@
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { Request, Response } from 'express';
+import { parse } from 'url';
+import { TableListItem, TableListParams } from '@/pages/ListTableList/data';
+
+// mock tableListDataSource
+const genList = (current: number, pageSize: number) => {
+  const tableListDataSource: TableListItem[] = [];
+
+  for (let i = 0; i < pageSize; i += 1) {
+    const index = (current - 1) * 10 + i;
+    tableListDataSource.push({
+      key: index,
+      disabled: i % 6 === 0,
+      href: 'https://ant.design',
+      avatar: [
+        'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
+        'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
+      ][i % 2],
+      name: `TradeCode ${index}`,
+      owner: '曲丽丽',
+      desc: '这是一段描述',
+      callNo: Math.floor(Math.random() * 1000),
+      status: Math.floor(Math.random() * 10) % 4,
+      updatedAt: new Date(),
+      createdAt: new Date(),
+      progress: Math.ceil(Math.random() * 100),
+    });
+  }
+  tableListDataSource.reverse();
+  return tableListDataSource;
+};
+
+let tableListDataSource = genList(1, 100);
+
+function getRule(req: Request, res: Response, u: string) {
+  let realUrl = u;
+  if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') {
+    realUrl = req.url;
+  }
+  const { current = 1, pageSize = 10 } = req.query;
+  const params = (parse(realUrl, true).query as unknown) as TableListParams;
+
+  let dataSource = [...tableListDataSource].slice(
+    ((current as number) - 1) * (pageSize as number),
+    (current as number) * (pageSize as number),
+  );
+  const sorter = JSON.parse(params.sorter as any);
+  if (sorter) {
+    dataSource = dataSource.sort((prev, next) => {
+      let sortNumber = 0;
+      Object.keys(sorter).forEach((key) => {
+        if (sorter[key] === 'descend') {
+          if (prev[key] - next[key] > 0) {
+            sortNumber += -1;
+          } else {
+            sortNumber += 1;
+          }
+          return;
+        }
+        if (prev[key] - next[key] > 0) {
+          sortNumber += 1;
+        } else {
+          sortNumber += -1;
+        }
+      });
+      return sortNumber;
+    });
+  }
+  if (params.filter) {
+    const filter = JSON.parse(params.filter as any) as {
+      [key: string]: string[];
+    };
+    if (Object.keys(filter).length > 0) {
+      dataSource = dataSource.filter((item) => {
+        return Object.keys(filter).some((key) => {
+          if (!filter[key]) {
+            return true;
+          }
+          if (filter[key].includes(`${item[key]}`)) {
+            return true;
+          }
+          return false;
+        });
+      });
+    }
+  }
+
+  if (params.name) {
+    dataSource = dataSource.filter((data) => data.name.includes(params.name || ''));
+  }
+  const result = {
+    data: dataSource,
+    total: tableListDataSource.length,
+    success: true,
+    pageSize,
+    current: parseInt(`${params.currentPage}`, 10) || 1,
+  };
+
+  return res.json(result);
+}
+
+function postRule(req: Request, res: Response, u: string, b: Request) {
+  let realUrl = u;
+  if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') {
+    realUrl = req.url;
+  }
+
+  const body = (b && b.body) || req.body;
+  const { method, name, desc, key } = body;
+
+  switch (method) {
+    /* eslint no-case-declarations:0 */
+    case 'delete':
+      tableListDataSource = tableListDataSource.filter((item) => key.indexOf(item.key) === -1);
+      break;
+    case 'post':
+      (() => {
+        const i = Math.ceil(Math.random() * 10000);
+        const newRule = {
+          key: tableListDataSource.length,
+          href: 'https://ant.design',
+          avatar: [
+            'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
+            'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
+          ][i % 2],
+          name,
+          owner: '曲丽丽',
+          desc,
+          callNo: Math.floor(Math.random() * 1000),
+          status: Math.floor(Math.random() * 10) % 2,
+          updatedAt: new Date(),
+          createdAt: new Date(),
+          progress: Math.ceil(Math.random() * 100),
+        };
+        tableListDataSource.unshift(newRule);
+        return res.json(newRule);
+      })();
+      return;
+
+    case 'update':
+      (() => {
+        let newRule = {};
+        tableListDataSource = tableListDataSource.map((item) => {
+          if (item.key === key) {
+            newRule = { ...item, desc, name };
+            return { ...item, desc, name };
+          }
+          return item;
+        });
+        return res.json(newRule);
+      })();
+      return;
+    default:
+      break;
+  }
+
+  const result = {
+    list: tableListDataSource,
+    pagination: {
+      total: tableListDataSource.length,
+    },
+  };
+
+  res.json(result);
+}
+
+export default {
+  'GET /api/rule': getRule,
+  'POST /api/rule': postRule,
+};

+ 107 - 0
mock/notices.ts

@@ -0,0 +1,107 @@
+import { Request, Response } from 'express';
+
+const getNotices = (req: Request, res: Response) => {
+  res.json({
+    data: [
+      {
+        id: '000000001',
+        avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
+        title: '你收到了 14 份新周报',
+        datetime: '2017-08-09',
+        type: 'notification',
+      },
+      {
+        id: '000000002',
+        avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
+        title: '你推荐的 曲妮妮 已通过第三轮面试',
+        datetime: '2017-08-08',
+        type: 'notification',
+      },
+      {
+        id: '000000003',
+        avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png',
+        title: '这种模板可以区分多种通知类型',
+        datetime: '2017-08-07',
+        read: true,
+        type: 'notification',
+      },
+      {
+        id: '000000004',
+        avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
+        title: '左侧图标用于区分不同的类型',
+        datetime: '2017-08-07',
+        type: 'notification',
+      },
+      {
+        id: '000000005',
+        avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
+        title: '内容不要超过两行字,超出时自动截断',
+        datetime: '2017-08-07',
+        type: 'notification',
+      },
+      {
+        id: '000000006',
+        avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
+        title: '曲丽丽 评论了你',
+        description: '描述信息描述信息描述信息',
+        datetime: '2017-08-07',
+        type: 'message',
+        clickClose: true,
+      },
+      {
+        id: '000000007',
+        avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
+        title: '朱偏右 回复了你',
+        description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
+        datetime: '2017-08-07',
+        type: 'message',
+        clickClose: true,
+      },
+      {
+        id: '000000008',
+        avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
+        title: '标题',
+        description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
+        datetime: '2017-08-07',
+        type: 'message',
+        clickClose: true,
+      },
+      {
+        id: '000000009',
+        title: '任务名称',
+        description: '任务需要在 2017-01-12 20:00 前启动',
+        extra: '未开始',
+        status: 'todo',
+        type: 'event',
+      },
+      {
+        id: '000000010',
+        title: '第三方紧急代码变更',
+        description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
+        extra: '马上到期',
+        status: 'urgent',
+        type: 'event',
+      },
+      {
+        id: '000000011',
+        title: '信息安全考试',
+        description: '指派竹尔于 2017-01-09 前完成更新并发布',
+        extra: '已耗时 8 天',
+        status: 'doing',
+        type: 'event',
+      },
+      {
+        id: '000000012',
+        title: 'ABCD 版本发布',
+        description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
+        extra: '进行中',
+        status: 'processing',
+        type: 'event',
+      },
+    ],
+  });
+};
+
+export default {
+  'GET /api/notices': getNotices,
+};

+ 5 - 0
mock/route.ts

@@ -0,0 +1,5 @@
+export default {
+  '/api/auth_routes': {
+    '/form/advanced-form': { authority: ['admin', 'user'] },
+  },
+};

+ 200 - 0
mock/user.ts

@@ -0,0 +1,200 @@
+import { Request, Response } from 'express';
+
+const waitTime = (time: number = 100) => {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve(true);
+    }, time);
+  });
+};
+
+async function getFakeCaptcha(req: Request, res: Response) {
+  await waitTime(2000);
+  return res.json('captcha-xxx');
+}
+
+const { ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION } = process.env;
+
+/**
+ * 当前用户的权限,如果为空代表没登录
+ * current user access, if is '', user need login
+ * 如果是 pro 的预览,默认是有权限的
+ */
+let access = ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site' ? 'admin' : '';
+
+const getAccess = () => {
+  return access;
+};
+
+// 代码中会兼容本地 service mock 以及部署站点的静态数据
+export default {
+  // 支持值为 Object 和 Array
+  'GET /api/currentUser': (req: Request, res: Response) => {
+    if (!getAccess()) {
+      res.status(401).send({
+        data: {
+          isLogin: false,
+        },
+        errorCode: '401',
+        errorMessage: '请先登录!',
+        success: true,
+      });
+      return;
+    }
+    res.send({
+      name: 'Serati Ma',
+      avatar: 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',
+      userid: '00000001',
+      email: 'antdesign@alipay.com',
+      signature: '海纳百川,有容乃大',
+      title: '交互专家',
+      group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED',
+      tags: [
+        {
+          key: '0',
+          label: '很有想法的',
+        },
+        {
+          key: '1',
+          label: '专注设计',
+        },
+        {
+          key: '2',
+          label: '辣~',
+        },
+        {
+          key: '3',
+          label: '大长腿',
+        },
+        {
+          key: '4',
+          label: '川妹子',
+        },
+        {
+          key: '5',
+          label: '海纳百川',
+        },
+      ],
+      notifyCount: 12,
+      unreadCount: 11,
+      country: 'China',
+      access: getAccess(),
+      geographic: {
+        province: {
+          label: '浙江省',
+          key: '330000',
+        },
+        city: {
+          label: '杭州市',
+          key: '330100',
+        },
+      },
+      address: '西湖区工专路 77 号',
+      phone: '0752-268888888',
+    });
+  },
+  // GET POST 可省略
+  'GET /api/users': [
+    {
+      key: '1',
+      name: 'John Brown',
+      age: 32,
+      address: 'New York No. 1 Lake Park',
+    },
+    {
+      key: '2',
+      name: 'Jim Green',
+      age: 42,
+      address: 'London No. 1 Lake Park',
+    },
+    {
+      key: '3',
+      name: 'Joe Black',
+      age: 32,
+      address: 'Sidney No. 1 Lake Park',
+    },
+  ],
+  'POST /api/login/account': async (req: Request, res: Response) => {
+    const { password, username, type } = req.body;
+    await waitTime(2000);
+    if (password === 'ant.design' && username === 'admin') {
+      res.send({
+        status: 'ok',
+        type,
+        currentAuthority: 'admin',
+      });
+      access = 'admin';
+      return;
+    }
+    if (password === 'ant.design' && username === 'user') {
+      res.send({
+        status: 'ok',
+        type,
+        currentAuthority: 'user',
+      });
+      access = 'user';
+      return;
+    }
+    if (type === 'mobile') {
+      res.send({
+        status: 'ok',
+        type,
+        currentAuthority: 'admin',
+      });
+      access = 'admin';
+      return;
+    }
+
+    res.send({
+      status: 'error',
+      type,
+      currentAuthority: 'guest',
+    });
+    access = 'guest';
+  },
+  'GET /api/login/outLogin': (req: Request, res: Response) => {
+    access = '';
+    res.send({ data: {}, success: true });
+  },
+  'POST /api/register': (req: Request, res: Response) => {
+    res.send({ status: 'ok', currentAuthority: 'user', success: true });
+  },
+  'GET /api/500': (req: Request, res: Response) => {
+    res.status(500).send({
+      timestamp: 1513932555104,
+      status: 500,
+      error: 'error',
+      message: 'error',
+      path: '/base/category/list',
+    });
+  },
+  'GET /api/404': (req: Request, res: Response) => {
+    res.status(404).send({
+      timestamp: 1513932643431,
+      status: 404,
+      error: 'Not Found',
+      message: 'No message available',
+      path: '/base/category/list/2121212',
+    });
+  },
+  'GET /api/403': (req: Request, res: Response) => {
+    res.status(403).send({
+      timestamp: 1513932555104,
+      status: 403,
+      error: 'Unauthorized',
+      message: 'Unauthorized',
+      path: '/base/category/list',
+    });
+  },
+  'GET /api/401': (req: Request, res: Response) => {
+    res.status(401).send({
+      timestamp: 1513932555104,
+      status: 401,
+      error: 'Unauthorized',
+      message: 'Unauthorized',
+      path: '/base/category/list',
+    });
+  },
+
+  'GET  /api/login/captcha': getFakeCaptcha,
+};

+ 107 - 0
package.json

@@ -0,0 +1,107 @@
+{
+  "name": "ant-design-pro",
+  "version": "5.0.0-beta.2",
+  "private": true,
+  "description": "An out-of-box UI solution for enterprise applications",
+  "scripts": {
+    "analyze": "cross-env ANALYZE=1 umi build",
+    "build": "umi build",
+    "deploy": "npm run site && npm run gh-pages",
+    "dev": "npm run start:dev",
+    "gh-pages": "gh-pages -d dist",
+    "i18n-remove": "pro i18n-remove --locale=zh-CN --write",
+    "postinstall": "umi g tmp",
+    "lint": "umi g tmp && npm run lint:js && npm run lint:style && npm run lint:prettier",
+    "lint-staged": "lint-staged",
+    "lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx ",
+    "lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src && npm run lint:style",
+    "lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src",
+    "lint:prettier": "prettier --check \"src/**/*\" --end-of-line auto",
+    "lint:style": "stylelint --fix \"src/**/*.less\" --syntax less",
+    "precommit": "lint-staged",
+    "prettier": "prettier -c --write \"src/**/*\"",
+    "start": "umi dev",
+    "start:dev": "cross-env REACT_APP_ENV=dev MOCK=none umi dev",
+    "start:no-mock": "cross-env MOCK=none umi dev",
+    "start:no-ui": "cross-env UMI_UI=none umi dev",
+    "start:pre": "cross-env REACT_APP_ENV=pre umi dev",
+    "start:test": "cross-env REACT_APP_ENV=test MOCK=none umi dev",
+    "pretest": "node ./tests/beforeTest",
+    "test": "umi test",
+    "test:all": "node ./tests/run-tests.js",
+    "test:component": "umi test ./src/components",
+    "tsc": "tsc --noEmit"
+  },
+  "lint-staged": {
+    "**/*.less": "stylelint --syntax less",
+    "**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js",
+    "**/*.{js,jsx,tsx,ts,less,md,json}": [
+      "prettier --write"
+    ]
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not ie <= 10"
+  ],
+  "dependencies": {
+    "@ant-design/icons": "^4.0.0",
+    "@ant-design/pro-descriptions": "^1.0.19",
+    "@ant-design/pro-form": "^1.3.0",
+    "@ant-design/pro-layout": "^6.5.0",
+    "@ant-design/pro-table": "^2.9.16",
+    "@umijs/route-utils": "^1.0.33",
+    "antd": "^4.8.6",
+    "classnames": "^2.2.6",
+    "js-cookie": "^2.2.1",
+    "lodash": "^4.17.11",
+    "moment": "^2.25.3",
+    "omit.js": "^2.0.2",
+    "qs": "^6.9.0",
+    "react": "^17.0.0",
+    "react-dev-inspector": "^1.1.1",
+    "react-dom": "^17.0.0",
+    "react-helmet-async": "^1.0.4",
+    "umi": "^3.2.14",
+    "umi-request": "^1.0.8",
+    "use-merge-value": "^1.0.1"
+  },
+  "devDependencies": {
+    "@ant-design/pro-cli": "^2.0.2",
+    "@types/classnames": "^2.2.7",
+    "@types/express": "^4.17.0",
+    "@types/history": "^4.7.2",
+    "@types/jest": "^26.0.0",
+    "@types/lodash": "^4.14.144",
+    "@types/qs": "^6.5.3",
+    "@types/react": "^17.0.0",
+    "@types/react-dom": "^17.0.0",
+    "@types/react-helmet": "^6.1.0",
+    "@umijs/fabric": "^2.3.0",
+    "@umijs/plugin-blocks": "^2.0.5",
+    "@umijs/plugin-esbuild": "^1.0.1",
+    "@umijs/preset-ant-design-pro": "^1.2.0",
+    "@umijs/preset-dumi": "^1.1.0-rc.6",
+    "@umijs/preset-react": "^1.7.4",
+    "@umijs/yorkie": "^2.0.3",
+    "carlo": "^0.9.46",
+    "cross-env": "^7.0.0",
+    "cross-port-killer": "^1.1.1",
+    "detect-installer": "^1.0.1",
+    "enzyme": "^3.11.0",
+    "eslint": "^7.1.0",
+    "express": "^4.17.1",
+    "gh-pages": "^3.0.0",
+    "jsdom-global": "^3.0.2",
+    "lint-staged": "^10.0.0",
+    "mockjs": "^1.0.1-beta3",
+    "prettier": "^2.0.1",
+    "pro-download": "1.0.1",
+    "puppeteer-core": "^5.0.0",
+    "stylelint": "^13.0.0",
+    "typescript": "^4.0.3"
+  },
+  "engines": {
+    "node": ">=10.0.0"
+  }
+}

+ 1 - 0
public/CNAME

@@ -0,0 +1 @@
+preview.pro.ant.design

BIN
public/favicon.ico


BIN
public/home_bg.png


BIN
public/icons/icon-128x128.png


BIN
public/icons/icon-192x192.png


BIN
public/icons/icon-512x512.png


File diff suppressed because it is too large
+ 1 - 0
public/logo.svg


File diff suppressed because it is too large
+ 5 - 0
public/pro_icon.svg


+ 7 - 0
src/access.ts

@@ -0,0 +1,7 @@
+// src/access.ts
+export default function access(initialState: { currentUser?: API.CurrentUser | undefined }) {
+  const { currentUser } = initialState || {};
+  return {
+    canAdmin: currentUser && currentUser.access === 'admin',
+  };
+}

+ 146 - 0
src/app.tsx

@@ -0,0 +1,146 @@
+import React from 'react';
+import { Settings as LayoutSettings, PageLoading } from '@ant-design/pro-layout';
+import {message, notification} from 'antd';
+import qs from 'qs';
+import { history, RequestConfig, RunTimeLayoutConfig } from 'umi';
+import RightContent from '@/components/RightContent';
+import Footer from '@/components/Footer';
+import { ResponseError } from 'umi-request';
+import { queryCurrent } from './services/user';
+import defaultSettings from '../config/defaultSettings';
+import cookies from 'js-cookie';
+
+/**
+ * 获取用户信息比较慢的时候会展示一个 loading
+ */
+export const initialStateConfig = {
+  loading: <PageLoading />,
+};
+
+export async function getInitialState(): Promise<{
+  settings?: LayoutSettings;
+  currentUser?: API.CurrentUser;
+  fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
+}> {
+  const fetchUserInfo = async () => {
+    try {
+      const currentUser = await queryCurrent();
+      currentUser.data.token = currentUser.token;
+      return currentUser.data;
+    } catch (error) {
+      // history.push('/user/login');
+    }
+    return undefined;
+  };
+  // 如果是登录页面,不执行
+  if (history.location.pathname !== '/user/login') {
+    const currentUser = await fetchUserInfo();
+    return {
+      fetchUserInfo,
+      currentUser,
+      settings: defaultSettings,
+    };
+  }
+  return {
+    fetchUserInfo,
+    settings: defaultSettings,
+  };
+}
+
+export const layout: RunTimeLayoutConfig = ({ initialState }) => {
+  return {
+    rightContentRender: () => <RightContent />,
+    disableContentMargin: false,
+    footerRender: () => <Footer />,
+    onPageChange: () => {
+      const { location } = history;
+      // 如果没有登录,重定向到 login
+      if (!initialState?.currentUser && location.pathname !== '/user/login') {
+        // history.push('/user/login');
+      }
+    },
+    menuHeaderRender: undefined,
+    // 自定义 403 页面
+    // unAccessible: <div>unAccessible</div>,
+    ...initialState?.settings,
+  };
+};
+
+const codeMessage = {
+  200: '服务器成功返回请求的数据。',
+  201: '新建或修改数据成功。',
+  202: '一个请求已经进入后台排队(异步任务)。',
+  204: '删除数据成功。',
+  400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
+  401: '用户没有权限(令牌、用户名、密码错误)。',
+  403: '用户得到授权,但是访问是被禁止的。',
+  404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
+  405: '请求方法不被允许。',
+  406: '请求的格式不可得。',
+  410: '请求的资源被永久删除,且不会再得到的。',
+  422: '当创建一个对象时,发生一个验证错误。',
+  500: '服务器发生错误,请检查服务器。',
+  502: '网关错误。',
+  503: '服务不可用,服务器暂时过载或维护。',
+  504: '网关超时。',
+};
+
+/**
+ * 异常处理程序
+ */
+const errorHandler = (error: ResponseError) => {
+  // const { response } = error;
+  // if (response && response.status) {
+  //   const errorText = codeMessage[response.status] || response.statusText;
+  //   const { status, url } = response;
+  //
+  //   notification.error({
+  //     message: `请求错误 ${status}: ${url}`,
+  //     description: errorText,
+  //   });
+  // }
+  //
+  // if (!response) {
+  //   notification.error({
+  //     description: '您的网络发生异常,无法连接服务器',
+  //     message: '网络异常',
+  //   });
+  // }
+  // throw error;
+};
+
+export const request: RequestConfig = {
+  errorHandler,
+  requestInterceptors: [
+    (url, config) => {
+      if (config.method === 'get') {
+        url = url + '?' + qs.stringify({
+          token: cookies.get('token'),
+        });
+      } else {
+        config.data = qs.stringify({
+          ...(config.data || {}),
+          token: cookies.get('token'),
+        });
+      }
+      config.headers = {
+        'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8;'
+      }
+      return {
+        url: `${url}`,
+        options: { ...config, interceptors: true },
+      };
+    },
+  ],
+  responseInterceptors: [
+    async (config) => {
+      const response = await config.clone().json();
+      if (response.error && response.error.length > 0 && response.error[0].field === '403') {
+        message.warning('登录超时请重新登录');
+        history.push('/user/login');
+      }
+      cookies.set('token',response.token);
+      return config;
+    }
+  ]
+};

+ 29 - 0
src/components/Footer/index.tsx

@@ -0,0 +1,29 @@
+import React from 'react';
+import { GithubOutlined } from '@ant-design/icons';
+import { DefaultFooter } from '@ant-design/pro-layout';
+
+export default () => (
+  <DefaultFooter
+    copyright="2020 蚂蚁集团体验技术部出品"
+    links={[
+      {
+        key: 'Ant Design Pro',
+        title: 'Ant Design Pro',
+        href: 'https://pro.ant.design',
+        blankTarget: true,
+      },
+      {
+        key: 'github',
+        title: <GithubOutlined />,
+        href: 'https://github.com/ant-design/ant-design-pro',
+        blankTarget: true,
+      },
+      {
+        key: 'Ant Design',
+        title: 'Ant Design',
+        href: 'https://ant.design',
+        blankTarget: true,
+      },
+    ]}
+  />
+);

+ 16 - 0
src/components/HeaderDropdown/index.less

@@ -0,0 +1,16 @@
+@import '~antd/es/style/themes/default.less';
+
+.container > * {
+  background-color: @popover-bg;
+  border-radius: 4px;
+  box-shadow: @shadow-1-down;
+}
+
+@media screen and (max-width: @screen-xs) {
+  .container {
+    width: 100% !important;
+  }
+  .container > * {
+    border-radius: 0 !important;
+  }
+}

+ 19 - 0
src/components/HeaderDropdown/index.tsx

@@ -0,0 +1,19 @@
+import { DropDownProps } from 'antd/es/dropdown';
+import { Dropdown } from 'antd';
+import React from 'react';
+import classNames from 'classnames';
+import styles from './index.less';
+
+declare type OverlayFunc = () => React.ReactNode;
+
+export interface HeaderDropdownProps extends Omit<DropDownProps, 'overlay'> {
+  overlayClassName?: string;
+  overlay: React.ReactNode | OverlayFunc | any;
+  placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter';
+}
+
+const HeaderDropdown: React.FC<HeaderDropdownProps> = ({ overlayClassName: cls, ...restProps }) => (
+  <Dropdown overlayClassName={classNames(styles.container, cls)} {...restProps} />
+);
+
+export default HeaderDropdown;

+ 25 - 0
src/components/HeaderSearch/index.less

@@ -0,0 +1,25 @@
+@import '~antd/es/style/themes/default.less';
+
+.headerSearch {
+  display: inline-flex;
+  align-items: center;
+  .input {
+    width: 0;
+    min-width: 0;
+    overflow: hidden;
+    background: transparent;
+    border-radius: 0;
+    transition: width 0.3s, margin-left 0.3s;
+    :global(.ant-select-selection) {
+      background: transparent;
+    }
+    input {
+      box-shadow: none !important;
+    }
+
+    &.show {
+      width: 210px;
+      margin-left: 8px;
+    }
+  }
+}

+ 101 - 0
src/components/HeaderSearch/index.tsx

@@ -0,0 +1,101 @@
+import { SearchOutlined } from '@ant-design/icons';
+import { AutoComplete, Input } from 'antd';
+import useMergeValue from 'use-merge-value';
+import { AutoCompleteProps } from 'antd/es/auto-complete';
+import React, { useRef } from 'react';
+
+import classNames from 'classnames';
+import styles from './index.less';
+
+export interface HeaderSearchProps {
+  onSearch?: (value?: string) => void;
+  onChange?: (value?: string) => void;
+  onVisibleChange?: (b: boolean) => void;
+  className?: string;
+  placeholder?: string;
+  options: AutoCompleteProps['options'];
+  defaultVisible?: boolean;
+  visible?: boolean;
+  defaultValue?: string;
+  value?: string;
+}
+
+const HeaderSearch: React.FC<HeaderSearchProps> = (props) => {
+  const {
+    className,
+    defaultValue,
+    onVisibleChange,
+    placeholder,
+    visible,
+    defaultVisible,
+    ...restProps
+  } = props;
+
+  const inputRef = useRef<Input | null>(null);
+
+  const [value, setValue] = useMergeValue<string | undefined>(defaultValue, {
+    value: props.value,
+    onChange: props.onChange,
+  });
+
+  const [searchMode, setSearchMode] = useMergeValue(defaultVisible || false, {
+    value: props.visible,
+    onChange: onVisibleChange,
+  });
+
+  const inputClass = classNames(styles.input, {
+    [styles.show]: searchMode,
+  });
+  return (
+    <div
+      className={classNames(className, styles.headerSearch)}
+      onClick={() => {
+        setSearchMode(true);
+        if (searchMode && inputRef.current) {
+          inputRef.current.focus();
+        }
+      }}
+      onTransitionEnd={({ propertyName }) => {
+        if (propertyName === 'width' && !searchMode) {
+          if (onVisibleChange) {
+            onVisibleChange(searchMode);
+          }
+        }
+      }}
+    >
+      <SearchOutlined
+        key="Icon"
+        style={{
+          cursor: 'pointer',
+        }}
+      />
+      <AutoComplete
+        key="AutoComplete"
+        className={inputClass}
+        value={value}
+        options={restProps.options}
+        onChange={setValue}
+      >
+        <Input
+          size="small"
+          ref={inputRef}
+          defaultValue={defaultValue}
+          aria-label={placeholder}
+          placeholder={placeholder}
+          onKeyDown={(e) => {
+            if (e.key === 'Enter') {
+              if (restProps.onSearch) {
+                restProps.onSearch(value);
+              }
+            }
+          }}
+          onBlur={() => {
+            setSearchMode(false);
+          }}
+        />
+      </AutoComplete>
+    </div>
+  );
+};
+
+export default HeaderSearch;

+ 125 - 0
src/components/NoticeIcon/NoticeIcon.tsx

@@ -0,0 +1,125 @@
+import { BellOutlined } from '@ant-design/icons';
+import { Badge, Spin, Tabs } from 'antd';
+import useMergeValue from 'use-merge-value';
+import React from 'react';
+import classNames from 'classnames';
+import NoticeList, { NoticeIconTabProps } from './NoticeList';
+import HeaderDropdown from '../HeaderDropdown';
+import styles from './index.less';
+
+const { TabPane } = Tabs;
+
+export interface NoticeIconProps {
+  count?: number;
+  bell?: React.ReactNode;
+  className?: string;
+  loading?: boolean;
+  onClear?: (tabName: string, tabKey: string) => void;
+  onItemClick?: (item: API.NoticeIconData, tabProps: NoticeIconTabProps) => void;
+  onViewMore?: (tabProps: NoticeIconTabProps, e: MouseEvent) => void;
+  onTabChange?: (tabTile: string) => void;
+  style?: React.CSSProperties;
+  onPopupVisibleChange?: (visible: boolean) => void;
+  popupVisible?: boolean;
+  clearText?: string;
+  viewMoreText?: string;
+  clearClose?: boolean;
+  emptyImage?: string;
+  children?: React.ReactElement<NoticeIconTabProps>[];
+}
+
+const NoticeIcon: React.FC<NoticeIconProps> & {
+  Tab: typeof NoticeList;
+} = (props) => {
+  const getNotificationBox = (): React.ReactNode => {
+    const {
+      children,
+      loading,
+      onClear,
+      onTabChange,
+      onItemClick,
+      onViewMore,
+      clearText,
+      viewMoreText,
+    } = props;
+    if (!children) {
+      return null;
+    }
+    const panes: React.ReactNode[] = [];
+    React.Children.forEach(children, (child: React.ReactElement<NoticeIconTabProps>): void => {
+      if (!child) {
+        return;
+      }
+      const { list, title, count, tabKey, showClear, showViewMore } = child.props;
+      const len = list && list.length ? list.length : 0;
+      const msgCount = count || count === 0 ? count : len;
+      const tabTitle: string = msgCount > 0 ? `${title} (${msgCount})` : title;
+      panes.push(
+        <TabPane tab={tabTitle} key={tabKey}>
+          <NoticeList
+            clearText={clearText}
+            viewMoreText={viewMoreText}
+            list={list}
+            tabKey={tabKey}
+            onClear={(): void => onClear && onClear(title, tabKey)}
+            onClick={(item): void => onItemClick && onItemClick(item, child.props)}
+            onViewMore={(event): void => onViewMore && onViewMore(child.props, event)}
+            showClear={showClear}
+            showViewMore={showViewMore}
+            title={title}
+          />
+        </TabPane>,
+      );
+    });
+    return (
+      <>
+        <Spin spinning={loading} delay={300}>
+          <Tabs className={styles.tabs} onChange={onTabChange}>
+            {panes}
+          </Tabs>
+        </Spin>
+      </>
+    );
+  };
+
+  const { className, count, bell } = props;
+
+  const [visible, setVisible] = useMergeValue<boolean>(false, {
+    value: props.popupVisible,
+    onChange: props.onPopupVisibleChange,
+  });
+  const noticeButtonClass = classNames(className, styles.noticeButton);
+  const notificationBox = getNotificationBox();
+  const NoticeBellIcon = bell || <BellOutlined className={styles.icon} />;
+  const trigger = (
+    <span className={classNames(noticeButtonClass, { opened: visible })}>
+      <Badge count={count} style={{ boxShadow: 'none' }} className={styles.badge}>
+        {NoticeBellIcon}
+      </Badge>
+    </span>
+  );
+  if (!notificationBox) {
+    return trigger;
+  }
+
+  return (
+    <HeaderDropdown
+      placement="bottomRight"
+      overlay={notificationBox}
+      overlayClassName={styles.popover}
+      trigger={['click']}
+      visible={visible}
+      onVisibleChange={setVisible}
+    >
+      {trigger}
+    </HeaderDropdown>
+  );
+};
+
+NoticeIcon.defaultProps = {
+  emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg',
+};
+
+NoticeIcon.Tab = NoticeList;
+
+export default NoticeIcon;

+ 103 - 0
src/components/NoticeIcon/NoticeList.less

@@ -0,0 +1,103 @@
+@import '~antd/es/style/themes/default.less';
+
+.list {
+  max-height: 400px;
+  overflow: auto;
+  &::-webkit-scrollbar {
+    display: none;
+  }
+  .item {
+    padding-right: 24px;
+    padding-left: 24px;
+    overflow: hidden;
+    cursor: pointer;
+    transition: all 0.3s;
+
+    .meta {
+      width: 100%;
+    }
+
+    .avatar {
+      margin-top: 4px;
+      background: @component-background;
+    }
+    .iconElement {
+      font-size: 32px;
+    }
+
+    &.read {
+      opacity: 0.4;
+    }
+    &:last-child {
+      border-bottom: 0;
+    }
+    &:hover {
+      background: @primary-1;
+    }
+    .title {
+      margin-bottom: 8px;
+      font-weight: normal;
+    }
+    .description {
+      font-size: 12px;
+      line-height: @line-height-base;
+    }
+    .datetime {
+      margin-top: 4px;
+      font-size: 12px;
+      line-height: @line-height-base;
+    }
+    .extra {
+      float: right;
+      margin-top: -1.5px;
+      margin-right: 0;
+      color: @text-color-secondary;
+      font-weight: normal;
+    }
+  }
+  .loadMore {
+    padding: 8px 0;
+    color: @primary-6;
+    text-align: center;
+    cursor: pointer;
+    &.loadedAll {
+      color: rgba(0, 0, 0, 0.25);
+      cursor: unset;
+    }
+  }
+}
+
+.notFound {
+  padding: 73px 0 88px;
+  color: @text-color-secondary;
+  text-align: center;
+  img {
+    display: inline-block;
+    height: 76px;
+    margin-bottom: 16px;
+  }
+}
+
+.bottomBar {
+  height: 46px;
+  color: @text-color;
+  line-height: 46px;
+  text-align: center;
+  border-top: 1px solid @border-color-split;
+  border-radius: 0 0 @border-radius-base @border-radius-base;
+  transition: all 0.3s;
+  div {
+    display: inline-block;
+    width: 50%;
+    cursor: pointer;
+    transition: all 0.3s;
+    user-select: none;
+
+    &:only-child {
+      width: 100%;
+    }
+    &:not(:only-child):last-child {
+      border-left: 1px solid @border-color-split;
+    }
+  }
+}

+ 111 - 0
src/components/NoticeIcon/NoticeList.tsx

@@ -0,0 +1,111 @@
+import { Avatar, List } from 'antd';
+
+import React from 'react';
+import classNames from 'classnames';
+import styles from './NoticeList.less';
+
+export interface NoticeIconTabProps {
+  count?: number;
+  showClear?: boolean;
+  showViewMore?: boolean;
+  style?: React.CSSProperties;
+  title: string;
+  tabKey: string;
+  onClick?: (item: API.NoticeIconData) => void;
+  onClear?: () => void;
+  emptyText?: string;
+  clearText?: string;
+  viewMoreText?: string;
+  list: API.NoticeIconData[];
+  onViewMore?: (e: any) => void;
+}
+const NoticeList: React.FC<NoticeIconTabProps> = ({
+  list = [],
+  onClick,
+  onClear,
+  title,
+  onViewMore,
+  emptyText,
+  showClear = true,
+  clearText,
+  viewMoreText,
+  showViewMore = false,
+}) => {
+  if (!list || list.length === 0) {
+    return (
+      <div className={styles.notFound}>
+        <img
+          src="https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg"
+          alt="not found"
+        />
+        <div>{emptyText}</div>
+      </div>
+    );
+  }
+  return (
+    <div>
+      <List<API.NoticeIconData>
+        className={styles.list}
+        dataSource={list}
+        renderItem={(item, i) => {
+          const itemCls = classNames(styles.item, {
+            [styles.read]: item.read,
+          });
+          // eslint-disable-next-line no-nested-ternary
+          const leftIcon = item.avatar ? (
+            typeof item.avatar === 'string' ? (
+              <Avatar className={styles.avatar} src={item.avatar} />
+            ) : (
+              <span className={styles.iconElement}>{item.avatar}</span>
+            )
+          ) : null;
+
+          return (
+            <List.Item
+              className={itemCls}
+              key={item.id || i}
+              onClick={() => onClick && onClick(item)}
+            >
+              <List.Item.Meta
+                className={styles.meta}
+                avatar={leftIcon}
+                title={
+                  <div className={styles.title}>
+                    {item.title}
+                    <div className={styles.extra}>{item.extra}</div>
+                  </div>
+                }
+                description={
+                  <div>
+                    <div className={styles.description}>{item.description}</div>
+                    <div className={styles.datetime}>{item.datetime}</div>
+                  </div>
+                }
+              />
+            </List.Item>
+          );
+        }}
+      />
+      <div className={styles.bottomBar}>
+        {showClear ? (
+          <div onClick={onClear}>
+            {clearText} {title}
+          </div>
+        ) : null}
+        {showViewMore ? (
+          <div
+            onClick={(e) => {
+              if (onViewMore) {
+                onViewMore(e);
+              }
+            }}
+          >
+            {viewMoreText}
+          </div>
+        ) : null}
+      </div>
+    </div>
+  );
+};
+
+export default NoticeList;

+ 35 - 0
src/components/NoticeIcon/index.less

@@ -0,0 +1,35 @@
+@import '~antd/es/style/themes/default.less';
+
+.popover {
+  position: relative;
+  width: 336px;
+}
+
+.noticeButton {
+  display: inline-block;
+  cursor: pointer;
+  transition: all 0.3s;
+}
+.icon {
+  padding: 4px;
+  vertical-align: middle;
+}
+
+.badge {
+  font-size: 16px;
+}
+
+.tabs {
+  :global {
+    .ant-tabs-nav-list {
+      margin: auto;
+    }
+
+    .ant-tabs-nav-scroll {
+      text-align: center;
+    }
+    .ant-tabs-bar {
+      margin-bottom: 0;
+    }
+  }
+}

+ 158 - 0
src/components/NoticeIcon/index.tsx

@@ -0,0 +1,158 @@
+import React, { useEffect, useState, useCallback } from 'react';
+import { Tag, message } from 'antd';
+import { groupBy } from 'lodash';
+import moment from 'moment';
+import { useModel } from 'umi';
+import { queryNotices } from '@/services/user';
+
+import NoticeIcon from './NoticeIcon';
+import styles from './index.less';
+
+const getNoticeData = (
+  notices: API.NoticeIconData[],
+): {
+  [key: string]: API.NoticeIconData[];
+} => {
+  if (!notices || notices.length === 0 || !Array.isArray(notices)) {
+    return {};
+  }
+
+  const newNotices = notices.map((notice) => {
+    const newNotice = { ...notice };
+
+    if (newNotice.datetime) {
+      newNotice.datetime = moment(notice.datetime as string).fromNow();
+    }
+
+    if (newNotice.id) {
+      newNotice.key = newNotice.id;
+    }
+
+    if (newNotice.extra && newNotice.status) {
+      const color = {
+        todo: '',
+        processing: 'blue',
+        urgent: 'red',
+        doing: 'gold',
+      }[newNotice.status];
+      newNotice.extra = (
+        <Tag
+          color={color}
+          style={{
+            marginRight: 0,
+          }}
+        >
+          {newNotice.extra}
+        </Tag>
+      );
+    }
+
+    return newNotice;
+  });
+  return groupBy(newNotices, 'type');
+};
+
+const getUnreadData = (noticeData: { [key: string]: API.NoticeIconData[] }) => {
+  const unreadMsg: {
+    [key: string]: number;
+  } = {};
+  Object.keys(noticeData).forEach((key) => {
+    const value = noticeData[key];
+
+    if (!unreadMsg[key]) {
+      unreadMsg[key] = 0;
+    }
+
+    if (Array.isArray(value)) {
+      unreadMsg[key] = value.filter((item) => !item.read).length;
+    }
+  });
+  return unreadMsg;
+};
+
+export interface GlobalHeaderRightProps {
+  fetchingNotices?: boolean;
+  onNoticeVisibleChange?: (visible: boolean) => void;
+  onNoticeClear?: (tabName?: string) => void;
+}
+
+const NoticeIconView = () => {
+  const { initialState } = useModel('@@initialState');
+  const { currentUser } = initialState || {};
+  const [notices, setNotices] = useState<API.NoticeIconData[]>([]);
+
+  useEffect(() => {
+    queryNotices().then(({ data }) => setNotices(data));
+  }, []);
+
+  const noticeData = getNoticeData(notices);
+  const unreadMsg = getUnreadData(noticeData || {});
+
+  const changeReadState = useCallback((id: string) => {
+    setNotices(
+      notices.map((item) => {
+        const notice = { ...item };
+        if (notice.id === id) {
+          notice.read = true;
+        }
+        return notice;
+      }),
+    );
+  }, []);
+
+  const clearReadState = (title: string, key: string) => {
+    setNotices(
+      notices.map((item) => {
+        const notice = { ...item };
+        if (notice.type === key) {
+          notice.read = true;
+        }
+        return notice;
+      }),
+    );
+    message.success(`${'清空了'} ${title}`);
+  };
+
+  return (
+    <NoticeIcon
+      className={styles.action}
+      count={currentUser && currentUser.unreadCount}
+      onItemClick={(item) => {
+        changeReadState(item.id);
+      }}
+      onClear={(title: string, key: string) => clearReadState(title, key)}
+      loading={false}
+      clearText="清空"
+      viewMoreText="查看更多"
+      onViewMore={() => message.info('Click on view more')}
+      clearClose
+    >
+      <NoticeIcon.Tab
+        tabKey="notification"
+        count={unreadMsg.notification}
+        list={noticeData.notification}
+        title="通知"
+        emptyText="你已查看所有通知"
+        showViewMore
+      />
+      <NoticeIcon.Tab
+        tabKey="message"
+        count={unreadMsg.message}
+        list={noticeData.message}
+        title="消息"
+        emptyText="您已读完所有消息"
+        showViewMore
+      />
+      <NoticeIcon.Tab
+        tabKey="event"
+        title="待办"
+        emptyText="你已完成所有待办"
+        count={unreadMsg.event}
+        list={noticeData.event}
+        showViewMore
+      />
+    </NoticeIcon>
+  );
+};
+
+export default NoticeIconView;

+ 107 - 0
src/components/RightContent/AvatarDropdown.tsx

@@ -0,0 +1,107 @@
+import React, { useCallback } from 'react';
+import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons';
+import { Avatar, Menu, Spin } from 'antd';
+import { history, useModel } from 'umi';
+import { outLogin } from '@/services/login';
+import { stringify } from 'querystring';
+import HeaderDropdown from '../HeaderDropdown';
+import styles from './index.less';
+
+export interface GlobalHeaderRightProps {
+  menu?: boolean;
+}
+
+/**
+ * 退出登录,并且将当前的 url 保存
+ */
+const loginOut = async () => {
+  await outLogin();
+  const { query, pathname } = history.location;
+  const { redirect } = query;
+  // Note: There may be security issues, please note
+  if (window.location.pathname !== '/user/login' && !redirect) {
+    history.replace({
+      pathname: '/user/login',
+      search: stringify({
+        redirect: pathname,
+      }),
+    });
+  }
+};
+
+const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
+  const { initialState, setInitialState } = useModel('@@initialState');
+
+  const onMenuClick = useCallback(
+    (event: {
+      key: React.Key;
+      keyPath: React.Key[];
+      item: React.ReactInstance;
+      domEvent: React.MouseEvent<HTMLElement>;
+    }) => {
+      const { key } = event;
+      if (key === 'logout' && initialState) {
+        setInitialState({ ...initialState, currentUser: undefined });
+        loginOut();
+        return;
+      }
+      history.push(`/account/${key}`);
+    },
+    [],
+  );
+
+  const loading = (
+    <span className={`${styles.action} ${styles.account}`}>
+      <Spin
+        size="small"
+        style={{
+          marginLeft: 8,
+          marginRight: 8,
+        }}
+      />
+    </span>
+  );
+
+  if (!initialState) {
+    return loading;
+  }
+
+  const { currentUser } = initialState;
+
+  if (!currentUser || !currentUser.name) {
+    return loading;
+  }
+
+  const menuHeaderDropdown = (
+    <Menu className={styles.menu} selectedKeys={[]} onClick={onMenuClick}>
+      {menu && (
+        <Menu.Item key="center">
+          <UserOutlined />
+          个人中心
+        </Menu.Item>
+      )}
+      {menu && (
+        <Menu.Item key="settings">
+          <SettingOutlined />
+          个人设置
+        </Menu.Item>
+      )}
+      {menu && <Menu.Divider />}
+
+      <Menu.Item key="logout">
+        <LogoutOutlined />
+        退出登录
+      </Menu.Item>
+    </Menu>
+  );
+  return (
+    <HeaderDropdown overlay={menuHeaderDropdown}>
+      <span className={`${styles.action} ${styles.account}`}>
+        <Avatar size="small" className={styles.avatar} src={currentUser.avatar} alt="avatar" />
+        <span className={`${styles.name} anticon`}>{currentUser.name}</span>
+      </span>
+    </HeaderDropdown>
+  );
+};
+
+export default AvatarDropdown;

+ 62 - 0
src/components/RightContent/index.less

@@ -0,0 +1,62 @@
+@import '~antd/es/style/themes/default.less';
+
+@pro-header-hover-bg: rgba(0, 0, 0, 0.025);
+
+.menu {
+  :global(.anticon) {
+    margin-right: 8px;
+  }
+  :global(.ant-dropdown-menu-item) {
+    min-width: 160px;
+  }
+}
+
+.right {
+  display: flex;
+  float: right;
+  height: 48px;
+  margin-left: auto;
+  overflow: hidden;
+  .action {
+    display: flex;
+    align-items: center;
+    height: 48px;
+    padding: 0 12px;
+    cursor: pointer;
+    transition: all 0.3s;
+    > span {
+      vertical-align: middle;
+    }
+    &:hover {
+      background: @pro-header-hover-bg;
+    }
+    &:global(.opened) {
+      background: @pro-header-hover-bg;
+    }
+  }
+  .search {
+    padding: 0 12px;
+    &:hover {
+      background: transparent;
+    }
+  }
+  .account {
+    .avatar {
+      margin-right: 8px;
+      color: @primary-color;
+      vertical-align: top;
+      background: rgba(255, 255, 255, 0.85);
+    }
+  }
+}
+
+.dark {
+  .action {
+    &:hover {
+      background: #252a3d;
+    }
+    &:global(.opened) {
+      background: #252a3d;
+    }
+  }
+}

+ 90 - 0
src/components/RightContent/index.tsx

@@ -0,0 +1,90 @@
+import { Tag, Space, Menu } from 'antd';
+import { QuestionCircleOutlined } from '@ant-design/icons';
+import React from 'react';
+import { useModel, SelectLang } from 'umi';
+import Avatar from './AvatarDropdown';
+import HeaderDropdown from '../HeaderDropdown';
+import HeaderSearch from '../HeaderSearch';
+import styles from './index.less';
+
+export type SiderTheme = 'light' | 'dark';
+
+const ENVTagColor = {
+  dev: 'orange',
+  test: 'green',
+  pre: '#87d068',
+};
+
+const GlobalHeaderRight: React.FC<{}> = () => {
+  const { initialState } = useModel('@@initialState');
+
+  if (!initialState || !initialState.settings) {
+    return null;
+  }
+
+  const { navTheme, layout } = initialState.settings;
+  let className = styles.right;
+
+  if ((navTheme === 'dark' && layout === 'top') || layout === 'mix') {
+    className = `${styles.right}  ${styles.dark}`;
+  }
+  return (
+    <Space className={className}>
+      <HeaderSearch
+        className={`${styles.action} ${styles.search}`}
+        placeholder="站内搜索"
+        defaultValue="umi ui"
+        options={[
+          { label: <a href="https://umijs.org/zh/guide/umi-ui.html">umi ui</a>, value: 'umi ui' },
+          {
+            label: <a href="next.ant.design">Ant Design</a>,
+            value: 'Ant Design',
+          },
+          {
+            label: <a href="https://protable.ant.design/">Pro Table</a>,
+            value: 'Pro Table',
+          },
+          {
+            label: <a href="https://prolayout.ant.design/">Pro Layout</a>,
+            value: 'Pro Layout',
+          },
+        ]}
+        // onSearch={value => {
+        //   console.log('input', value);
+        // }}
+      />
+      <HeaderDropdown
+        overlay={
+          <Menu>
+            <Menu.Item
+              onClick={() => {
+                window.open('/~docs');
+              }}
+            >
+              组件文档
+            </Menu.Item>
+            <Menu.Item
+              onClick={() => {
+                window.open('https://pro.ant.design/docs/getting-started');
+              }}
+            >
+              Ant Design Pro 文档
+            </Menu.Item>
+          </Menu>
+        }
+      >
+        <span className={styles.action}>
+          <QuestionCircleOutlined />
+        </span>
+      </HeaderDropdown>
+      <Avatar />
+      {REACT_APP_ENV && (
+        <span>
+          <Tag color={ENVTagColor[REACT_APP_ENV]}>{REACT_APP_ENV}</Tag>
+        </span>
+      )}
+      <SelectLang className={styles.action} />
+    </Space>
+  );
+};
+export default GlobalHeaderRight;

+ 170 - 0
src/components/common/DataTable/index.tsx

@@ -0,0 +1,170 @@
+import React, { useRef } from 'react';
+import { PlusOutlined, EllipsisOutlined } from '@ant-design/icons';
+import { Button, Tag, Space, Menu, Dropdown } from 'antd';
+import ProTable, { ProColumns, TableDropdown, ActionType } from '@ant-design/pro-table';
+import request from 'umi-request';
+
+const columns: ProColumns[] = [
+  {
+    dataIndex: 'index',
+    valueType: 'indexBorder',
+    width: 48,
+  },
+  {
+    title: '标题',
+    dataIndex: 'title',
+    copyable: true,
+    ellipsis: true,
+    tip: '标题过长会自动收缩',
+    formItemProps: {
+      rules: [
+        {
+          required: true,
+          message: '此项为必填项',
+        },
+      ],
+    },
+    width: '30%',
+    search: false,
+  },
+  {
+    title: '状态',
+    dataIndex: 'state',
+    initialValue: 'open',
+    filters: true,
+    valueType: 'select',
+    valueEnum: {
+      all: { text: '全部', status: 'Default' },
+      open: {
+        text: '未解决',
+        status: 'Error',
+      },
+      closed: {
+        text: '已解决',
+        status: 'Success',
+      },
+      processing: {
+        text: '解决中',
+        status: 'Processing',
+      },
+    },
+  },
+  {
+    title: '标签',
+    dataIndex: 'labels',
+    renderFormItem: (_, { defaultRender }) => {
+      return defaultRender(_);
+    },
+    render: (_, record) => (
+      <Space>
+        {record.labels.map(({ name, color }) => (
+          <Tag color={color} key={name}>
+            {name}
+          </Tag>
+        ))}
+      </Space>
+    ),
+  },
+  {
+    title: '创建时间',
+    key: 'created_at',
+    dataIndex: 'created_at',
+    valueType: 'date',
+  },
+  {
+    title: '操作',
+    valueType: 'option',
+    render: (text, record, _, action) => [
+      <a
+        key="editable"
+        onClick={() => {
+          action.startEditable?.(record.id);
+        }}
+      >
+        编辑
+      </a>,
+      <a href={record.url} target="_blank" rel="noopener noreferrer" key="view">
+        查看
+      </a>,
+      <TableDropdown
+        key="actionGroup"
+        onSelect={() => action.reload()}
+        menus={[
+          { key: 'copy', name: '复制' },
+          { key: 'delete', name: '删除' },
+        ]}
+      />,
+    ],
+  },
+];
+
+const menu = (
+  <Menu>
+    <Menu.Item key="1">1st item</Menu.Item>
+    <Menu.Item key="2">2nd item</Menu.Item>
+    <Menu.Item key="3">3rd item</Menu.Item>
+  </Menu>
+);
+
+export default (props) => {
+  const actionRef = useRef<ActionType>();
+
+  return (
+    <ProTable
+      columns={
+        //默认配置列都不出现在搜索中
+        props.columns.map((v) => {
+          if(!v.search){
+            v.search = false;
+          }
+          return v
+        })
+      }
+      actionRef={actionRef}
+      request={async (params = {}) =>
+      {
+        params.pageNo = params.current;
+        delete params.current;
+        const msg = await request(props.url, {
+          params,
+        });
+        if(msg.error.length > 0){
+          return {
+            success: false,
+          };
+        }else{
+          return {
+            data: msg.data.list,
+            success: true,
+            total: msg.data.totalCount,
+          };
+        }
+      }
+      }
+      editable={{
+        type: 'multiple',
+      }}
+      rowKey={props.rowKey || 'id'}
+      search={{
+        filterType: 'query',
+        labelWidth: 'auto',
+        defaultCollapsed: false,
+      }}
+      pagination={{
+        pageSize: 10,
+      }}
+      dateFormatter="string"
+      headerTitle="高级表格"
+      toolBarRender={() => [
+      <Button key="button" icon={<PlusOutlined />} type="primary">
+        新建
+      </Button>,
+      <Dropdown key="menu" overlay={menu}>
+        <Button>
+          <EllipsisOutlined />
+        </Button>
+      </Dropdown>,
+    ]}
+      />
+  );
+};

+ 270 - 0
src/components/index.md

@@ -0,0 +1,270 @@
+---
+title: Ant Design Pro
+sidemenu: false
+---
+
+# Ant Design Pro
+
+这里列举了 Pro 中所有用到的组件,这些组件不适合作为组件库,但是在业务中却真实需要。所以我们准备了这个文档,来指导大家是否需要使用这个组件。
+
+## Footer 页脚组件
+
+这个组件自带了一些 Pro 的配置的,你一般都需要改掉它的信息。
+
+```tsx
+/**
+ * background: '#f0f2f5'
+ */
+import React from 'react';
+import Footer from '@/components/Footer';
+
+export default () => <Footer />;
+```
+
+## HeaderDropdown 头部下拉列表
+
+HeaderDropdown 是 antd Dropdown 的封装,但是增加了移动端的特殊处理,用法也是相同的。
+
+```tsx
+/**
+ * background: '#f0f2f5'
+ */
+import { Button, Menu } from 'antd';
+import React from 'react';
+import HeaderDropdown from '@/components/HeaderDropdown';
+
+export default () => {
+  const menuHeaderDropdown = (
+    <Menu selectedKeys={[]}>
+      <Menu.Item key="center">个人中心</Menu.Item>
+      <Menu.Item key="settings">个人设置</Menu.Item>
+      <Menu.Divider />
+      <Menu.Item key="logout">退出登录</Menu.Item>
+    </Menu>
+  );
+  return (
+    <HeaderDropdown overlay={menuHeaderDropdown}>
+      <Button>hover 展示菜单</Button>
+    </HeaderDropdown>
+  );
+};
+```
+
+## HeaderSearch 头部搜索框
+
+一个带补全数据的输入框,支持收起和展开 Input
+
+```tsx
+/**
+ * background: '#f0f2f5'
+ */
+import { Button, Menu } from 'antd';
+import React from 'react';
+import HeaderSearch from '@/components/HeaderSearch';
+
+export default () => {
+  return (
+    <HeaderSearch
+      placeholder="站内搜索"
+      defaultValue="umi ui"
+      options={[
+        { label: 'Ant Design Pro', value: 'Ant Design Pro' },
+        {
+          label: 'Ant Design',
+          value: 'Ant Design',
+        },
+        {
+          label: 'Pro Table',
+          value: 'Pro Table',
+        },
+        {
+          label: 'Pro Layout',
+          value: 'Pro Layout',
+        },
+      ]}
+      onSearch={(value) => {
+        console.log('input', value);
+      }}
+    />
+  );
+};
+```
+
+### API
+
+| 参数            | 说明                               | 类型                         | 默认值 |
+| --------------- | ---------------------------------- | ---------------------------- | ------ |
+| value           | 输入框的值                         | `string`                     | -      |
+| onChange        | 值修改后触发                       | `(value?: string) => void`   | -      |
+| onSearch        | 查询后触发                         | `(value?: string) => void`   | -      |
+| options         | 选项菜单的的列表                   | `{label,value}[]`            | -      |
+| defaultVisible  | 输入框默认是否显示,只有第一次生效 | `boolean`                    | -      |
+| visible         | 输入框是否显示                     | `boolean`                    | -      |
+| onVisibleChange | 输入框显示隐藏的回调函数           | `(visible: boolean) => void` | -      |
+
+## NoticeIcon 通知工具
+
+通知工具提供一个展示多种通知信息的界面。
+
+```tsx
+/**
+ * background: '#f0f2f5'
+ */
+import { message } from 'antd';
+import React from 'react';
+import NoticeIcon from '@/components/NoticeIcon/NoticeIcon';
+
+export default () => {
+  const list = [
+    {
+      id: '000000001',
+      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
+      title: '你收到了 14 份新周报',
+      datetime: '2017-08-09',
+      type: 'notification',
+    },
+    {
+      id: '000000002',
+      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
+      title: '你推荐的 曲妮妮 已通过第三轮面试',
+      datetime: '2017-08-08',
+      type: 'notification',
+    },
+  ];
+  return (
+    <NoticeIcon
+      count={10}
+      onItemClick={(item) => {
+        message.info(`${item.title} 被点击了`);
+      }}
+      onClear={(title: string, key: string) => message.info('点击了清空更多')}
+      loading={false}
+      clearText="清空"
+      viewMoreText="查看更多"
+      onViewMore={() => message.info('点击了查看更多')}
+      clearClose
+    >
+      <NoticeIcon.Tab
+        tabKey="notification"
+        count={2}
+        list={list}
+        title="通知"
+        emptyText="你已查看所有通知"
+        showViewMore
+      />
+      <NoticeIcon.Tab
+        tabKey="message"
+        count={2}
+        list={list}
+        title="消息"
+        emptyText="您已读完所有消息"
+        showViewMore
+      />
+      <NoticeIcon.Tab
+        tabKey="event"
+        title="待办"
+        emptyText="你已完成所有待办"
+        count={2}
+        list={list}
+        showViewMore
+      />
+    </NoticeIcon>
+  );
+};
+```
+
+### NoticeIcon API
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| count | 有多少未读通知 | `number` | - |
+| bell | 铃铛的图表 | `ReactNode` | - |
+| onClear | 点击清空数据按钮 | `(tabName: string, tabKey: string) => void` | - |
+| onItemClick | 未读消息列被点击 | `(item: API.NoticeIconData, tabProps: NoticeIconTabProps) => void` | - |
+| onViewMore | 查看更多的按钮点击 | `(tabProps: NoticeIconTabProps, e: MouseEvent) => void` | - |
+| onTabChange | 通知 Tab 的切换 | `(tabTile: string) => void;` | - |
+| popupVisible | 通知显示是否展示 | `boolean` | - |
+| onPopupVisibleChange | 通知信息显示隐藏的回调函数 | `(visible: boolean) => void` | - |
+| clearText | 清空按钮的文字 | `string` | - |
+| viewMoreText | 查看更多的按钮文字 | `string` | - |
+| clearClose | 展示清空按钮 | `boolean` | - |
+| emptyImage | 列表为空时的兜底展示 | `ReactNode` | - |
+
+### NoticeIcon.Tab API
+
+| 参数         | 说明               | 类型                                 | 默认值 |
+| ------------ | ------------------ | ------------------------------------ | ------ |
+| count        | 有多少未读通知     | `number`                             | -      |
+| title        | 通知 Tab 的标题    | `ReactNode`                          | -      |
+| showClear    | 展示清除按钮       | `boolean`                            | `true` |
+| showViewMore | 展示加载更         | `boolean`                            | `true` |
+| tabKey       | Tab 的唯一 key     | `string`                             | -      |
+| onClick      | 子项的单击事件     | `(item: API.NoticeIconData) => void` | -      |
+| onClear      | 清楚按钮的点击     | `()=>void`                           | -      |
+| emptyText    | 为空的时候测试     | `()=>void`                           | -      |
+| viewMoreText | 查看更多的按钮文字 | `string`                             | -      |
+| onViewMore   | 查看更多的按钮点击 | `( e: MouseEvent) => void`           | -      |
+| list         | 通知信息的列表     | `API.NoticeIconData`                 | -      |
+
+### NoticeIconData
+
+```tsx | pure
+export interface NoticeIconData {
+  id: string;
+  key: string;
+  avatar: string;
+  title: string;
+  datetime: string;
+  type: string;
+  read?: boolean;
+  description: string;
+  clickClose?: boolean;
+  extra: any;
+  status: string;
+}
+```
+
+## RightContent
+
+RightContent 是以上几个组件的组合,同时新增了 plugins 的 `SelectLang` 插件。
+
+```tsx | pure
+<Space>
+  <HeaderSearch
+    placeholder="站内搜索"
+    defaultValue="umi ui"
+    options={[
+      { label: <a href="https://umijs.org/zh/guide/umi-ui.html">umi ui</a>, value: 'umi ui' },
+      {
+        label: <a href="next.ant.design">Ant Design</a>,
+        value: 'Ant Design',
+      },
+      {
+        label: <a href="https://protable.ant.design/">Pro Table</a>,
+        value: 'Pro Table',
+      },
+      {
+        label: <a href="https://prolayout.ant.design/">Pro Layout</a>,
+        value: 'Pro Layout',
+      },
+    ]}
+  />
+  <Tooltip title="使用文档">
+    <span
+      className={styles.action}
+      onClick={() => {
+        window.location.href = 'https://pro.ant.design/docs/getting-started';
+      }}
+    >
+      <QuestionCircleOutlined />
+    </span>
+  </Tooltip>
+  <Avatar />
+  {REACT_APP_ENV && (
+    <span>
+      <Tag color={ENVTagColor[REACT_APP_ENV]}>{REACT_APP_ENV}</Tag>
+    </span>
+  )}
+  <SelectLang className={styles.action} />
+</Space>
+```

+ 57 - 0
src/e2e/baseLayout.e2e.js

@@ -0,0 +1,57 @@
+const { uniq } = require('lodash');
+const RouterConfig = require('../../config/config').default.routes;
+
+const BASE_URL = `http://localhost:${process.env.PORT || 8000}`;
+
+function formatter(routes, parentPath = '') {
+  const fixedParentPath = parentPath.replace(/\/{1,}/g, '/');
+  let result = [];
+  routes.forEach((item) => {
+    if (item.path) {
+      result.push(`${fixedParentPath}/${item.path}`.replace(/\/{1,}/g, '/'));
+    }
+    if (item.routes) {
+      result = result.concat(
+        formatter(item.routes, item.path ? `${fixedParentPath}/${item.path}` : parentPath),
+      );
+    }
+  });
+  return uniq(result.filter((item) => !!item));
+}
+
+beforeEach(async () => {
+  await page.goto(`${BASE_URL}`);
+  await page.evaluate(() => {
+    localStorage.setItem('antd-pro-authority', '["admin"]');
+  });
+});
+
+describe('Ant Design Pro E2E test', () => {
+  const testPage = (path) => async () => {
+    await page.goto(`${BASE_URL}${path}`);
+    await page.waitForSelector('footer', {
+      timeout: 2000,
+    });
+    const haveFooter = await page.evaluate(
+      () => document.getElementsByTagName('footer').length > 0,
+    );
+    expect(haveFooter).toBeTruthy();
+  };
+
+  const routers = formatter(RouterConfig);
+  routers.forEach((route) => {
+    it(`test pages ${route}`, testPage(route));
+  });
+
+  it('topmenu should have footer', async () => {
+    const params = '?navTheme=light&layout=topmenu';
+    await page.goto(`${BASE_URL}${params}`);
+    await page.waitForSelector('footer', {
+      timeout: 2000,
+    });
+    const haveFooter = await page.evaluate(
+      () => document.getElementsByTagName('footer').length > 0,
+    );
+    expect(haveFooter).toBeTruthy();
+  });
+});

+ 54 - 0
src/global.less

@@ -0,0 +1,54 @@
+@import '~antd/es/style/themes/default.less';
+
+html,
+body,
+#root {
+  height: 100%;
+}
+
+.colorWeak {
+  filter: invert(80%);
+}
+
+.ant-layout {
+  min-height: 100vh;
+}
+
+canvas {
+  display: block;
+}
+
+body {
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+ul,
+ol {
+  list-style: none;
+}
+
+@media (max-width: @screen-xs) {
+  .ant-table {
+    width: 100%;
+    overflow-x: auto;
+    &-thead > tr,
+    &-tbody > tr {
+      > th,
+      > td {
+        white-space: pre;
+        > span {
+          display: block;
+        }
+      }
+    }
+  }
+}
+
+// 兼容IE11
+@media screen and(-ms-high-contrast: active), (-ms-high-contrast: none) {
+  body .ant-design-pro > .ant-layout {
+    min-height: 100vh;
+  }
+}

+ 85 - 0
src/global.tsx

@@ -0,0 +1,85 @@
+import { Button, message, notification } from 'antd';
+
+import React from 'react';
+import { useIntl } from 'umi';
+import defaultSettings from '../config/defaultSettings';
+
+const { pwa } = defaultSettings;
+const isHttps = document.location.protocol === 'https:';
+
+// if pwa is true
+if (pwa) {
+  // Notify user if offline now
+  window.addEventListener('sw.offline', () => {
+    message.warning(useIntl().formatMessage({ id: 'app.pwa.offline' }));
+  });
+
+  // Pop up a prompt on the page asking the user if they want to use the latest version
+  window.addEventListener('sw.updated', (event: Event) => {
+    const e = event as CustomEvent;
+    const reloadSW = async () => {
+      // Check if there is sw whose state is waiting in ServiceWorkerRegistration
+      // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration
+      const worker = e.detail && e.detail.waiting;
+      if (!worker) {
+        return true;
+      }
+      // Send skip-waiting event to waiting SW with MessageChannel
+      await new Promise((resolve, reject) => {
+        const channel = new MessageChannel();
+        channel.port1.onmessage = (msgEvent) => {
+          if (msgEvent.data.error) {
+            reject(msgEvent.data.error);
+          } else {
+            resolve(msgEvent.data);
+          }
+        };
+        worker.postMessage({ type: 'skip-waiting' }, [channel.port2]);
+      });
+      // Refresh current page to use the updated HTML and other assets after SW has skiped waiting
+      window.location.reload(true);
+      return true;
+    };
+    const key = `open${Date.now()}`;
+    const btn = (
+      <Button
+        type="primary"
+        onClick={() => {
+          notification.close(key);
+          reloadSW();
+        }}
+      >
+        {useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated.ok' })}
+      </Button>
+    );
+    notification.open({
+      message: useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated' }),
+      description: useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated.hint' }),
+      btn,
+      key,
+      onClose: async () => {},
+    });
+  });
+} else if ('serviceWorker' in navigator && isHttps) {
+  // unregister service worker
+  const { serviceWorker } = navigator;
+  if (serviceWorker.getRegistrations) {
+    serviceWorker.getRegistrations().then((sws) => {
+      sws.forEach((sw) => {
+        sw.unregister();
+      });
+    });
+  }
+  serviceWorker.getRegistration().then((sw) => {
+    if (sw) sw.unregister();
+  });
+
+  // remove all caches
+  if (window.caches && window.caches.keys) {
+    caches.keys().then((keys) => {
+      keys.forEach((key) => {
+        caches.delete(key);
+      });
+    });
+  }
+}

+ 24 - 0
src/locales/en-US.ts

@@ -0,0 +1,24 @@
+import component from './en-US/component';
+import globalHeader from './en-US/globalHeader';
+import menu from './en-US/menu';
+import pwa from './en-US/pwa';
+import settingDrawer from './en-US/settingDrawer';
+import settings from './en-US/settings';
+import pages from './en-US/pages';
+
+export default {
+  'navBar.lang': 'Languages',
+  'layout.user.link.help': 'Help',
+  'layout.user.link.privacy': 'Privacy',
+  'layout.user.link.terms': 'Terms',
+  'app.preview.down.block': 'Download this page to your local project',
+  'app.welcome.link.fetch-blocks': 'Get all block',
+  'app.welcome.link.block-list': 'Quickly build standard, pages based on `block` development',
+  ...globalHeader,
+  ...menu,
+  ...settingDrawer,
+  ...settings,
+  ...pwa,
+  ...component,
+  ...pages,
+};

+ 5 - 0
src/locales/en-US/component.ts

@@ -0,0 +1,5 @@
+export default {
+  'component.tagSelect.expand': 'Expand',
+  'component.tagSelect.collapse': 'Collapse',
+  'component.tagSelect.all': 'All',
+};

+ 17 - 0
src/locales/en-US/globalHeader.ts

@@ -0,0 +1,17 @@
+export default {
+  'component.globalHeader.search': 'Search',
+  'component.globalHeader.search.example1': 'Search example 1',
+  'component.globalHeader.search.example2': 'Search example 2',
+  'component.globalHeader.search.example3': 'Search example 3',
+  'component.globalHeader.help': 'Help',
+  'component.globalHeader.notification': 'Notification',
+  'component.globalHeader.notification.empty': 'You have viewed all notifications.',
+  'component.globalHeader.message': 'Message',
+  'component.globalHeader.message.empty': 'You have viewed all messsages.',
+  'component.globalHeader.event': 'Event',
+  'component.globalHeader.event.empty': 'You have viewed all events.',
+  'component.noticeIcon.clear': 'Clear',
+  'component.noticeIcon.cleared': 'Cleared',
+  'component.noticeIcon.empty': 'No notifications',
+  'component.noticeIcon.view-more': 'View more',
+};

+ 52 - 0
src/locales/en-US/menu.ts

@@ -0,0 +1,52 @@
+export default {
+  'menu.welcome': 'Welcome',
+  'menu.more-blocks': 'More Blocks',
+  'menu.home': 'Home',
+  'menu.admin': 'Admin',
+  'menu.admin.sub-page': 'Sub-Page',
+  'menu.login': 'Login',
+  'menu.register': 'Register',
+  'menu.register.result': 'Register Result',
+  'menu.dashboard': 'Dashboard',
+  'menu.dashboard.analysis': 'Analysis',
+  'menu.dashboard.monitor': 'Monitor',
+  'menu.dashboard.workplace': 'Workplace',
+  'menu.exception.403': '403',
+  'menu.exception.404': '404',
+  'menu.exception.500': '500',
+  'menu.form': 'Form',
+  'menu.form.basic-form': 'Basic Form',
+  'menu.form.step-form': 'Step Form',
+  'menu.form.step-form.info': 'Step Form(write transfer information)',
+  'menu.form.step-form.confirm': 'Step Form(confirm transfer information)',
+  'menu.form.step-form.result': 'Step Form(finished)',
+  'menu.form.advanced-form': 'Advanced Form',
+  'menu.list': 'List',
+  'menu.list.table-list': 'Search Table',
+  'menu.list.basic-list': 'Basic List',
+  'menu.list.card-list': 'Card List',
+  'menu.list.search-list': 'Search List',
+  'menu.list.search-list.articles': 'Search List(articles)',
+  'menu.list.search-list.projects': 'Search List(projects)',
+  'menu.list.search-list.applications': 'Search List(applications)',
+  'menu.profile': 'Profile',
+  'menu.profile.basic': 'Basic Profile',
+  'menu.profile.advanced': 'Advanced Profile',
+  'menu.result': 'Result',
+  'menu.result.success': 'Success',
+  'menu.result.fail': 'Fail',
+  'menu.exception': 'Exception',
+  'menu.exception.not-permission': '403',
+  'menu.exception.not-find': '404',
+  'menu.exception.server-error': '500',
+  'menu.exception.trigger': 'Trigger',
+  'menu.account': 'Account',
+  'menu.account.center': 'Account Center',
+  'menu.account.settings': 'Account Settings',
+  'menu.account.trigger': 'Trigger Error',
+  'menu.account.logout': 'Logout',
+  'menu.editor': 'Graphic Editor',
+  'menu.editor.flow': 'Flow Editor',
+  'menu.editor.mind': 'Mind Editor',
+  'menu.editor.koni': 'Koni Editor',
+};

+ 68 - 0
src/locales/en-US/pages.ts

@@ -0,0 +1,68 @@
+export default {
+  'pages.layouts.userLayout.title':
+    'Ant Design is the most influential web design specification in Xihu district',
+  'pages.login.accountLogin.tab': 'Account Login',
+  'pages.login.accountLogin.errorMessage': 'Incorrect username/password(admin/ant.design)',
+  'pages.login.username.placeholder': 'Username: admin or user',
+  'pages.login.username.required': 'Please input your username!',
+  'pages.login.password.placeholder': 'Password: ant.design',
+  'pages.login.password.required': 'Please input your password!',
+  'pages.login.phoneLogin.tab': 'Phone Login',
+  'pages.login.phoneLogin.errorMessage': 'Verification Code Error',
+  'pages.login.phoneNumber.placeholder': 'Phone Number',
+  'pages.login.phoneNumber.required': 'Please input your phone number!',
+  'pages.login.phoneNumber.invalid': 'Phone number is invalid!',
+  'pages.login.captcha.placeholder': 'Verification Code',
+  'pages.login.captcha.required': 'Please input verification code!',
+  'pages.login.phoneLogin.getVerificationCode': 'Get Code',
+  'pages.getCaptchaSecondText': 'sec(s)',
+  'pages.login.rememberMe': 'Remember me',
+  'pages.login.forgotPassword': 'Forgot Password ?',
+  'pages.login.submit': 'Login',
+  'pages.login.loginWith': 'Login with :',
+  'pages.login.registerAccount': 'Register Account',
+  'pages.welcome.advancedComponent': 'Advanced Component',
+  'pages.welcome.link': 'Welcome',
+  'pages.welcome.advancedLayout': 'Advanced Layout',
+  'pages.welcome.alertMessage': 'Faster and stronger heavy-duty components have been released.',
+  'pages.admin.subPage.title': 'This page can only be viewed by Admin',
+  'pages.admin.subPage.alertMessage':
+    'Umi ui is now released, welcome to use npm run ui to start the experience.',
+  'pages.searchTable.createForm.newRule': 'New Rule',
+  'pages.searchTable.updateForm.ruleConfig': 'Rule configuration',
+  'pages.searchTable.updateForm.basicConfig': 'Basic Information',
+  'pages.searchTable.updateForm.ruleName.nameLabel': 'Rule Name',
+  'pages.searchTable.updateForm.ruleName.nameRules': 'Please enter the rule name!',
+  'pages.searchTable.updateForm.ruleDesc.descLabel': 'Rule Description',
+  'pages.searchTable.updateForm.ruleDesc.descPlaceholder': 'Please enter at least five characters',
+  'pages.searchTable.updateForm.ruleDesc.descRules':
+    'Please enter a rule description of at least five characters!',
+  'pages.searchTable.updateForm.ruleProps.title': 'Configure Properties',
+  'pages.searchTable.updateForm.object': 'Monitoring Object',
+  'pages.searchTable.updateForm.ruleProps.templateLabel': 'Rule Template',
+  'pages.searchTable.updateForm.ruleProps.typeLabel': 'Rule Type',
+  'pages.searchTable.updateForm.schedulingPeriod.title': 'Set Scheduling Period',
+  'pages.searchTable.updateForm.schedulingPeriod.timeLabel': 'Starting Time',
+  'pages.searchTable.updateForm.schedulingPeriod.timeRules': 'Please choose a start time!',
+  'pages.searchTable.titleDesc': 'Description',
+  'pages.searchTable.ruleName': 'Rule name is required',
+  'pages.searchTable.titleCallNo': 'Number of Service Calls',
+  'pages.searchTable.titleStatus': 'Status',
+  'pages.searchTable.nameStatus.default': 'default',
+  'pages.searchTable.nameStatus.running': 'running',
+  'pages.searchTable.nameStatus.online': 'online',
+  'pages.searchTable.nameStatus.abnormal': 'abnormal',
+  'pages.searchTable.titleUpdatedAt': 'Last Scheduled at',
+  'pages.searchTable.exception': 'Please enter the reason for the exception!',
+  'pages.searchTable.titleOption': 'Option',
+  'pages.searchTable.config': 'Configuration',
+  'pages.searchTable.subscribeAlert': 'Subscribe to alerts',
+  'pages.searchTable.title': 'Enquiry Form',
+  'pages.searchTable.new': 'New',
+  'pages.searchTable.chosen': 'chosen',
+  'pages.searchTable.item': 'item',
+  'pages.searchTable.totalServiceCalls': 'Total Number of Service Calls',
+  'pages.searchTable.tenThousand': '0000',
+  'pages.searchTable.batchDeletion': 'bacth deletion',
+  'pages.searchTable.batchApproval': 'batch approval',
+};

+ 6 - 0
src/locales/en-US/pwa.ts

@@ -0,0 +1,6 @@
+export default {
+  'app.pwa.offline': 'You are offline now',
+  'app.pwa.serviceworker.updated': 'New content is available',
+  'app.pwa.serviceworker.updated.hint': 'Please press the "Refresh" button to reload current page',
+  'app.pwa.serviceworker.updated.ok': 'Refresh',
+};

+ 31 - 0
src/locales/en-US/settingDrawer.ts

@@ -0,0 +1,31 @@
+export default {
+  'app.setting.pagestyle': 'Page style setting',
+  'app.setting.pagestyle.dark': 'Dark style',
+  'app.setting.pagestyle.light': 'Light style',
+  'app.setting.content-width': 'Content Width',
+  'app.setting.content-width.fixed': 'Fixed',
+  'app.setting.content-width.fluid': 'Fluid',
+  'app.setting.themecolor': 'Theme Color',
+  'app.setting.themecolor.dust': 'Dust Red',
+  'app.setting.themecolor.volcano': 'Volcano',
+  'app.setting.themecolor.sunset': 'Sunset Orange',
+  'app.setting.themecolor.cyan': 'Cyan',
+  'app.setting.themecolor.green': 'Polar Green',
+  'app.setting.themecolor.daybreak': 'Daybreak Blue (default)',
+  'app.setting.themecolor.geekblue': 'Geek Glue',
+  'app.setting.themecolor.purple': 'Golden Purple',
+  'app.setting.navigationmode': 'Navigation Mode',
+  'app.setting.sidemenu': 'Side Menu Layout',
+  'app.setting.topmenu': 'Top Menu Layout',
+  'app.setting.fixedheader': 'Fixed Header',
+  'app.setting.fixedsidebar': 'Fixed Sidebar',
+  'app.setting.fixedsidebar.hint': 'Works on Side Menu Layout',
+  'app.setting.hideheader': 'Hidden Header when scrolling',
+  'app.setting.hideheader.hint': 'Works when Hidden Header is enabled',
+  'app.setting.othersettings': 'Other Settings',
+  'app.setting.weakmode': 'Weak Mode',
+  'app.setting.copy': 'Copy Setting',
+  'app.setting.copyinfo': 'copy success,please replace defaultSettings in src/models/setting.js',
+  'app.setting.production.hint':
+    'Setting panel shows in development environment only, please manually modify',
+};

+ 60 - 0
src/locales/en-US/settings.ts

@@ -0,0 +1,60 @@
+export default {
+  'app.settings.menuMap.basic': 'Basic Settings',
+  'app.settings.menuMap.security': 'Security Settings',
+  'app.settings.menuMap.binding': 'Account Binding',
+  'app.settings.menuMap.notification': 'New Message Notification',
+  'app.settings.basic.avatar': 'Avatar',
+  'app.settings.basic.change-avatar': 'Change avatar',
+  'app.settings.basic.email': 'Email',
+  'app.settings.basic.email-message': 'Please input your email!',
+  'app.settings.basic.nickname': 'Nickname',
+  'app.settings.basic.nickname-message': 'Please input your Nickname!',
+  'app.settings.basic.profile': 'Personal profile',
+  'app.settings.basic.profile-message': 'Please input your personal profile!',
+  'app.settings.basic.profile-placeholder': 'Brief introduction to yourself',
+  'app.settings.basic.country': 'Country/Region',
+  'app.settings.basic.country-message': 'Please input your country!',
+  'app.settings.basic.geographic': 'Province or city',
+  'app.settings.basic.geographic-message': 'Please input your geographic info!',
+  'app.settings.basic.address': 'Street Address',
+  'app.settings.basic.address-message': 'Please input your address!',
+  'app.settings.basic.phone': 'Phone Number',
+  'app.settings.basic.phone-message': 'Please input your phone!',
+  'app.settings.basic.update': 'Update Information',
+  'app.settings.security.strong': 'Strong',
+  'app.settings.security.medium': 'Medium',
+  'app.settings.security.weak': 'Weak',
+  'app.settings.security.password': 'Account Password',
+  'app.settings.security.password-description': 'Current password strength',
+  'app.settings.security.phone': 'Security Phone',
+  'app.settings.security.phone-description': 'Bound phone',
+  'app.settings.security.question': 'Security Question',
+  'app.settings.security.question-description':
+    'The security question is not set, and the security policy can effectively protect the account security',
+  'app.settings.security.email': 'Backup Email',
+  'app.settings.security.email-description': 'Bound Email',
+  'app.settings.security.mfa': 'MFA Device',
+  'app.settings.security.mfa-description':
+    'Unbound MFA device, after binding, can be confirmed twice',
+  'app.settings.security.modify': 'Modify',
+  'app.settings.security.set': 'Set',
+  'app.settings.security.bind': 'Bind',
+  'app.settings.binding.taobao': 'Binding Taobao',
+  'app.settings.binding.taobao-description': 'Currently unbound Taobao account',
+  'app.settings.binding.alipay': 'Binding Alipay',
+  'app.settings.binding.alipay-description': 'Currently unbound Alipay account',
+  'app.settings.binding.dingding': 'Binding DingTalk',
+  'app.settings.binding.dingding-description': 'Currently unbound DingTalk account',
+  'app.settings.binding.bind': 'Bind',
+  'app.settings.notification.password': 'Account Password',
+  'app.settings.notification.password-description':
+    'Messages from other users will be notified in the form of a station letter',
+  'app.settings.notification.messages': 'System Messages',
+  'app.settings.notification.messages-description':
+    'System messages will be notified in the form of a station letter',
+  'app.settings.notification.todo': 'To-do Notification',
+  'app.settings.notification.todo-description':
+    'The to-do list will be notified in the form of a letter from the station',
+  'app.settings.open': 'Open',
+  'app.settings.close': 'Close',
+};

+ 23 - 0
src/locales/id-ID.ts

@@ -0,0 +1,23 @@
+import component from './id-ID/component';
+import globalHeader from './id-ID/globalHeader';
+import menu from './id-ID/menu';
+import pwa from './id-ID/pwa';
+import settingDrawer from './id-ID/settingDrawer';
+import settings from './id-ID/settings';
+
+export default {
+  'navbar.lang': 'Bahasa',
+  'layout.user.link.help': 'Bantuan',
+  'layout.user.link.privacy': 'Privasi',
+  'layout.user.link.terms': 'Ketentuan',
+  'app.preview.down.block': 'Unduh halaman ini dalam projek lokal anda',
+  'app.welcome.link.fetch-blocks': 'Dapatkan semua blok',
+  'app.welcome.link.block-list':
+    'Buat standar dengan cepat, halaman-halaman berdasarkan pengembangan `block`',
+  ...globalHeader,
+  ...menu,
+  ...settingDrawer,
+  ...settings,
+  ...pwa,
+  ...component,
+};

+ 5 - 0
src/locales/id-ID/component.ts

@@ -0,0 +1,5 @@
+export default {
+  'component.tagSelect.expand': 'Perluas',
+  'component.tagSelect.collapse': 'Lipat',
+  'component.tagSelect.all': 'Semua',
+};

+ 17 - 0
src/locales/id-ID/globalHeader.ts

@@ -0,0 +1,17 @@
+export default {
+  'component.globalHeader.search': 'Pencarian',
+  'component.globalHeader.search.example1': 'Contoh 1 Pencarian',
+  'component.globalHeader.search.example2': 'Contoh 2 Pencarian',
+  'component.globalHeader.search.example3': 'Contoh 3 Pencarian',
+  'component.globalHeader.help': 'Bantuan',
+  'component.globalHeader.notification': 'Notifikasi',
+  'component.globalHeader.notification.empty': 'Anda telah membaca semua notifikasi',
+  'component.globalHeader.message': 'Pesan',
+  'component.globalHeader.message.empty': 'Anda telah membaca semua pesan.',
+  'component.globalHeader.event': 'Acara',
+  'component.globalHeader.event.empty': 'Anda telah melihat semua acara.',
+  'component.noticeIcon.clear': 'Kosongkan',
+  'component.noticeIcon.cleared': 'Berhasil dikosongkan',
+  'component.noticeIcon.empty': 'Tidak ada pemberitahuan',
+  'component.noticeIcon.view-more': 'Melihat lebih',
+};

+ 52 - 0
src/locales/id-ID/menu.ts

@@ -0,0 +1,52 @@
+export default {
+  'menu.welcome': 'Selamat Datang',
+  'menu.more-blocks': 'Blocks Lainnya',
+  'menu.home': 'Halaman Awal',
+  'menu.admin': 'Admin',
+  'menu.admin.sub-page': 'Sub-Halaman',
+  'menu.login': 'Masuk',
+  'menu.register': 'Pendaftaran',
+  'menu.register.result': 'Hasil Pendaftaran',
+  'menu.dashboard': 'Dasbor',
+  'menu.dashboard.analysis': 'Analisis',
+  'menu.dashboard.monitor': 'Monitor',
+  'menu.dashboard.workplace': 'Workplace',
+  'menu.exception.403': '403',
+  'menu.exception.404': '404',
+  'menu.exception.500': '500',
+  'menu.form': 'Form',
+  'menu.form.basic-form': 'Form Dasar',
+  'menu.form.step-form': 'Form Bertahap',
+  'menu.form.step-form.info': 'Form Bertahap(menulis informasi yang dibagikan)',
+  'menu.form.step-form.confirm': 'Form Bertahap(konfirmasi informasi yang dibagikan)',
+  'menu.form.step-form.result': 'Form Bertahap(selesai)',
+  'menu.form.advanced-form': 'Form Lanjutan',
+  'menu.list': 'Daftar',
+  'menu.list.table-list': 'Tabel Pencarian',
+  'menu.list.basic-list': 'Daftar Dasar',
+  'menu.list.card-list': 'Daftar Kartu',
+  'menu.list.search-list': 'Daftar Pencarian',
+  'menu.list.search-list.articles': 'Daftar Pencarian(artikel)',
+  'menu.list.search-list.projects': 'Daftar Pencarian(projek)',
+  'menu.list.search-list.applications': 'Daftar Pencarian(aplikasi)',
+  'menu.profile': 'Profil',
+  'menu.profile.basic': 'Profil Dasar',
+  'menu.profile.advanced': 'Profile Lanjutan',
+  'menu.result': 'Hasil',
+  'menu.result.success': 'Sukses',
+  'menu.result.fail': 'Gagal',
+  'menu.exception': 'Pengecualian',
+  'menu.exception.not-permission': '403',
+  'menu.exception.not-find': '404',
+  'menu.exception.server-error': '500',
+  'menu.exception.trigger': 'Jalankan',
+  'menu.account': 'Akun',
+  'menu.account.center': 'Detail Akun',
+  'menu.account.settings': 'Pengaturan Akun',
+  'menu.account.trigger': 'Mengaktivasi Error',
+  'menu.account.logout': 'Keluar',
+  'menu.editor': 'Penyusun Grafis',
+  'menu.editor.flow': 'Penyusun Alur',
+  'menu.editor.mind': 'Penyusun Mind',
+  'menu.editor.koni': 'Penyusun Koni',
+};

+ 7 - 0
src/locales/id-ID/pwa.ts

@@ -0,0 +1,7 @@
+export default {
+  'app.pwa.offline': 'Koneksi anda terputus',
+  'app.pwa.serviceworker.updated': 'Konten baru sudah tersedia',
+  'app.pwa.serviceworker.updated.hint':
+    'Silahkan klik tombol "Refresh" untuk memuat ulang halaman ini',
+  'app.pwa.serviceworker.updated.ok': 'Memuat ulang',
+};

+ 32 - 0
src/locales/id-ID/settingDrawer.ts

@@ -0,0 +1,32 @@
+export default {
+  'app.setting.pagestyle': 'Pengaturan style Halaman',
+  'app.setting.pagestyle.dark': 'Style Gelap',
+  'app.setting.pagestyle.light': 'Style Cerah',
+  'app.setting.content-width': 'Lebar Konten',
+  'app.setting.content-width.fixed': 'Tetap',
+  'app.setting.content-width.fluid': 'Fluid',
+  'app.setting.themecolor': 'Theme Color',
+  'app.setting.themecolor.dust': 'Dust Red',
+  'app.setting.themecolor.volcano': 'Volcano',
+  'app.setting.themecolor.sunset': 'Sunset Orange',
+  'app.setting.themecolor.cyan': 'Cyan',
+  'app.setting.themecolor.green': 'Polar Green',
+  'app.setting.themecolor.daybreak': 'Daybreak Blue (bawaan)',
+  'app.setting.themecolor.geekblue': 'Geek Glue',
+  'app.setting.themecolor.purple': 'Golden Purple',
+  'app.setting.navigationmode': 'Mode Navigasi',
+  'app.setting.sidemenu': 'Susunan Menu Samping',
+  'app.setting.topmenu': 'Susunan Menu Atas',
+  'app.setting.fixedheader': 'Header Tetap',
+  'app.setting.fixedsidebar': 'Sidebar Tetap',
+  'app.setting.fixedsidebar.hint': 'Berjalan pada Susunan Menu Samping',
+  'app.setting.hideheader': 'Sembunyikan Header ketika gulir ke bawah',
+  'app.setting.hideheader.hint': 'Bekerja ketika Header tersembunyi dimunculkan', // 'app.setting.hideheader.hint': 'Works when Hidden Header is enabled',
+  'app.setting.othersettings': 'Pengaturan Lainnya',
+  'app.setting.weakmode': 'Mode Lemah',
+  'app.setting.copy': 'Salin Pengaturan',
+  'app.setting.copyinfo':
+    'Berhasil disalin,tolong ubah defaultSettings pada src/models/setting.js',
+  'app.setting.production.hint':
+    'Panel pengaturan hanya muncul pada lingkungan pengembangan, silahkan modifikasi secara menual',
+};

+ 60 - 0
src/locales/id-ID/settings.ts

@@ -0,0 +1,60 @@
+export default {
+  'app.settings.menuMap.basic': 'Pengaturan Dasar',
+  'app.settings.menuMap.security': 'Pengaturan Keamanan',
+  'app.settings.menuMap.binding': 'Pengikatan Akun',
+  'app.settings.menuMap.notification': 'Notifikasi Pesan Baru',
+  'app.settings.basic.avatar': 'Avatar',
+  'app.settings.basic.change-avatar': 'Ubah avatar',
+  'app.settings.basic.email': 'Email',
+  'app.settings.basic.email-message': 'Tolong masukkan email!',
+  'app.settings.basic.nickname': 'Nickname',
+  'app.settings.basic.nickname-message': 'Tolong masukkan Nickname!',
+  'app.settings.basic.profile': 'Profil Personal',
+  'app.settings.basic.profile-message': 'Tolong masukkan profil personal!',
+  'app.settings.basic.profile-placeholder': 'Perkenalan Singkat tentang Diri Anda',
+  'app.settings.basic.country': 'Negara/Wilayah',
+  'app.settings.basic.country-message': 'Tolong masukkan negara anda!',
+  'app.settings.basic.geographic': 'Provinsi atau kota',
+  'app.settings.basic.geographic-message': 'Tolong masukkan info geografis anda!',
+  'app.settings.basic.address': 'Alamat Jalan',
+  'app.settings.basic.address-message': 'Tolong masukkan Alamat Jalan anda!',
+  'app.settings.basic.phone': 'Nomor Ponsel',
+  'app.settings.basic.phone-message': 'Tolong masukkan Nomor Ponsel anda!',
+  'app.settings.basic.update': 'Perbarui Informasi',
+  'app.settings.security.strong': 'Kuat',
+  'app.settings.security.medium': 'Sedang',
+  'app.settings.security.weak': 'Lemah',
+  'app.settings.security.password': 'Kata Sandi Akun',
+  'app.settings.security.password-description': 'Kekuatan Kata Sandi saat ini',
+  'app.settings.security.phone': 'Keamanan Ponsel',
+  'app.settings.security.phone-description': 'Mengikat Ponsel',
+  'app.settings.security.question': 'Pertanyaan Keamanan',
+  'app.settings.security.question-description':
+    'Pertanyaan Keamanan belum diatur, dan kebijakan keamanan dapat melindungi akun secara efektif',
+  'app.settings.security.email': 'Email Cadangan',
+  'app.settings.security.email-description': 'Mengikat Email',
+  'app.settings.security.mfa': 'Perangka MFA',
+  'app.settings.security.mfa-description':
+    'Tidak mengikat Perangkat MFA, setelah diikat, dapat dikonfirmasi dua kali',
+  'app.settings.security.modify': 'Modifikasi',
+  'app.settings.security.set': 'Setel',
+  'app.settings.security.bind': 'Ikat',
+  'app.settings.binding.taobao': 'Mengikat Taobao',
+  'app.settings.binding.taobao-description': 'Tidak mengikat akun Taobao saat ini',
+  'app.settings.binding.alipay': 'Mengikat Alipay',
+  'app.settings.binding.alipay-description': 'Tidak mengikat akun Alipay saat ini',
+  'app.settings.binding.dingding': 'Mengikat DingTalk',
+  'app.settings.binding.dingding-description': 'Tidak mengikat akun DingTalk',
+  'app.settings.binding.bind': 'Ikat',
+  'app.settings.notification.password': 'Kata Sandi Akun',
+  'app.settings.notification.password-description':
+    'Pesan dari pengguna lain akan diberitahu dalam bentuk surat',
+  'app.settings.notification.messages': 'Pesan Sistem',
+  'app.settings.notification.messages-description':
+    'Pesan sistem akan diberitahu dalam bentuk surat',
+  'app.settings.notification.todo': 'Notifikasi daftar To-do',
+  'app.settings.notification.todo-description':
+    'Daftar to-do akan diberitahukan dalam bentuk surat dari stasiun',
+  'app.settings.open': 'Buka',
+  'app.settings.close': 'Tutup',
+};

+ 20 - 0
src/locales/pt-BR.ts

@@ -0,0 +1,20 @@
+import component from './pt-BR/component';
+import globalHeader from './pt-BR/globalHeader';
+import menu from './pt-BR/menu';
+import pwa from './pt-BR/pwa';
+import settingDrawer from './pt-BR/settingDrawer';
+import settings from './pt-BR/settings';
+
+export default {
+  'navBar.lang': 'Idiomas',
+  'layout.user.link.help': 'ajuda',
+  'layout.user.link.privacy': 'política de privacidade',
+  'layout.user.link.terms': 'termos de serviços',
+  'app.preview.down.block': 'Download this page to your local project',
+  ...globalHeader,
+  ...menu,
+  ...settingDrawer,
+  ...settings,
+  ...pwa,
+  ...component,
+};

+ 5 - 0
src/locales/pt-BR/component.ts

@@ -0,0 +1,5 @@
+export default {
+  'component.tagSelect.expand': 'Expandir',
+  'component.tagSelect.collapse': 'Diminuir',
+  'component.tagSelect.all': 'Todas',
+};

+ 18 - 0
src/locales/pt-BR/globalHeader.ts

@@ -0,0 +1,18 @@
+export default {
+  'component.globalHeader.search': 'Busca',
+  'component.globalHeader.search.example1': 'Exemplo de busca 1',
+  'component.globalHeader.search.example2': 'Exemplo de busca 2',
+  'component.globalHeader.search.example3': 'Exemplo de busca 3',
+  'component.globalHeader.help': 'Ajuda',
+  'component.globalHeader.notification': 'Notificação',
+  'component.globalHeader.notification.empty': 'Você visualizou todas as notificações.',
+  'component.globalHeader.message': 'Mensagem',
+  'component.globalHeader.message.empty': 'Você visualizou todas as mensagens.',
+  'component.globalHeader.event': 'Evento',
+  'component.globalHeader.event.empty': 'Você visualizou todos os eventos.',
+  'component.noticeIcon.clear': 'Limpar',
+  'component.noticeIcon.cleared': 'Limpo',
+  'component.noticeIcon.empty': 'Sem notificações',
+  'component.noticeIcon.loaded': 'Carregado',
+  'component.noticeIcon.view-more': 'Veja mais',
+};

+ 52 - 0
src/locales/pt-BR/menu.ts

@@ -0,0 +1,52 @@
+export default {
+  'menu.welcome': 'Welcome',
+  'menu.more-blocks': 'More Blocks',
+  'menu.home': 'Início',
+  'menu.login': 'Login',
+  'menu.admin': 'Admin',
+  'menu.admin.sub-page': 'Sub-Page',
+  'menu.register': 'Registro',
+  'menu.register.result': 'Resultado de registro',
+  'menu.dashboard': 'Dashboard',
+  'menu.dashboard.analysis': 'Análise',
+  'menu.dashboard.monitor': 'Monitor',
+  'menu.dashboard.workplace': 'Ambiente de Trabalho',
+  'menu.exception.403': '403',
+  'menu.exception.404': '404',
+  'menu.exception.500': '500',
+  'menu.form': 'Formulário',
+  'menu.form.basic-form': 'Formulário Básico',
+  'menu.form.step-form': 'Formulário Assistido',
+  'menu.form.step-form.info': 'Formulário Assistido(gravar informações de transferência)',
+  'menu.form.step-form.confirm': 'Formulário Assistido(confirmar informações de transferência)',
+  'menu.form.step-form.result': 'Formulário Assistido(finalizado)',
+  'menu.form.advanced-form': 'Formulário Avançado',
+  'menu.list': 'Lista',
+  'menu.list.table-list': 'Tabela de Busca',
+  'menu.list.basic-list': 'Lista Básica',
+  'menu.list.card-list': 'Lista de Card',
+  'menu.list.search-list': 'Lista de Busca',
+  'menu.list.search-list.articles': 'Lista de Busca(artigos)',
+  'menu.list.search-list.projects': 'Lista de Busca(projetos)',
+  'menu.list.search-list.applications': 'Lista de Busca(aplicações)',
+  'menu.profile': 'Perfil',
+  'menu.profile.basic': 'Perfil Básico',
+  'menu.profile.advanced': 'Perfil Avançado',
+  'menu.result': 'Resultado',
+  'menu.result.success': 'Sucesso',
+  'menu.result.fail': 'Falha',
+  'menu.exception': 'Exceção',
+  'menu.exception.not-permission': '403',
+  'menu.exception.not-find': '404',
+  'menu.exception.server-error': '500',
+  'menu.exception.trigger': 'Disparar',
+  'menu.account': 'Conta',
+  'menu.account.center': 'Central da Conta',
+  'menu.account.settings': 'Configurar Conta',
+  'menu.account.trigger': 'Disparar Erro',
+  'menu.account.logout': 'Sair',
+  'menu.editor': 'Graphic Editor',
+  'menu.editor.flow': 'Flow Editor',
+  'menu.editor.mind': 'Mind Editor',
+  'menu.editor.koni': 'Koni Editor',
+};

+ 7 - 0
src/locales/pt-BR/pwa.ts

@@ -0,0 +1,7 @@
+export default {
+  'app.pwa.offline': 'Você está offline agora',
+  'app.pwa.serviceworker.updated': 'Novo conteúdo está disponível',
+  'app.pwa.serviceworker.updated.hint':
+    'Por favor, pressione o botão "Atualizar" para recarregar a página atual',
+  'app.pwa.serviceworker.updated.ok': 'Atualizar',
+};

+ 32 - 0
src/locales/pt-BR/settingDrawer.ts

@@ -0,0 +1,32 @@
+export default {
+  'app.setting.pagestyle': 'Configuração de estilo da página',
+  'app.setting.pagestyle.dark': 'Dark style',
+  'app.setting.pagestyle.light': 'Light style',
+  'app.setting.content-width': 'Largura do conteúdo',
+  'app.setting.content-width.fixed': 'Fixo',
+  'app.setting.content-width.fluid': 'Fluido',
+  'app.setting.themecolor': 'Cor do Tema',
+  'app.setting.themecolor.dust': 'Dust Red',
+  'app.setting.themecolor.volcano': 'Volcano',
+  'app.setting.themecolor.sunset': 'Sunset Orange',
+  'app.setting.themecolor.cyan': 'Cyan',
+  'app.setting.themecolor.green': 'Polar Green',
+  'app.setting.themecolor.daybreak': 'Daybreak Blue (default)',
+  'app.setting.themecolor.geekblue': 'Geek Glue',
+  'app.setting.themecolor.purple': 'Golden Purple',
+  'app.setting.navigationmode': 'Modo de Navegação',
+  'app.setting.sidemenu': 'Layout do Menu Lateral',
+  'app.setting.topmenu': 'Layout do Menu Superior',
+  'app.setting.fixedheader': 'Cabeçalho fixo',
+  'app.setting.fixedsidebar': 'Barra lateral fixa',
+  'app.setting.fixedsidebar.hint': 'Funciona no layout do menu lateral',
+  'app.setting.hideheader': 'Esconder o cabeçalho quando rolar',
+  'app.setting.hideheader.hint': 'Funciona quando o esconder cabeçalho está abilitado',
+  'app.setting.othersettings': 'Outras configurações',
+  'app.setting.weakmode': 'Weak Mode',
+  'app.setting.copy': 'Copiar Configuração',
+  'app.setting.copyinfo':
+    'copiado com sucesso,por favor trocar o defaultSettings em src/models/setting.js',
+  'app.setting.production.hint':
+    'O painel de configuração apenas é exibido no ambiente de desenvolvimento, por favor modifique manualmente o',
+};

+ 60 - 0
src/locales/pt-BR/settings.ts

@@ -0,0 +1,60 @@
+export default {
+  'app.settings.menuMap.basic': 'Configurações Básicas',
+  'app.settings.menuMap.security': 'Configurações de Segurança',
+  'app.settings.menuMap.binding': 'Vinculação de Conta',
+  'app.settings.menuMap.notification': 'Mensagens de Notificação',
+  'app.settings.basic.avatar': 'Avatar',
+  'app.settings.basic.change-avatar': 'Alterar avatar',
+  'app.settings.basic.email': 'Email',
+  'app.settings.basic.email-message': 'Por favor insira seu email!',
+  'app.settings.basic.nickname': 'Nome de usuário',
+  'app.settings.basic.nickname-message': 'Por favor insira seu nome de usuário!',
+  'app.settings.basic.profile': 'Perfil pessoal',
+  'app.settings.basic.profile-message': 'Por favor insira seu perfil pessoal!',
+  'app.settings.basic.profile-placeholder': 'Breve introdução sua',
+  'app.settings.basic.country': 'País/Região',
+  'app.settings.basic.country-message': 'Por favor insira país!',
+  'app.settings.basic.geographic': 'Província, estado ou cidade',
+  'app.settings.basic.geographic-message': 'Por favor insira suas informações geográficas!',
+  'app.settings.basic.address': 'Endereço',
+  'app.settings.basic.address-message': 'Por favor insira seu endereço!',
+  'app.settings.basic.phone': 'Número de telefone',
+  'app.settings.basic.phone-message': 'Por favor insira seu número de telefone!',
+  'app.settings.basic.update': 'Atualizar Informações',
+  'app.settings.security.strong': 'Forte',
+  'app.settings.security.medium': 'Média',
+  'app.settings.security.weak': 'Fraca',
+  'app.settings.security.password': 'Senha da Conta',
+  'app.settings.security.password-description': 'Força da senha',
+  'app.settings.security.phone': 'Telefone de Seguraça',
+  'app.settings.security.phone-description': 'Telefone vinculado',
+  'app.settings.security.question': 'Pergunta de Segurança',
+  'app.settings.security.question-description':
+    'A pergunta de segurança não está definida e a política de segurança pode proteger efetivamente a segurança da conta',
+  'app.settings.security.email': 'Email de Backup',
+  'app.settings.security.email-description': 'Email vinculado',
+  'app.settings.security.mfa': 'Dispositivo MFA',
+  'app.settings.security.mfa-description':
+    'O dispositivo MFA não vinculado, após a vinculação, pode ser confirmado duas vezes',
+  'app.settings.security.modify': 'Modificar',
+  'app.settings.security.set': 'Atribuir',
+  'app.settings.security.bind': 'Vincular',
+  'app.settings.binding.taobao': 'Vincular Taobao',
+  'app.settings.binding.taobao-description': 'Atualmente não vinculado à conta Taobao',
+  'app.settings.binding.alipay': 'Vincular Alipay',
+  'app.settings.binding.alipay-description': 'Atualmente não vinculado à conta Alipay',
+  'app.settings.binding.dingding': 'Vincular DingTalk',
+  'app.settings.binding.dingding-description': 'Atualmente não vinculado à conta DingTalk',
+  'app.settings.binding.bind': 'Vincular',
+  'app.settings.notification.password': 'Senha da Conta',
+  'app.settings.notification.password-description':
+    'Mensagens de outros usuários serão notificadas na forma de uma estação de letra',
+  'app.settings.notification.messages': 'Mensagens de Sistema',
+  'app.settings.notification.messages-description':
+    'Mensagens de sistema serão notificadas na forma de uma estação de letra',
+  'app.settings.notification.todo': 'Notificação de To-do',
+  'app.settings.notification.todo-description':
+    'A lista de to-do será notificada na forma de uma estação de letra',
+  'app.settings.open': 'Aberto',
+  'app.settings.close': 'Fechado',
+};

+ 24 - 0
src/locales/zh-CN.ts

@@ -0,0 +1,24 @@
+import component from './zh-CN/component';
+import globalHeader from './zh-CN/globalHeader';
+import menu from './zh-CN/menu';
+import pwa from './zh-CN/pwa';
+import settingDrawer from './zh-CN/settingDrawer';
+import settings from './zh-CN/settings';
+import pages from './zh-CN/pages';
+
+export default {
+  'navBar.lang': '语言',
+  'layout.user.link.help': '帮助',
+  'layout.user.link.privacy': '隐私',
+  'layout.user.link.terms': '条款',
+  'app.preview.down.block': '下载此页面到本地项目',
+  'app.welcome.link.fetch-blocks': '获取全部区块',
+  'app.welcome.link.block-list': '基于 block 开发,快速构建标准页面',
+  ...pages,
+  ...globalHeader,
+  ...menu,
+  ...settingDrawer,
+  ...settings,
+  ...pwa,
+  ...component,
+};

+ 5 - 0
src/locales/zh-CN/component.ts

@@ -0,0 +1,5 @@
+export default {
+  'component.tagSelect.expand': '展开',
+  'component.tagSelect.collapse': '收起',
+  'component.tagSelect.all': '全部',
+};

+ 17 - 0
src/locales/zh-CN/globalHeader.ts

@@ -0,0 +1,17 @@
+export default {
+  'component.globalHeader.search': '站内搜索',
+  'component.globalHeader.search.example1': '搜索提示一',
+  'component.globalHeader.search.example2': '搜索提示二',
+  'component.globalHeader.search.example3': '搜索提示三',
+  'component.globalHeader.help': '使用文档',
+  'component.globalHeader.notification': '通知',
+  'component.globalHeader.notification.empty': '你已查看所有通知',
+  'component.globalHeader.message': '消息',
+  'component.globalHeader.message.empty': '您已读完所有消息',
+  'component.globalHeader.event': '待办',
+  'component.globalHeader.event.empty': '你已完成所有待办',
+  'component.noticeIcon.clear': '清空',
+  'component.noticeIcon.cleared': '清空了',
+  'component.noticeIcon.empty': '暂无数据',
+  'component.noticeIcon.view-more': '查看更多',
+};

+ 52 - 0
src/locales/zh-CN/menu.ts

@@ -0,0 +1,52 @@
+export default {
+  'menu.welcome': '欢迎',
+  'menu.more-blocks': '更多区块',
+  'menu.home': '首页',
+  'menu.admin': '管理页',
+  'menu.admin.sub-page': '二级管理页',
+  'menu.login': '登录',
+  'menu.register': '注册',
+  'menu.register.result': '注册结果',
+  'menu.dashboard': 'Dashboard',
+  'menu.dashboard.analysis': '分析页',
+  'menu.dashboard.monitor': '监控页',
+  'menu.dashboard.workplace': '工作台',
+  'menu.exception.403': '403',
+  'menu.exception.404': '404',
+  'menu.exception.500': '500',
+  'menu.form': '表单页',
+  'menu.form.basic-form': '基础表单',
+  'menu.form.step-form': '分步表单',
+  'menu.form.step-form.info': '分步表单(填写转账信息)',
+  'menu.form.step-form.confirm': '分步表单(确认转账信息)',
+  'menu.form.step-form.result': '分步表单(完成)',
+  'menu.form.advanced-form': '高级表单',
+  'menu.list': '列表页',
+  'menu.list.table-list': '查询表格',
+  'menu.list.basic-list': '标准列表',
+  'menu.list.card-list': '卡片列表',
+  'menu.list.search-list': '搜索列表',
+  'menu.list.search-list.articles': '搜索列表(文章)',
+  'menu.list.search-list.projects': '搜索列表(项目)',
+  'menu.list.search-list.applications': '搜索列表(应用)',
+  'menu.profile': '详情页',
+  'menu.profile.basic': '基础详情页',
+  'menu.profile.advanced': '高级详情页',
+  'menu.result': '结果页',
+  'menu.result.success': '成功页',
+  'menu.result.fail': '失败页',
+  'menu.exception': '异常页',
+  'menu.exception.not-permission': '403',
+  'menu.exception.not-find': '404',
+  'menu.exception.server-error': '500',
+  'menu.exception.trigger': '触发错误',
+  'menu.account': '个人页',
+  'menu.account.center': '个人中心',
+  'menu.account.settings': '个人设置',
+  'menu.account.trigger': '触发报错',
+  'menu.account.logout': '退出登录',
+  'menu.editor': '图形编辑器',
+  'menu.editor.flow': '流程编辑器',
+  'menu.editor.mind': '脑图编辑器',
+  'menu.editor.koni': '拓扑编辑器',
+};

+ 65 - 0
src/locales/zh-CN/pages.ts

@@ -0,0 +1,65 @@
+export default {
+  'pages.layouts.userLayout.title': 'Ant Design 是西湖区最具影响力的 Web 设计规范',
+  'pages.login.accountLogin.tab': '账户密码登录',
+  'pages.login.accountLogin.errorMessage': '错误的用户名和密码(admin/ant.design)',
+  'pages.login.username.placeholder': '用户名: admin or user',
+  'pages.login.username.required': '用户名是必填项!',
+  'pages.login.password.placeholder': '密码: ant.design',
+  'pages.login.password.required': '密码是必填项!',
+  'pages.login.phoneLogin.tab': '手机号登录',
+  'pages.login.phoneLogin.errorMessage': '验证码错误',
+  'pages.login.phoneNumber.placeholder': '请输入手机号!',
+  'pages.login.phoneNumber.required': '手机号是必填项!',
+  'pages.login.phoneNumber.invalid': '不合法的手机号!',
+  'pages.login.captcha.placeholder': '请输入验证码!',
+  'pages.login.captcha.required': '验证码是必填项!',
+  'pages.login.phoneLogin.getVerificationCode': '获取验证码',
+  'pages.getCaptchaSecondText': '秒后重新获取',
+  'pages.login.rememberMe': '自动登录',
+  'pages.login.forgotPassword': '忘记密码 ?',
+  'pages.login.submit': '登录',
+  'pages.login.loginWith': '其他登录方式 :',
+  'pages.login.registerAccount': '注册账户',
+  'pages.welcome.advancedComponent': '高级表格',
+  'pages.welcome.link': '欢迎使用',
+  'pages.welcome.advancedLayout': '高级布局',
+  'pages.welcome.alertMessage': '更快更强的重型组件,已经发布。',
+  'pages.admin.subPage.title': ' 这个页面只有 admin 权限才能查看',
+  'pages.admin.subPage.alertMessage': 'umi ui 现已发布,欢迎使用 npm run ui 启动体验。',
+  'pages.searchTable.createForm.newRule': '新建规则',
+  'pages.searchTable.updateForm.ruleConfig': '规则配置',
+  'pages.searchTable.updateForm.basicConfig': '基本信息',
+  'pages.searchTable.updateForm.ruleName.nameLabel': '规则名称',
+  'pages.searchTable.updateForm.ruleName.nameRules': '请输入规则名称!',
+  'pages.searchTable.updateForm.ruleDesc.descLabel': '规则描述',
+  'pages.searchTable.updateForm.ruleDesc.descPlaceholder': '请输入至少五个字符',
+  'pages.searchTable.updateForm.ruleDesc.descRules': '请输入至少五个字符的规则描述!',
+  'pages.searchTable.updateForm.ruleProps.title': '配置规则属性',
+  'pages.searchTable.updateForm.object': '监控对象',
+  'pages.searchTable.updateForm.ruleProps.templateLabel': '规则模板',
+  'pages.searchTable.updateForm.ruleProps.typeLabel': '规则类型',
+  'pages.searchTable.updateForm.schedulingPeriod.title': '设定调度周期',
+  'pages.searchTable.updateForm.schedulingPeriod.timeLabel': '开始时间',
+  'pages.searchTable.updateForm.schedulingPeriod.timeRules': '请选择开始时间!',
+  'pages.searchTable.titleDesc': '描述',
+  'pages.searchTable.ruleName': '规则名称为必填项',
+  'pages.searchTable.titleCallNo': '服务调用次数',
+  'pages.searchTable.titleStatus': '状态',
+  'pages.searchTable.nameStatus.default': '关闭',
+  'pages.searchTable.nameStatus.running': '运行中',
+  'pages.searchTable.nameStatus.online': '已上线',
+  'pages.searchTable.nameStatus.abnormal': '异常',
+  'pages.searchTable.titleUpdatedAt': '上次调度时间',
+  'pages.searchTable.exception': '请输入异常原因!',
+  'pages.searchTable.titleOption': '操作',
+  'pages.searchTable.config': '配置',
+  'pages.searchTable.subscribeAlert': '订阅警报',
+  'pages.searchTable.title': '查询表格',
+  'pages.searchTable.new': '新建',
+  'pages.searchTable.chosen': '已选择',
+  'pages.searchTable.item': '项',
+  'pages.searchTable.totalServiceCalls': '服务调用次数总计',
+  'pages.searchTable.tenThousand': '万',
+  'pages.searchTable.batchDeletion': '批量删除',
+  'pages.searchTable.batchApproval': '批量审批',
+};

+ 6 - 0
src/locales/zh-CN/pwa.ts

@@ -0,0 +1,6 @@
+export default {
+  'app.pwa.offline': '当前处于离线状态',
+  'app.pwa.serviceworker.updated': '有新内容',
+  'app.pwa.serviceworker.updated.hint': '请点击“刷新”按钮或者手动刷新页面',
+  'app.pwa.serviceworker.updated.ok': '刷新',
+};

+ 31 - 0
src/locales/zh-CN/settingDrawer.ts

@@ -0,0 +1,31 @@
+export default {
+  'app.setting.pagestyle': '整体风格设置',
+  'app.setting.pagestyle.dark': '暗色菜单风格',
+  'app.setting.pagestyle.light': '亮色菜单风格',
+  'app.setting.content-width': '内容区域宽度',
+  'app.setting.content-width.fixed': '定宽',
+  'app.setting.content-width.fluid': '流式',
+  'app.setting.themecolor': '主题色',
+  'app.setting.themecolor.dust': '薄暮',
+  'app.setting.themecolor.volcano': '火山',
+  'app.setting.themecolor.sunset': '日暮',
+  'app.setting.themecolor.cyan': '明青',
+  'app.setting.themecolor.green': '极光绿',
+  'app.setting.themecolor.daybreak': '拂晓蓝(默认)',
+  'app.setting.themecolor.geekblue': '极客蓝',
+  'app.setting.themecolor.purple': '酱紫',
+  'app.setting.navigationmode': '导航模式',
+  'app.setting.sidemenu': '侧边菜单布局',
+  'app.setting.topmenu': '顶部菜单布局',
+  'app.setting.fixedheader': '固定 Header',
+  'app.setting.fixedsidebar': '固定侧边菜单',
+  'app.setting.fixedsidebar.hint': '侧边菜单布局时可配置',
+  'app.setting.hideheader': '下滑时隐藏 Header',
+  'app.setting.hideheader.hint': '固定 Header 时可配置',
+  'app.setting.othersettings': '其他设置',
+  'app.setting.weakmode': '色弱模式',
+  'app.setting.copy': '拷贝设置',
+  'app.setting.copyinfo': '拷贝成功,请到 src/defaultSettings.js 中替换默认配置',
+  'app.setting.production.hint':
+    '配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改配置文件',
+};

+ 55 - 0
src/locales/zh-CN/settings.ts

@@ -0,0 +1,55 @@
+export default {
+  'app.settings.menuMap.basic': '基本设置',
+  'app.settings.menuMap.security': '安全设置',
+  'app.settings.menuMap.binding': '账号绑定',
+  'app.settings.menuMap.notification': '新消息通知',
+  'app.settings.basic.avatar': '头像',
+  'app.settings.basic.change-avatar': '更换头像',
+  'app.settings.basic.email': '邮箱',
+  'app.settings.basic.email-message': '请输入您的邮箱!',
+  'app.settings.basic.nickname': '昵称',
+  'app.settings.basic.nickname-message': '请输入您的昵称!',
+  'app.settings.basic.profile': '个人简介',
+  'app.settings.basic.profile-message': '请输入个人简介!',
+  'app.settings.basic.profile-placeholder': '个人简介',
+  'app.settings.basic.country': '国家/地区',
+  'app.settings.basic.country-message': '请输入您的国家或地区!',
+  'app.settings.basic.geographic': '所在省市',
+  'app.settings.basic.geographic-message': '请输入您的所在省市!',
+  'app.settings.basic.address': '街道地址',
+  'app.settings.basic.address-message': '请输入您的街道地址!',
+  'app.settings.basic.phone': '联系电话',
+  'app.settings.basic.phone-message': '请输入您的联系电话!',
+  'app.settings.basic.update': '更新基本信息',
+  'app.settings.security.strong': '强',
+  'app.settings.security.medium': '中',
+  'app.settings.security.weak': '弱',
+  'app.settings.security.password': '账户密码',
+  'app.settings.security.password-description': '当前密码强度',
+  'app.settings.security.phone': '密保手机',
+  'app.settings.security.phone-description': '已绑定手机',
+  'app.settings.security.question': '密保问题',
+  'app.settings.security.question-description': '未设置密保问题,密保问题可有效保护账户安全',
+  'app.settings.security.email': '备用邮箱',
+  'app.settings.security.email-description': '已绑定邮箱',
+  'app.settings.security.mfa': 'MFA 设备',
+  'app.settings.security.mfa-description': '未绑定 MFA 设备,绑定后,可以进行二次确认',
+  'app.settings.security.modify': '修改',
+  'app.settings.security.set': '设置',
+  'app.settings.security.bind': '绑定',
+  'app.settings.binding.taobao': '绑定淘宝',
+  'app.settings.binding.taobao-description': '当前未绑定淘宝账号',
+  'app.settings.binding.alipay': '绑定支付宝',
+  'app.settings.binding.alipay-description': '当前未绑定支付宝账号',
+  'app.settings.binding.dingding': '绑定钉钉',
+  'app.settings.binding.dingding-description': '当前未绑定钉钉账号',
+  'app.settings.binding.bind': '绑定',
+  'app.settings.notification.password': '账户密码',
+  'app.settings.notification.password-description': '其他用户的消息将以站内信的形式通知',
+  'app.settings.notification.messages': '系统消息',
+  'app.settings.notification.messages-description': '系统消息将以站内信的形式通知',
+  'app.settings.notification.todo': '待办任务',
+  'app.settings.notification.todo-description': '待办任务将以站内信的形式通知',
+  'app.settings.open': '开',
+  'app.settings.close': '关',
+};

+ 20 - 0
src/locales/zh-TW.ts

@@ -0,0 +1,20 @@
+import component from './zh-TW/component';
+import globalHeader from './zh-TW/globalHeader';
+import menu from './zh-TW/menu';
+import pwa from './zh-TW/pwa';
+import settingDrawer from './zh-TW/settingDrawer';
+import settings from './zh-TW/settings';
+
+export default {
+  'navBar.lang': '語言',
+  'layout.user.link.help': '幫助',
+  'layout.user.link.privacy': '隱私',
+  'layout.user.link.terms': '條款',
+  'app.preview.down.block': '下載此頁面到本地項目',
+  ...globalHeader,
+  ...menu,
+  ...settingDrawer,
+  ...settings,
+  ...pwa,
+  ...component,
+};

+ 5 - 0
src/locales/zh-TW/component.ts

@@ -0,0 +1,5 @@
+export default {
+  'component.tagSelect.expand': '展開',
+  'component.tagSelect.collapse': '收起',
+  'component.tagSelect.all': '全部',
+};

+ 17 - 0
src/locales/zh-TW/globalHeader.ts

@@ -0,0 +1,17 @@
+export default {
+  'component.globalHeader.search': '站內搜索',
+  'component.globalHeader.search.example1': '搜索提示壹',
+  'component.globalHeader.search.example2': '搜索提示二',
+  'component.globalHeader.search.example3': '搜索提示三',
+  'component.globalHeader.help': '使用手冊',
+  'component.globalHeader.notification': '通知',
+  'component.globalHeader.notification.empty': '妳已查看所有通知',
+  'component.globalHeader.message': '消息',
+  'component.globalHeader.message.empty': '您已讀完所有消息',
+  'component.globalHeader.event': '待辦',
+  'component.globalHeader.event.empty': '妳已完成所有待辦',
+  'component.noticeIcon.clear': '清空',
+  'component.noticeIcon.cleared': '清空了',
+  'component.noticeIcon.empty': '暫無資料',
+  'component.noticeIcon.view-more': '查看更多',
+};

+ 52 - 0
src/locales/zh-TW/menu.ts

@@ -0,0 +1,52 @@
+export default {
+  'menu.welcome': '歡迎',
+  'menu.more-blocks': '更多區塊',
+  'menu.home': '首頁',
+  'menu.login': '登錄',
+  'menu.admin': '权限',
+  'menu.admin.sub-page': '二级管理页',
+  'menu.exception.403': '403',
+  'menu.exception.404': '404',
+  'menu.exception.500': '500',
+  'menu.register': '註冊',
+  'menu.register.result': '註冊結果',
+  'menu.dashboard': 'Dashboard',
+  'menu.dashboard.analysis': '分析頁',
+  'menu.dashboard.monitor': '監控頁',
+  'menu.dashboard.workplace': '工作臺',
+  'menu.form': '表單頁',
+  'menu.form.basic-form': '基礎表單',
+  'menu.form.step-form': '分步表單',
+  'menu.form.step-form.info': '分步表單(填寫轉賬信息)',
+  'menu.form.step-form.confirm': '分步表單(確認轉賬信息)',
+  'menu.form.step-form.result': '分步表單(完成)',
+  'menu.form.advanced-form': '高級表單',
+  'menu.list': '列表頁',
+  'menu.list.table-list': '查詢表格',
+  'menu.list.basic-list': '標淮列表',
+  'menu.list.card-list': '卡片列表',
+  'menu.list.search-list': '搜索列表',
+  'menu.list.search-list.articles': '搜索列表(文章)',
+  'menu.list.search-list.projects': '搜索列表(項目)',
+  'menu.list.search-list.applications': '搜索列表(應用)',
+  'menu.profile': '詳情頁',
+  'menu.profile.basic': '基礎詳情頁',
+  'menu.profile.advanced': '高級詳情頁',
+  'menu.result': '結果頁',
+  'menu.result.success': '成功頁',
+  'menu.result.fail': '失敗頁',
+  'menu.account': '個人頁',
+  'menu.account.center': '個人中心',
+  'menu.account.settings': '個人設置',
+  'menu.account.trigger': '觸發報錯',
+  'menu.account.logout': '退出登錄',
+  'menu.exception': '异常页',
+  'menu.exception.not-permission': '403',
+  'menu.exception.not-find': '404',
+  'menu.exception.server-error': '500',
+  'menu.exception.trigger': '触发错误',
+  'menu.editor': '圖形編輯器',
+  'menu.editor.flow': '流程編輯器',
+  'menu.editor.mind': '腦圖編輯器',
+  'menu.editor.koni': '拓撲編輯器',
+};

+ 6 - 0
src/locales/zh-TW/pwa.ts

@@ -0,0 +1,6 @@
+export default {
+  'app.pwa.offline': '當前處於離線狀態',
+  'app.pwa.serviceworker.updated': '有新內容',
+  'app.pwa.serviceworker.updated.hint': '請點擊“刷新”按鈕或者手動刷新頁面',
+  'app.pwa.serviceworker.updated.ok': '刷新',
+};

+ 31 - 0
src/locales/zh-TW/settingDrawer.ts

@@ -0,0 +1,31 @@
+export default {
+  'app.setting.pagestyle': '整體風格設置',
+  'app.setting.pagestyle.dark': '暗色菜單風格',
+  'app.setting.pagestyle.light': '亮色菜單風格',
+  'app.setting.content-width': '內容區域寬度',
+  'app.setting.content-width.fixed': '定寬',
+  'app.setting.content-width.fluid': '流式',
+  'app.setting.themecolor': '主題色',
+  'app.setting.themecolor.dust': '薄暮',
+  'app.setting.themecolor.volcano': '火山',
+  'app.setting.themecolor.sunset': '日暮',
+  'app.setting.themecolor.cyan': '明青',
+  'app.setting.themecolor.green': '極光綠',
+  'app.setting.themecolor.daybreak': '拂曉藍(默認)',
+  'app.setting.themecolor.geekblue': '極客藍',
+  'app.setting.themecolor.purple': '醬紫',
+  'app.setting.navigationmode': '導航模式',
+  'app.setting.sidemenu': '側邊菜單布局',
+  'app.setting.topmenu': '頂部菜單布局',
+  'app.setting.fixedheader': '固定 Header',
+  'app.setting.fixedsidebar': '固定側邊菜單',
+  'app.setting.fixedsidebar.hint': '側邊菜單布局時可配置',
+  'app.setting.hideheader': '下滑時隱藏 Header',
+  'app.setting.hideheader.hint': '固定 Header 時可配置',
+  'app.setting.othersettings': '其他設置',
+  'app.setting.weakmode': '色弱模式',
+  'app.setting.copy': '拷貝設置',
+  'app.setting.copyinfo': '拷貝成功,請到 src/defaultSettings.js 中替換默認配置',
+  'app.setting.production.hint':
+    '配置欄只在開發環境用於預覽,生產環境不會展現,請拷貝後手動修改配置文件',
+};

+ 55 - 0
src/locales/zh-TW/settings.ts

@@ -0,0 +1,55 @@
+export default {
+  'app.settings.menuMap.basic': '基本設置',
+  'app.settings.menuMap.security': '安全設置',
+  'app.settings.menuMap.binding': '賬號綁定',
+  'app.settings.menuMap.notification': '新消息通知',
+  'app.settings.basic.avatar': '頭像',
+  'app.settings.basic.change-avatar': '更換頭像',
+  'app.settings.basic.email': '郵箱',
+  'app.settings.basic.email-message': '請輸入您的郵箱!',
+  'app.settings.basic.nickname': '昵稱',
+  'app.settings.basic.nickname-message': '請輸入您的昵稱!',
+  'app.settings.basic.profile': '個人簡介',
+  'app.settings.basic.profile-message': '請輸入個人簡介!',
+  'app.settings.basic.profile-placeholder': '個人簡介',
+  'app.settings.basic.country': '國家/地區',
+  'app.settings.basic.country-message': '請輸入您的國家或地區!',
+  'app.settings.basic.geographic': '所在省市',
+  'app.settings.basic.geographic-message': '請輸入您的所在省市!',
+  'app.settings.basic.address': '街道地址',
+  'app.settings.basic.address-message': '請輸入您的街道地址!',
+  'app.settings.basic.phone': '聯系電話',
+  'app.settings.basic.phone-message': '請輸入您的聯系電話!',
+  'app.settings.basic.update': '更新基本信息',
+  'app.settings.security.strong': '強',
+  'app.settings.security.medium': '中',
+  'app.settings.security.weak': '弱',
+  'app.settings.security.password': '賬戶密碼',
+  'app.settings.security.password-description': '當前密碼強度',
+  'app.settings.security.phone': '密保手機',
+  'app.settings.security.phone-description': '已綁定手機',
+  'app.settings.security.question': '密保問題',
+  'app.settings.security.question-description': '未設置密保問題,密保問題可有效保護賬戶安全',
+  'app.settings.security.email': '備用郵箱',
+  'app.settings.security.email-description': '已綁定郵箱',
+  'app.settings.security.mfa': 'MFA 設備',
+  'app.settings.security.mfa-description': '未綁定 MFA 設備,綁定後,可以進行二次確認',
+  'app.settings.security.modify': '修改',
+  'app.settings.security.set': '設置',
+  'app.settings.security.bind': '綁定',
+  'app.settings.binding.taobao': '綁定淘寶',
+  'app.settings.binding.taobao-description': '當前未綁定淘寶賬號',
+  'app.settings.binding.alipay': '綁定支付寶',
+  'app.settings.binding.alipay-description': '當前未綁定支付寶賬號',
+  'app.settings.binding.dingding': '綁定釘釘',
+  'app.settings.binding.dingding-description': '當前未綁定釘釘賬號',
+  'app.settings.binding.bind': '綁定',
+  'app.settings.notification.password': '賬戶密碼',
+  'app.settings.notification.password-description': '其他用戶的消息將以站內信的形式通知',
+  'app.settings.notification.messages': '系統消息',
+  'app.settings.notification.messages-description': '系統消息將以站內信的形式通知',
+  'app.settings.notification.todo': '待辦任務',
+  'app.settings.notification.todo-description': '待辦任務將以站內信的形式通知',
+  'app.settings.open': '開',
+  'app.settings.close': '關',
+};

+ 22 - 0
src/manifest.json

@@ -0,0 +1,22 @@
+{
+  "name": "Ant Design Pro",
+  "short_name": "Ant Design Pro",
+  "display": "standalone",
+  "start_url": "./?utm_source=homescreen",
+  "theme_color": "#002140",
+  "background_color": "#001529",
+  "icons": [
+    {
+      "src": "icons/icon-192x192.png",
+      "sizes": "192x192"
+    },
+    {
+      "src": "icons/icon-128x128.png",
+      "sizes": "128x128"
+    },
+    {
+      "src": "icons/icon-512x512.png",
+      "sizes": "512x512"
+    }
+  ]
+}

+ 18 - 0
src/pages/404.tsx

@@ -0,0 +1,18 @@
+import { Button, Result } from 'antd';
+import React from 'react';
+import { history } from 'umi';
+
+const NoFoundPage: React.FC<{}> = () => (
+  <Result
+    status="404"
+    title="404"
+    subTitle="Sorry, the page you visited does not exist."
+    extra={
+      <Button type="primary" onClick={() => history.push('/')}>
+        Back Home
+      </Button>
+    }
+  />
+);
+
+export default NoFoundPage;

+ 43 - 0
src/pages/Admin.tsx

@@ -0,0 +1,43 @@
+import React from 'react';
+import { HeartTwoTone, SmileTwoTone } from '@ant-design/icons';
+import { Card, Typography, Alert } from 'antd';
+import { PageHeaderWrapper } from '@ant-design/pro-layout';
+import { useIntl } from 'umi';
+
+export default (): React.ReactNode => {
+  const intl = useIntl();
+  return (
+    <PageHeaderWrapper
+      content={intl.formatMessage({
+        id: 'pages.admin.subPage.title',
+        defaultMessage: ' 这个页面只有 admin 权限才能查看',
+      })}
+    >
+      <Card>
+        <Alert
+          message={intl.formatMessage({
+            id: 'pages.welcome.alertMessage',
+            defaultMessage: '更快更强的重型组件,已经发布。',
+          })}
+          type="success"
+          showIcon
+          banner
+          style={{
+            margin: -12,
+            marginBottom: 48,
+          }}
+        />
+        <Typography.Title level={2} style={{ textAlign: 'center' }}>
+          <SmileTwoTone /> Ant Design Pro <HeartTwoTone twoToneColor="#eb2f96" /> You
+        </Typography.Title>
+      </Card>
+      <p style={{ textAlign: 'center', marginTop: 24 }}>
+        Want to add more pages? Please refer to{' '}
+        <a href="https://pro.ant.design/docs/block-cn" target="_blank" rel="noopener noreferrer">
+          use block
+        </a>
+        。
+      </p>
+    </PageHeaderWrapper>
+  );
+};

+ 30 - 0
src/pages/ListTableList/components/CreateForm.tsx

@@ -0,0 +1,30 @@
+import React from 'react';
+import { Modal } from 'antd';
+import { useIntl } from 'umi';
+
+interface CreateFormProps {
+  modalVisible: boolean;
+  onCancel: () => void;
+}
+
+const CreateForm: React.FC<CreateFormProps> = (props) => {
+  const { modalVisible, onCancel } = props;
+  const intl = useIntl();
+
+  return (
+    <Modal
+      destroyOnClose
+      title={intl.formatMessage({
+        id: 'pages.searchTable.createForm.newRule',
+        defaultMessage: '新建规则',
+      })}
+      visible={modalVisible}
+      onCancel={() => onCancel()}
+      footer={null}
+    >
+      {props.children}
+    </Modal>
+  );
+};
+
+export default CreateForm;

+ 209 - 0
src/pages/ListTableList/components/UpdateForm.tsx

@@ -0,0 +1,209 @@
+import React from 'react';
+import { Modal } from 'antd';
+import {
+  ProFormSelect,
+  ProFormText,
+  ProFormTextArea,
+  StepsForm,
+  ProFormRadio,
+  ProFormDateTimePicker,
+} from '@ant-design/pro-form';
+import { useIntl, FormattedMessage } from 'umi';
+
+import { TableListItem } from '../data.d';
+
+export interface FormValueType extends Partial<TableListItem> {
+  target?: string;
+  template?: string;
+  type?: string;
+  time?: string;
+  frequency?: string;
+}
+
+export interface UpdateFormProps {
+  onCancel: (flag?: boolean, formVals?: FormValueType) => void;
+  onSubmit: (values: FormValueType) => Promise<void>;
+  updateModalVisible: boolean;
+  values: Partial<TableListItem>;
+}
+
+const UpdateForm: React.FC<UpdateFormProps> = (props) => {
+  const intl = useIntl();
+  return (
+    <StepsForm
+      stepsProps={{
+        size: 'small',
+      }}
+      stepsFormRender={(dom, submitter) => {
+        return (
+          <Modal
+            width={640}
+            bodyStyle={{ padding: '32px 40px 48px' }}
+            destroyOnClose
+            title={intl.formatMessage({
+              id: 'pages.searchTable.updateForm.ruleConfig',
+              defaultMessage: '规则配置',
+            })}
+            visible={props.updateModalVisible}
+            footer={submitter}
+            onCancel={() => props.onCancel()}
+          >
+            {dom}
+          </Modal>
+        );
+      }}
+      onFinish={props.onSubmit}
+    >
+      <StepsForm.StepForm
+        initialValues={{
+          name: props.values.name,
+          desc: props.values.desc,
+        }}
+        title={intl.formatMessage({
+          id: 'pages.searchTable.updateForm.basicConfig',
+          defaultMessage: '基本信息',
+        })}
+      >
+        <ProFormText
+          name="name"
+          label={intl.formatMessage({
+            id: 'pages.searchTable.updateForm.ruleName.nameLabel',
+            defaultMessage: '规则名称',
+          })}
+          width="m"
+          rules={[
+            {
+              required: true,
+              message: (
+                <FormattedMessage
+                  id="pages.searchTable.updateForm.ruleName.nameRules"
+                  defaultMessage="请输入规则名称!"
+                />
+              ),
+            },
+          ]}
+        />
+        <ProFormTextArea
+          name="desc"
+          width="m"
+          label={intl.formatMessage({
+            id: 'pages.searchTable.updateForm.ruleDesc.descLabel',
+            defaultMessage: '规则描述',
+          })}
+          placeholder={intl.formatMessage({
+            id: 'pages.searchTable.updateForm.ruleDesc.descPlaceholder',
+            defaultMessage: '请输入至少五个字符',
+          })}
+          rules={[
+            {
+              required: true,
+              message: (
+                <FormattedMessage
+                  id="pages.searchTable.updateForm.ruleDesc.descRules"
+                  defaultMessage="请输入至少五个字符的规则描述!"
+                />
+              ),
+              min: 5,
+            },
+          ]}
+        />
+      </StepsForm.StepForm>
+      <StepsForm.StepForm
+        initialValues={{
+          target: '0',
+          template: '0',
+        }}
+        title={intl.formatMessage({
+          id: 'pages.searchTable.updateForm.ruleProps.title',
+          defaultMessage: '配置规则属性',
+        })}
+      >
+        <ProFormSelect
+          name="target"
+          width="m"
+          label={intl.formatMessage({
+            id: 'pages.searchTable.updateForm.object',
+            defaultMessage: '监控对象',
+          })}
+          valueEnum={{
+            0: '表一',
+            1: '表二',
+          }}
+        />
+        <ProFormSelect
+          name="template"
+          width="m"
+          label={intl.formatMessage({
+            id: 'pages.searchTable.updateForm.ruleProps.templateLabel',
+            defaultMessage: '规则模板',
+          })}
+          valueEnum={{
+            0: '规则模板一',
+            1: '规则模板二',
+          }}
+        />
+        <ProFormRadio.Group
+          name="type"
+          label={intl.formatMessage({
+            id: 'pages.searchTable.updateForm.ruleProps.typeLabel',
+            defaultMessage: '规则类型',
+          })}
+          options={[
+            {
+              value: '0',
+              label: '强',
+            },
+            {
+              value: '1',
+              label: '弱',
+            },
+          ]}
+        />
+      </StepsForm.StepForm>
+      <StepsForm.StepForm
+        initialValues={{
+          type: '1',
+          frequency: 'month',
+        }}
+        title={intl.formatMessage({
+          id: 'pages.searchTable.updateForm.schedulingPeriod.title',
+          defaultMessage: '设定调度周期',
+        })}
+      >
+        <ProFormDateTimePicker
+          name="time"
+          width="m"
+          label={intl.formatMessage({
+            id: 'pages.searchTable.updateForm.schedulingPeriod.timeLabel',
+            defaultMessage: '开始时间',
+          })}
+          rules={[
+            {
+              required: true,
+              message: (
+                <FormattedMessage
+                  id="pages.searchTable.updateForm.schedulingPeriod.timeRules"
+                  defaultMessage="请选择开始时间!"
+                />
+              ),
+            },
+          ]}
+        />
+        <ProFormSelect
+          name="frequency"
+          label={intl.formatMessage({
+            id: 'pages.searchTable.updateForm.object',
+            defaultMessage: '监控对象',
+          })}
+          width="m"
+          valueEnum={{
+            month: '月',
+            week: '周',
+          }}
+        />
+      </StepsForm.StepForm>
+    </StepsForm>
+  );
+};
+
+export default UpdateForm;

+ 36 - 0
src/pages/ListTableList/data.d.ts

@@ -0,0 +1,36 @@
+export interface TableListItem {
+  key: number;
+  disabled?: boolean;
+  href: string;
+  avatar: string;
+  name: string;
+  owner: string;
+  desc: string;
+  callNo: number;
+  status: number;
+  updatedAt: Date;
+  createdAt: Date;
+  progress: number;
+}
+
+export interface TableListPagination {
+  total: number;
+  pageSize: number;
+  current: number;
+}
+
+export interface TableListData {
+  list: TableListItem[];
+  pagination: Partial<TableListPagination>;
+}
+
+export interface TableListParams {
+  status?: string;
+  name?: string;
+  desc?: string;
+  key?: number;
+  pageSize?: number;
+  currentPage?: number;
+  filter?: { [key: string]: any[] };
+  sorter?: { [key: string]: any };
+}

+ 323 - 0
src/pages/ListTableList/index.tsx

@@ -0,0 +1,323 @@
+import { PlusOutlined } from '@ant-design/icons';
+import { Button, Divider, message, Input, Drawer } from 'antd';
+import React, { useState, useRef } from 'react';
+import { useIntl, FormattedMessage } from 'umi';
+import { PageContainer, FooterToolbar } from '@ant-design/pro-layout';
+import ProTable, { ProColumns, ActionType } from '@ant-design/pro-table';
+import ProDescriptions, { ProDescriptionsItemProps } from '@ant-design/pro-descriptions';
+import CreateForm from './components/CreateForm';
+import UpdateForm, { FormValueType } from './components/UpdateForm';
+import { TableListItem } from './data.d';
+import { queryRule, updateRule, addRule, removeRule } from './service';
+
+/**
+ * 添加节点
+ * @param fields
+ */
+const handleAdd = async (fields: TableListItem) => {
+  const hide = message.loading('正在添加');
+  try {
+    await addRule({ ...fields });
+    hide();
+    message.success('添加成功');
+    return true;
+  } catch (error) {
+    hide();
+    message.error('添加失败请重试!');
+    return false;
+  }
+};
+
+/**
+ * 更新节点
+ * @param fields
+ */
+const handleUpdate = async (fields: FormValueType) => {
+  const hide = message.loading('正在配置');
+  try {
+    await updateRule({
+      name: fields.name,
+      desc: fields.desc,
+      key: fields.key,
+    });
+    hide();
+
+    message.success('配置成功');
+    return true;
+  } catch (error) {
+    hide();
+    message.error('配置失败请重试!');
+    return false;
+  }
+};
+
+/**
+ *  删除节点
+ * @param selectedRows
+ */
+const handleRemove = async (selectedRows: TableListItem[]) => {
+  const hide = message.loading('正在删除');
+  if (!selectedRows) return true;
+  try {
+    await removeRule({
+      key: selectedRows.map((row) => row.key),
+    });
+    hide();
+    message.success('删除成功,即将刷新');
+    return true;
+  } catch (error) {
+    hide();
+    message.error('删除失败,请重试');
+    return false;
+  }
+};
+
+const TableList: React.FC<{}> = () => {
+  const [createModalVisible, handleModalVisible] = useState<boolean>(false);
+  const [updateModalVisible, handleUpdateModalVisible] = useState<boolean>(false);
+  const [stepFormValues, setStepFormValues] = useState({});
+  const actionRef = useRef<ActionType>();
+  const [row, setRow] = useState<TableListItem>();
+  const [selectedRowsState, setSelectedRows] = useState<TableListItem[]>([]);
+  const intl = useIntl();
+  const columns: ProColumns<TableListItem>[] = [
+    {
+      title: (
+        <FormattedMessage
+          id="pages.searchTable.updateForm.ruleName.nameLabel"
+          defaultMessage="规则名称"
+        />
+      ),
+      dataIndex: 'name',
+      tip: '规则名称是唯一的 key',
+      formItemProps: {
+        rules: [
+          {
+            required: true,
+            message: (
+              <FormattedMessage id="pages.searchTable.ruleName" defaultMessage="规则名称为必填项" />
+            ),
+          },
+        ],
+      },
+      render: (dom, entity) => {
+        return <a onClick={() => setRow(entity)}>{dom}</a>;
+      },
+    },
+    {
+      title: <FormattedMessage id="pages.searchTable.titleDesc" defaultMessage="描述" />,
+      dataIndex: 'desc',
+      valueType: 'textarea',
+    },
+    {
+      title: <FormattedMessage id="pages.searchTable.titleCallNo" defaultMessage="服务调用次数" />,
+      dataIndex: 'callNo',
+      sorter: true,
+      hideInForm: true,
+      renderText: (val: string) =>
+        `${val}${intl.formatMessage({
+          id: 'pages.searchTable.tenThousand',
+          defaultMessage: ' 万 ',
+        })}`,
+    },
+    {
+      title: <FormattedMessage id="pages.searchTable.titleStatus" defaultMessage="状态" />,
+      dataIndex: 'status',
+      hideInForm: true,
+      valueEnum: {
+        0: {
+          text: (
+            <FormattedMessage id="pages.searchTable.nameStatus.default" defaultMessage="关闭" />
+          ),
+          status: 'Default',
+        },
+        1: {
+          text: (
+            <FormattedMessage id="pages.searchTable.nameStatus.running" defaultMessage="运行中" />
+          ),
+          status: 'Processing',
+        },
+        2: {
+          text: (
+            <FormattedMessage id="pages.searchTable.nameStatus.online" defaultMessage="已上线" />
+          ),
+          status: 'Success',
+        },
+        3: {
+          text: (
+            <FormattedMessage id="pages.searchTable.nameStatus.abnormal" defaultMessage="异常" />
+          ),
+          status: 'Error',
+        },
+      },
+    },
+    {
+      title: (
+        <FormattedMessage id="pages.searchTable.titleUpdatedAt" defaultMessage="上次调度时间" />
+      ),
+      dataIndex: 'updatedAt',
+      sorter: true,
+      valueType: 'dateTime',
+      hideInForm: true,
+      renderFormItem: (item, { defaultRender, ...rest }, form) => {
+        const status = form.getFieldValue('status');
+        if (`${status}` === '0') {
+          return false;
+        }
+        if (`${status}` === '3') {
+          return (
+            <Input
+              {...rest}
+              placeholder={intl.formatMessage({
+                id: 'pages.searchTable.exception',
+                defaultMessage: '请输入异常原因!',
+              })}
+            />
+          );
+        }
+        return defaultRender(item);
+      },
+    },
+    {
+      title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />,
+      dataIndex: 'option',
+      valueType: 'option',
+      render: (_, record) => (
+        <>
+          <a
+            onClick={() => {
+              handleUpdateModalVisible(true);
+              setStepFormValues(record);
+            }}
+          >
+            <FormattedMessage id="pages.searchTable.config" defaultMessage="配置" />
+          </a>
+          <Divider type="vertical" />
+          <a href="">
+            <FormattedMessage id="pages.searchTable.subscribeAlert" defaultMessage="订阅警报" />
+          </a>
+        </>
+      ),
+    },
+  ];
+
+  return (
+    <PageContainer>
+      <ProTable<TableListItem>
+        headerTitle={intl.formatMessage({
+          id: 'pages.searchTable.title',
+          defaultMessage: '查询表格',
+        })}
+        actionRef={actionRef}
+        rowKey="key"
+        search={{
+          labelWidth: 120,
+        }}
+        toolBarRender={() => [
+          <Button type="primary" key="primary" onClick={() => handleModalVisible(true)}>
+            <PlusOutlined /> <FormattedMessage id="pages.searchTable.new" defaultMessage="新建" />
+          </Button>,
+        ]}
+        request={(params, sorter, filter) => queryRule({ ...params, sorter, filter })}
+        columns={columns}
+        rowSelection={{
+          onChange: (_, selectedRows) => setSelectedRows(selectedRows),
+        }}
+      />
+      {selectedRowsState?.length > 0 && (
+        <FooterToolbar
+          extra={
+            <div>
+              <FormattedMessage id="pages.searchTable.chosen" defaultMessage="已选择" />{' '}
+              <a style={{ fontWeight: 600 }}>{selectedRowsState.length}</a>{' '}
+              <FormattedMessage id="pages.searchTable.item" defaultMessage="项" />
+              &nbsp;&nbsp;
+              <span>
+                <FormattedMessage
+                  id="pages.searchTable.totalServiceCalls"
+                  defaultMessage="服务调用次数总计"
+                />{' '}
+                {selectedRowsState.reduce((pre, item) => pre + item.callNo, 0)}{' '}
+                <FormattedMessage id="pages.searchTable.tenThousand" defaultMessage="万" />
+              </span>
+            </div>
+          }
+        >
+          <Button
+            onClick={async () => {
+              await handleRemove(selectedRowsState);
+              setSelectedRows([]);
+              actionRef.current?.reloadAndRest?.();
+            }}
+          >
+            <FormattedMessage id="pages.searchTable.batchDeletion" defaultMessage="批量删除" />
+          </Button>
+          <Button type="primary">
+            <FormattedMessage id="pages.searchTable.batchApproval" defaultMessage="批量审批" />
+          </Button>
+        </FooterToolbar>
+      )}
+      <CreateForm onCancel={() => handleModalVisible(false)} modalVisible={createModalVisible}>
+        <ProTable<TableListItem, TableListItem>
+          onSubmit={async (value) => {
+            const success = await handleAdd(value);
+            if (success) {
+              handleModalVisible(false);
+              if (actionRef.current) {
+                actionRef.current.reload();
+              }
+            }
+          }}
+          rowKey="key"
+          type="form"
+          columns={columns}
+        />
+      </CreateForm>
+      {stepFormValues && Object.keys(stepFormValues).length ? (
+        <UpdateForm
+          onSubmit={async (value) => {
+            const success = await handleUpdate(value);
+            if (success) {
+              handleUpdateModalVisible(false);
+              setStepFormValues({});
+              if (actionRef.current) {
+                actionRef.current.reload();
+              }
+            }
+          }}
+          onCancel={() => {
+            handleUpdateModalVisible(false);
+            setStepFormValues({});
+          }}
+          updateModalVisible={updateModalVisible}
+          values={stepFormValues}
+        />
+      ) : null}
+
+      <Drawer
+        width={600}
+        visible={!!row}
+        onClose={() => {
+          setRow(undefined);
+        }}
+        closable={false}
+      >
+        {row?.name && (
+          <ProDescriptions<TableListItem>
+            column={2}
+            title={row?.name}
+            request={async () => ({
+              data: row || {},
+            })}
+            params={{
+              id: row?.name,
+            }}
+            columns={columns as ProDescriptionsItemProps<TableListItem>[]}
+          />
+        )}
+      </Drawer>
+    </PageContainer>
+  );
+};
+
+export default TableList;

+ 38 - 0
src/pages/ListTableList/service.ts

@@ -0,0 +1,38 @@
+import { request } from 'umi';
+import { TableListParams, TableListItem } from './data.d';
+
+export async function queryRule(params?: TableListParams) {
+  return request('/api/rule', {
+    params,
+  });
+}
+
+export async function removeRule(params: { key: number[] }) {
+  return request('/api/rule', {
+    method: 'POST',
+    data: {
+      ...params,
+      method: 'delete',
+    },
+  });
+}
+
+export async function addRule(params: TableListItem) {
+  return request('/api/rule', {
+    method: 'POST',
+    data: {
+      ...params,
+      method: 'post',
+    },
+  });
+}
+
+export async function updateRule(params: TableListParams) {
+  return request('/api/rule', {
+    method: 'POST',
+    data: {
+      ...params,
+      method: 'update',
+    },
+  });
+}

+ 0 - 0
src/pages/Order/Billing/index.module.less


+ 65 - 0
src/pages/Order/Billing/index.tsx

@@ -0,0 +1,65 @@
+import React, { Component } from 'react';
+import DataTable from '@/components/common/DataTable';
+import {Space, Tag} from "antd";
+import {TableDropdown} from "@ant-design/pro-table";
+
+class Billing extends Component{
+  constructor(props) {
+    super(props);
+    this.state = {
+      columns: [
+        {
+          title: '合同编号',
+          dataIndex: 'contractNo',
+          width: 200,
+          lock: 'left',
+          search: true,
+        },
+        {
+          title: '订单编号',
+          dataIndex: 'orderNo',
+          width: 200,
+        },
+        {
+          title: '客户名称',
+          dataIndex: 'userName',
+          render: text => {
+            return text && text.length > 9 ? text.substr(0, 9) + '...' : text;
+          },
+        },
+        {
+          title: '订单部门',
+          dataIndex: 'depName',
+          width: 200,
+        },
+        {
+          title: '下单时间',
+          dataIndex: 'createDate',
+        },
+        {
+          title: '合同签订时间',
+          dataIndex: 'signDate',
+          width: 200,
+        },
+      ]
+    }
+  }
+
+  componentDidMount() {
+
+  }
+
+  render() {
+    return (
+      <div>
+        <DataTable
+          url={'/api/admin/newOrder/orderNewList'}
+          rowKey={'orderNo'}
+          columns={this.state.columns}
+        />
+      </div>
+    );
+  }
+}
+
+export default Billing;

+ 8 - 0
src/pages/Welcome.less

@@ -0,0 +1,8 @@
+@import '~antd/lib/style/themes/default.less';
+
+.pre {
+  margin: 12px 0;
+  padding: 12px 20px;
+  background: @input-bg;
+  box-shadow: @card-shadow;
+}

+ 63 - 0
src/pages/Welcome.tsx

@@ -0,0 +1,63 @@
+import React from 'react';
+import { PageContainer } from '@ant-design/pro-layout';
+import { Card, Alert, Typography } from 'antd';
+import { useIntl, FormattedMessage } from 'umi';
+import styles from './Welcome.less';
+
+const CodePreview: React.FC<{}> = ({ children }) => (
+  <pre className={styles.pre}>
+    <code>
+      <Typography.Text copyable>{children}</Typography.Text>
+    </code>
+  </pre>
+);
+
+export default (): React.ReactNode => {
+  const intl = useIntl();
+  return (
+    <PageContainer>
+      <Card>
+        <Alert
+          message={intl.formatMessage({
+            id: 'pages.welcome.alertMessage',
+            defaultMessage: '更快更强的重型组件,已经发布。',
+          })}
+          type="success"
+          showIcon
+          banner
+          style={{
+            margin: -12,
+            marginBottom: 24,
+          }}
+        />
+        <Typography.Text strong>
+          <FormattedMessage id="pages.welcome.advancedComponent" defaultMessage="高级表格" />{' '}
+          <a
+            href="https://procomponents.ant.design/components/table"
+            rel="noopener noreferrer"
+            target="__blank"
+          >
+            <FormattedMessage id="pages.welcome.link" defaultMessage="欢迎使用" />
+          </a>
+        </Typography.Text>
+        <CodePreview>yarn add @ant-design/pro-table</CodePreview>
+        <Typography.Text
+          strong
+          style={{
+            marginBottom: 12,
+          }}
+        >
+          <FormattedMessage id="pages.welcome.advancedLayout" defaultMessage="高级布局" />{' '}
+          <a
+            href="https://procomponents.ant.design/components/layout"
+            rel="noopener noreferrer"
+            target="__blank"
+          >
+            <FormattedMessage id="pages.welcome.link" defaultMessage="欢迎使用" />
+          </a>
+        </Typography.Text>
+        <CodePreview>yarn add @ant-design/pro-layout</CodePreview>
+      </Card>
+    </PageContainer>
+  );
+};

+ 213 - 0
src/pages/document.ejs

@@ -0,0 +1,213 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta
+      name="keywords"
+      content="antd,umi,umijs,ant design,脚手架,布局, Ant Design,项目,Pro,admin,控制台,主页,开箱即用,中后台,解决方案,组件库"
+    />
+    <meta
+      name="description"
+      content="
+    An out-of-box UI solution for enterprise applications as a React boilerplate."
+    />
+    <meta
+      name="description"
+      content="
+      开箱即用的中台前端/设计解决方案。"
+    />
+    <meta
+      name="viewport"
+      content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
+    />
+    <title>Ant Design Pro</title>
+    <link rel="icon" href="<%= context.config.publicPath +'favicon.ico'%>" type="image/x-icon" />
+  </head>
+  <body>
+    <noscript>Out-of-the-box mid-stage front/design solution!</noscript>
+    <div id="root">
+      <style>
+        html,
+        body,
+        #root {
+          height: 100%;
+          margin: 0;
+          padding: 0;
+        }
+        #root {
+          background-image: url('<%= context.config.publicPath +"home_bg.png"%>');
+          background-repeat: no-repeat;
+          background-size: 100% auto;
+        }
+        .page-loading-warp {
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          padding: 98px;
+        }
+        .ant-spin {
+          position: absolute;
+          display: none;
+          -webkit-box-sizing: border-box;
+          box-sizing: border-box;
+          margin: 0;
+          padding: 0;
+          color: rgba(0, 0, 0, 0.65);
+          color: #1890ff;
+          font-size: 14px;
+          font-variant: tabular-nums;
+          line-height: 1.5;
+          text-align: center;
+          list-style: none;
+          opacity: 0;
+          -webkit-transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
+          transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
+          transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
+          transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86),
+            -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
+          -webkit-font-feature-settings: 'tnum';
+          font-feature-settings: 'tnum';
+        }
+
+        .ant-spin-spinning {
+          position: static;
+          display: inline-block;
+          opacity: 1;
+        }
+
+        .ant-spin-dot {
+          position: relative;
+          display: inline-block;
+          width: 20px;
+          height: 20px;
+          font-size: 20px;
+        }
+
+        .ant-spin-dot-item {
+          position: absolute;
+          display: block;
+          width: 9px;
+          height: 9px;
+          background-color: #1890ff;
+          border-radius: 100%;
+          -webkit-transform: scale(0.75);
+          -ms-transform: scale(0.75);
+          transform: scale(0.75);
+          -webkit-transform-origin: 50% 50%;
+          -ms-transform-origin: 50% 50%;
+          transform-origin: 50% 50%;
+          opacity: 0.3;
+          -webkit-animation: antspinmove 1s infinite linear alternate;
+          animation: antSpinMove 1s infinite linear alternate;
+        }
+
+        .ant-spin-dot-item:nth-child(1) {
+          top: 0;
+          left: 0;
+        }
+
+        .ant-spin-dot-item:nth-child(2) {
+          top: 0;
+          right: 0;
+          -webkit-animation-delay: 0.4s;
+          animation-delay: 0.4s;
+        }
+
+        .ant-spin-dot-item:nth-child(3) {
+          right: 0;
+          bottom: 0;
+          -webkit-animation-delay: 0.8s;
+          animation-delay: 0.8s;
+        }
+
+        .ant-spin-dot-item:nth-child(4) {
+          bottom: 0;
+          left: 0;
+          -webkit-animation-delay: 1.2s;
+          animation-delay: 1.2s;
+        }
+
+        .ant-spin-dot-spin {
+          -webkit-transform: rotate(45deg);
+          -ms-transform: rotate(45deg);
+          transform: rotate(45deg);
+          -webkit-animation: antrotate 1.2s infinite linear;
+          animation: antRotate 1.2s infinite linear;
+        }
+
+        .ant-spin-lg .ant-spin-dot {
+          width: 32px;
+          height: 32px;
+          font-size: 32px;
+        }
+
+        .ant-spin-lg .ant-spin-dot i {
+          width: 14px;
+          height: 14px;
+        }
+
+        @media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
+          .ant-spin-blur {
+            background: #fff;
+            opacity: 0.5;
+          }
+        }
+
+        @-webkit-keyframes antSpinMove {
+          to {
+            opacity: 1;
+          }
+        }
+
+        @keyframes antSpinMove {
+          to {
+            opacity: 1;
+          }
+        }
+
+        @-webkit-keyframes antRotate {
+          to {
+            -webkit-transform: rotate(405deg);
+            transform: rotate(405deg);
+          }
+        }
+
+        @keyframes antRotate {
+          to {
+            -webkit-transform: rotate(405deg);
+            transform: rotate(405deg);
+          }
+        }
+      </style>
+      <div
+        style="
+          display: flex;
+          flex-direction: column;
+          align-items: center;
+          justify-content: center;
+          height: 100%;
+          min-height: 420px;
+        "
+      >
+        <img src="<%= context.config.publicPath +'pro_icon.svg'%>" alt="logo" width="256" />
+        <div class="page-loading-warp">
+          <div class="ant-spin ant-spin-lg ant-spin-spinning">
+            <span class="ant-spin-dot ant-spin-dot-spin"
+              ><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i
+              ><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i
+            ></span>
+          </div>
+        </div>
+        <div style="display: flex; align-items: center; justify-content: center">
+          <img
+            src="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg"
+            width="32"
+            style="margin-right: 8px"
+          />
+          Ant Design
+        </div>
+      </div>
+    </div>
+  </body>
+</html>

+ 114 - 0
src/pages/user/login/index.less

@@ -0,0 +1,114 @@
+@import '~antd/es/style/themes/default.less';
+
+.container {
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+  overflow: auto;
+  background: @layout-body-background;
+}
+
+.lang {
+  width: 100%;
+  height: 40px;
+  line-height: 44px;
+  text-align: right;
+  :global(.ant-dropdown-trigger) {
+    margin-right: 24px;
+  }
+}
+
+.content {
+  flex: 1;
+  padding: 32px 0;
+}
+
+@media (min-width: @screen-md-min) {
+  .container {
+    background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
+    background-repeat: no-repeat;
+    background-position: center 110px;
+    background-size: 100%;
+  }
+
+  .content {
+    padding: 32px 0 24px;
+  }
+}
+
+.top {
+  text-align: center;
+}
+
+.header {
+  height: 44px;
+  line-height: 44px;
+  a {
+    text-decoration: none;
+  }
+}
+
+.logo {
+  height: 44px;
+  margin-right: 16px;
+  vertical-align: top;
+}
+
+.title {
+  position: relative;
+  top: 2px;
+  color: @heading-color;
+  font-weight: 600;
+  font-size: 33px;
+  font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif;
+}
+
+.desc {
+  margin-top: 12px;
+  margin-bottom: 40px;
+  color: @text-color-secondary;
+  font-size: @font-size-base;
+}
+
+.main {
+  width: 328px;
+  margin: 0 auto;
+  @media screen and (max-width: @screen-sm) {
+    width: 95%;
+    max-width: 328px;
+  }
+
+  :global {
+    .@{ant-prefix}-tabs-nav-list {
+      margin: auto;
+      font-size: 16rpx;
+    }
+  }
+
+  .icon {
+    margin-left: 16px;
+    color: rgba(0, 0, 0, 0.2);
+    font-size: 24px;
+    vertical-align: middle;
+    cursor: pointer;
+    transition: color 0.3s;
+
+    &:hover {
+      color: @primary-color;
+    }
+  }
+
+  .other {
+    margin-top: 24px;
+    line-height: 22px;
+    text-align: left;
+    .register {
+      float: right;
+    }
+  }
+
+  .prefixIcon {
+    color: @primary-color;
+    font-size: @font-size-base;
+  }
+}

+ 308 - 0
src/pages/user/login/index.tsx

@@ -0,0 +1,308 @@
+import {
+  AlipayCircleOutlined,
+  LockTwoTone,
+  MailTwoTone,
+  MobileTwoTone,
+  TaobaoCircleOutlined,
+  UserOutlined,
+  WeiboCircleOutlined,
+} from '@ant-design/icons';
+import { Alert, Space, message, Tabs } from 'antd';
+
+import React, { useState } from 'react';
+import ProForm, { ProFormCaptcha, ProFormCheckbox, ProFormText } from '@ant-design/pro-form';
+import { useIntl, Link, history, FormattedMessage, SelectLang, useModel } from 'umi';
+import Footer from '@/components/Footer';
+import { fakeAccountLogin, getFakeCaptcha, LoginParamsType } from '@/services/login';
+
+import styles from './index.less';
+
+const LoginMessage: React.FC<{
+  content: string;
+}> = ({ content }) => (
+  <Alert
+    style={{
+      marginBottom: 24,
+    }}
+    message={content}
+    type="error"
+    showIcon
+  />
+);
+
+/**
+ * 此方法会跳转到 redirect 参数所在的位置
+ */
+const goto = () => {
+  if (!history) return;
+  setTimeout(() => {
+    const { query } = history.location;
+    const { redirect } = query as { redirect: string };
+    history.push(redirect || '/');
+  }, 10);
+};
+
+const Login: React.FC<{}> = () => {
+  const [submitting, setSubmitting] = useState(false);
+  const [userLoginState, setUserLoginState] = useState<API.LoginStateType>({});
+  const [type, setType] = useState<string>('account');
+  const { initialState, setInitialState } = useModel('@@initialState');
+
+  const intl = useIntl();
+
+  const fetchUserInfo = async () => {
+    const userInfo = await initialState?.fetchUserInfo?.();
+    if (userInfo) {
+      setInitialState({
+        ...initialState,
+        currentUser: userInfo,
+      });
+    }
+  };
+
+  const handleSubmit = async (values: LoginParamsType) => {
+    setSubmitting(true);
+    try {
+      // 登录
+      const msg = await fakeAccountLogin({ ...values, type });
+      console.log(msg,'sss');
+      if(msg.error.length === 0){
+        message.success('登录成功!');
+        // await fetchUserInfo();
+        goto();
+        return;
+      }else{
+        message.info(msg.error[0].message)
+      }
+      // 如果失败去设置用户错误信息
+      setUserLoginState(msg);
+    } catch (error) {
+      message.error('登录失败,请重试!');
+    }
+    setSubmitting(false);
+  };
+  const { status, type: loginType } = userLoginState;
+
+  return (
+    <div className={styles.container}>
+      <div className={styles.lang}>{SelectLang && <SelectLang />}</div>
+      <div className={styles.content}>
+        <div className={styles.top}>
+          <div className={styles.header}>
+            <Link to="/">
+              <img alt="logo" className={styles.logo} src="/logo.svg" />
+              <span className={styles.title}>Ant Design</span>
+            </Link>
+          </div>
+          <div className={styles.desc}>Ant Design 是西湖区最具影响力的 Web 设计规范</div>
+        </div>
+
+        <div className={styles.main}>
+          <ProForm
+            initialValues={{
+              remeber: true,
+            }}
+            submitter={{
+              searchConfig: {
+                submitText: intl.formatMessage({
+                  id: 'pages.login.submit',
+                  defaultMessage: '登录',
+                }),
+              },
+              render: (_, dom) => dom.pop(),
+              submitButtonProps: {
+                loading: submitting,
+                size: 'large',
+                style: {
+                  width: '100%',
+                },
+              },
+            }}
+            onFinish={async (values) => {
+              handleSubmit(values);
+            }}
+          >
+            <Tabs activeKey={type} onChange={setType}>
+              <Tabs.TabPane
+                key="account"
+                tab={intl.formatMessage({
+                  id: 'pages.login.accountLogin.tab',
+                  defaultMessage: '账户密码登录',
+                })}
+              />
+              <Tabs.TabPane
+                key="mobile"
+                tab={intl.formatMessage({
+                  id: 'pages.login.phoneLogin.tab',
+                  defaultMessage: '手机号登录',
+                })}
+              />
+            </Tabs>
+
+            {status === 'error' && loginType === 'account' && (
+              <LoginMessage
+                content={intl.formatMessage({
+                  id: 'pages.login.accountLogin.errorMessage',
+                  defaultMessage: '账户或密码错误(admin/ant.design)',
+                })}
+              />
+            )}
+            {type === 'account' && (
+              <>
+                <ProFormText
+                  name="mobile"
+                  fieldProps={{
+                    size: 'large',
+                    prefix: <UserOutlined className={styles.prefixIcon} />,
+                  }}
+                  placeholder={intl.formatMessage({
+                    id: 'pages.login.username.placeholder',
+                    defaultMessage: '用户名: admin or user',
+                  })}
+                  rules={[
+                    {
+                      required: true,
+                      message: (
+                        <FormattedMessage
+                          id="pages.login.username.required"
+                          defaultMessage="请输入用户名!"
+                        />
+                      ),
+                    },
+                  ]}
+                />
+                <ProFormText.Password
+                  name="password"
+                  fieldProps={{
+                    size: 'large',
+                    prefix: <LockTwoTone className={styles.prefixIcon} />,
+                  }}
+                  placeholder={intl.formatMessage({
+                    id: 'pages.login.password.placeholder',
+                    defaultMessage: '密码: ant.design',
+                  })}
+                  rules={[
+                    {
+                      required: true,
+                      message: (
+                        <FormattedMessage
+                          id="pages.login.password.required"
+                          defaultMessage="请输入密码!"
+                        />
+                      ),
+                    },
+                  ]}
+                />
+              </>
+            )}
+
+            {status === 'error' && loginType === 'mobile' && <LoginMessage content="验证码错误" />}
+            {type === 'mobile' && (
+              <>
+                <ProFormText
+                  fieldProps={{
+                    size: 'large',
+                    prefix: <MobileTwoTone className={styles.prefixIcon} />,
+                  }}
+                  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: <MailTwoTone className={styles.prefixIcon} />,
+                  }}
+                  captchaProps={{
+                    size: 'large',
+                  }}
+                  placeholder={intl.formatMessage({
+                    id: 'pages.login.captcha.placeholder',
+                    defaultMessage: '请输入验证码',
+                  })}
+                  captchaTextRender={(timing, count) =>
+                    timing
+                      ? `${count} ${intl.formatMessage({
+                          id: 'pages.getCaptchaSecondText',
+                          defaultMessage: '获取验证码',
+                        })}`
+                      : intl.formatMessage({
+                          id: 'pages.login.phoneLogin.getVerificationCode',
+                          defaultMessage: '获取验证码',
+                        })
+                  }
+                  name="captcha"
+                  rules={[
+                    {
+                      required: true,
+                      message: (
+                        <FormattedMessage
+                          id="pages.login.captcha.required"
+                          defaultMessage="请输入验证码!"
+                        />
+                      ),
+                    },
+                  ]}
+                  onGetCaptcha={async (mobile) => {
+                    const result = await getFakeCaptcha(mobile);
+                    if (result === false) {
+                      return;
+                    }
+                    message.success('获取验证码成功!验证码为:1234');
+                  }}
+                />
+              </>
+            )}
+            <div
+              style={{
+                marginBottom: 24,
+              }}
+            >
+              <ProFormCheckbox noStyle name="remeber">
+                <FormattedMessage id="pages.login.rememberMe" defaultMessage="自动登录" />
+              </ProFormCheckbox>
+              <a
+                style={{
+                  float: 'right',
+                }}
+              >
+                <FormattedMessage id="pages.login.forgotPassword" defaultMessage="忘记密码" />
+              </a>
+            </div>
+          </ProForm>
+          <Space className={styles.other}>
+            <FormattedMessage id="pages.login.loginWith" defaultMessage="其他登录方式" />
+            <AlipayCircleOutlined className={styles.icon} />
+            <TaobaoCircleOutlined className={styles.icon} />
+            <WeiboCircleOutlined className={styles.icon} />
+          </Space>
+        </div>
+      </div>
+      <Footer />
+    </div>
+  );
+};
+
+export default Login;

+ 70 - 0
src/service-worker.js

@@ -0,0 +1,70 @@
+/* eslint-disable eslint-comments/disable-enable-pair */
+/* eslint-disable no-restricted-globals */
+/* eslint-disable no-underscore-dangle */
+/* globals workbox */
+workbox.core.setCacheNameDetails({
+  prefix: 'antd-pro',
+  suffix: 'v1',
+});
+// Control all opened tabs ASAP
+workbox.clientsClaim();
+
+/**
+ * Use precaching list generated by workbox in build process.
+ * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.precaching
+ */
+workbox.precaching.precacheAndRoute(self.__precacheManifest || []);
+
+/**
+ * Register a navigation route.
+ * https://developers.google.com/web/tools/workbox/modules/workbox-routing#how_to_register_a_navigation_route
+ */
+workbox.routing.registerNavigationRoute('/index.html');
+
+/**
+ * Use runtime cache:
+ * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.routing#.registerRoute
+ *
+ * Workbox provides all common caching strategies including CacheFirst, NetworkFirst etc.
+ * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.strategies
+ */
+
+/**
+ * Handle API requests
+ */
+workbox.routing.registerRoute(/\/api\//, workbox.strategies.networkFirst());
+
+/**
+ * Handle third party requests
+ */
+workbox.routing.registerRoute(
+  /^https:\/\/gw\.alipayobjects\.com\//,
+  workbox.strategies.networkFirst(),
+);
+workbox.routing.registerRoute(
+  /^https:\/\/cdnjs\.cloudflare\.com\//,
+  workbox.strategies.networkFirst(),
+);
+workbox.routing.registerRoute(/\/color.less/, workbox.strategies.networkFirst());
+
+/**
+ * Response to client after skipping waiting with MessageChannel
+ */
+addEventListener('message', (event) => {
+  const replyPort = event.ports[0];
+  const message = event.data;
+  if (replyPort && message && message.type === 'skip-waiting') {
+    event.waitUntil(
+      self.skipWaiting().then(
+        () =>
+          replyPort.postMessage({
+            error: null,
+          }),
+        (error) =>
+          replyPort.postMessage({
+            error,
+          }),
+      ),
+    );
+  }
+});

+ 0 - 0
src/services/API.d.ts


Some files were not shown because too many files changed in this diff