浏览代码

[新增]1.新增ant-design-pro模板

石头哥哥 6 年之前
当前提交
5bdc633cf1
共有 100 个文件被更改,包括 5075 次插入0 次删除
  1. 16 0
      .editorconfig
  2. 4 0
      .eslintignore
  3. 11 0
      .eslintrc.js
  4. 44 0
      .gitignore
  5. 20 0
      .prettierignore
  6. 5 0
      .prettierrc.js
  7. 5 0
      .stylelintrc.js
  8. 57 0
      README.md
  9. 344 0
      config/config.js
  10. 16 0
      config/defaultSettings.js
  11. 113 0
      config/plugin.config.js
  12. 12 0
      jest-puppeteer.config.js
  13. 8 0
      jest.config.js
  14. 10 0
      jsconfig.json
  15. 103 0
      mock/notices.js
  16. 7 0
      mock/route.js
  17. 148 0
      mock/user.js
  18. 137 0
      package.json
  19. 二进制
      public/favicon.png
  20. 二进制
      public/icons/icon-128x128.png
  21. 二进制
      public/icons/icon-192x192.png
  22. 二进制
      public/icons/icon-512x512.png
  23. 43 0
      src/assets/logo.svg
  24. 21 0
      src/components/Authorized/Authorized.jsx
  25. 25 0
      src/components/Authorized/AuthorizedRoute.jsx
  26. 76 0
      src/components/Authorized/CheckPermissions.jsx
  27. 78 0
      src/components/Authorized/PromiseRender.jsx
  28. 70 0
      src/components/Authorized/Secured.jsx
  29. 9 0
      src/components/Authorized/index.jsx
  30. 30 0
      src/components/Authorized/renderAuthorize.js
  31. 71 0
      src/components/CopyBlock/index.jsx
  32. 29 0
      src/components/CopyBlock/index.less
  33. 79 0
      src/components/GlobalHeader/AvatarDropdown.jsx
  34. 177 0
      src/components/GlobalHeader/NoticeIconView.jsx
  35. 69 0
      src/components/GlobalHeader/RightContent.jsx
  36. 101 0
      src/components/GlobalHeader/index.less
  37. 10 0
      src/components/HeaderDropdown/index.jsx
  38. 16 0
      src/components/HeaderDropdown/index.less
  39. 137 0
      src/components/HeaderSearch/index.jsx
  40. 32 0
      src/components/HeaderSearch/index.less
  41. 94 0
      src/components/NoticeIcon/NoticeList.jsx
  42. 105 0
      src/components/NoticeIcon/NoticeList.less
  43. 162 0
      src/components/NoticeIcon/index.jsx
  44. 31 0
      src/components/NoticeIcon/index.less
  45. 16 0
      src/components/PageLoading/index.jsx
  46. 53 0
      src/components/SelectLang/index.jsx
  47. 24 0
      src/components/SelectLang/index.less
  48. 35 0
      src/components/SettingDrawer/themeColorClient.js
  49. 101 0
      src/global.jsx
  50. 47 0
      src/global.less
  51. 193 0
      src/layouts/BasicLayout.jsx
  52. 11 0
      src/layouts/BlankLayout.jsx
  53. 51 0
      src/layouts/SecurityLayout.jsx
  54. 60 0
      src/layouts/UserLayout.jsx
  55. 71 0
      src/layouts/UserLayout.less
  56. 22 0
      src/locales/en-US.js
  57. 5 0
      src/locales/en-US/component.js
  58. 17 0
      src/locales/en-US/globalHeader.js
  59. 51 0
      src/locales/en-US/menu.js
  60. 6 0
      src/locales/en-US/pwa.js
  61. 31 0
      src/locales/en-US/settingDrawer.js
  62. 60 0
      src/locales/en-US/settings.js
  63. 20 0
      src/locales/pt-BR.js
  64. 5 0
      src/locales/pt-BR/component.js
  65. 18 0
      src/locales/pt-BR/globalHeader.js
  66. 51 0
      src/locales/pt-BR/menu.js
  67. 7 0
      src/locales/pt-BR/pwa.js
  68. 32 0
      src/locales/pt-BR/settingDrawer.js
  69. 60 0
      src/locales/pt-BR/settings.js
  70. 22 0
      src/locales/zh-CN.js
  71. 5 0
      src/locales/zh-CN/component.js
  72. 17 0
      src/locales/zh-CN/globalHeader.js
  73. 51 0
      src/locales/zh-CN/menu.js
  74. 6 0
      src/locales/zh-CN/pwa.js
  75. 31 0
      src/locales/zh-CN/settingDrawer.js
  76. 55 0
      src/locales/zh-CN/settings.js
  77. 20 0
      src/locales/zh-TW.js
  78. 5 0
      src/locales/zh-TW/component.js
  79. 17 0
      src/locales/zh-TW/globalHeader.js
  80. 51 0
      src/locales/zh-TW/menu.js
  81. 6 0
      src/locales/zh-TW/pwa.js
  82. 31 0
      src/locales/zh-TW/settingDrawer.js
  83. 55 0
      src/locales/zh-TW/settings.js
  84. 22 0
      src/manifest.json
  85. 115 0
      src/models/global.js
  86. 70 0
      src/models/login.js
  87. 82 0
      src/models/setting.js
  88. 47 0
      src/models/user.js
  89. 19 0
      src/pages/404.jsx
  90. 41 0
      src/pages/Admin.jsx
  91. 53 0
      src/pages/Authorized.jsx
  92. 71 0
      src/pages/Welcome.jsx
  93. 99 0
      src/pages/account/center/Center.less
  94. 218 0
      src/pages/account/center/_mock.js
  95. 124 0
      src/pages/account/center/components/Applications/index.jsx
  96. 51 0
      src/pages/account/center/components/Applications/index.less
  97. 17 0
      src/pages/account/center/components/ArticleListContent/index.jsx
  98. 38 0
      src/pages/account/center/components/ArticleListContent/index.less
  99. 64 0
      src/pages/account/center/components/Articles/index.jsx
  100. 0 0
      src/pages/account/center/components/Articles/index.less

+ 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

+ 4 - 0
.eslintignore

@@ -0,0 +1,4 @@
+/lambda/
+/scripts
+/config
+.history

+ 11 - 0
.eslintrc.js

@@ -0,0 +1,11 @@
+module.exports = {
+  extends: [require.resolve('@umijs/fabric/dist/eslint')],
+  globals: {
+    ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true,
+    page: true,
+  },
+  rules: {
+    'max-len': 0,
+    'no-console': 0,
+  },
+};

+ 44 - 0
.gitignore

@@ -0,0 +1,44 @@
+# 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
+.eslintcache
+# 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
+.idea
+*iml
+package-lock.json
+

+ 20 - 0
.prettierignore

@@ -0,0 +1,20 @@
+**/*.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

+ 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,
+};

+ 57 - 0
README.md

@@ -0,0 +1,57 @@
+# Ant Design Pro
+
+This project is initialized with [Ant Design Pro](https://pro.ant.design). Follow is the quick guide for how to use.
+
+## Environment Prepare
+
+Install `node_modules`:
+
+```bash
+npm install
+```
+
+or
+
+```bash
+yarn
+```
+
+## Provided Scripts
+
+Ant Design Pro provides some useful script to help you quick start and build with web project, code style check and test.
+
+Scripts provided in `package.json`. It's safe to modify or add additional script:
+
+### Start project
+
+```bash
+npm start
+```
+
+### Build project
+
+```bash
+npm run build
+```
+
+### Check code style
+
+```bash
+npm run lint
+```
+
+You can also use script to auto fix some lint error:
+
+```bash
+npm run lint:fix
+```
+
+### Test code
+
+```bash
+npm test
+```
+
+## More
+
+You can view full document on our [official website](https://pro.ant.design). And welcome any feedback in our [github](https://github.com/ant-design/ant-design-pro).

+ 344 - 0
config/config.js

@@ -0,0 +1,344 @@
+import defaultSettings from './defaultSettings'; // https://umijs.org/config/
+
+import slash from 'slash2';
+import webpackPlugin from './plugin.config';
+const { pwa, primaryColor } = defaultSettings; // preview.pro.ant.design only do not use in your production ;
+// preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。
+
+const { ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION } = process.env;
+const isAntDesignProPreview = ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site';
+const plugins = [
+  [
+    'umi-plugin-react',
+    {
+      antd: true,
+      dva: {
+        hmr: true,
+      },
+      locale: {
+        // default false
+        enable: true,
+        // default zh-CN
+        default: 'zh-CN',
+        // default true, when it is true, will use `navigator.language` overwrite default
+        baseNavigator: true,
+      },
+      // dynamicImport: {
+      //   loadingComponent: './components/PageLoading/index',
+      //   webpackChunkName: true,
+      //   level: 3,
+      // },
+      pwa: pwa
+        ? {
+            workboxPluginMode: 'InjectManifest',
+            workboxOptions: {
+              importWorkboxFrom: 'local',
+            },
+          }
+        : false,
+      // default close dll, because issue https://github.com/ant-design/ant-design-pro/issues/4665
+      // dll features https://webpack.js.org/plugins/dll-plugin/
+      // dll: {
+      //   include: ['dva', 'dva/router', 'dva/saga', 'dva/fetch'],
+      //   exclude: ['@babel/runtime', 'netlify-lambda'],
+      // },
+      hd: false,
+    },
+  ],
+  [
+    'umi-plugin-pro-block',
+    {
+      moveMock: false,
+      moveService: false,
+      modifyRequest: true,
+      autoAddMenu: true,
+    },
+  ],
+]; // 针对 preview.pro.ant.design 的 GA 统计代码
+
+if (isAntDesignProPreview) {
+  plugins.push([
+    'umi-plugin-ga',
+    {
+      code: 'UA-72788897-6',
+    },
+  ]);
+}
+
+export default {
+  plugins,
+  block: {
+    // 国内用户可以使用码云
+    defaultGitUrl: 'https://gitee.com/ant-design/pro-blocks', // defaultGitUrl: 'https://github.com/ant-design/pro-blocks',
+  },
+  history: 'browser',
+  hash: false,
+  targets: {
+    ie: 11,
+  },
+  devtool: isAntDesignProPreview ? 'source-map' : false,
+  // umi routes: https://umijs.org/zh/guide/router.html
+  routes: [
+    {
+      path: '/',
+      component: '../layouts/BlankLayout',
+      routes: [
+        {
+          path: '/user',
+          component: '../layouts/UserLayout',
+          routes: [
+            {
+              path: '/user',
+              redirect: '/user/login',
+            },
+            {
+              name: 'login',
+              icon: 'smile',
+              path: '/user/login',
+              component: './user/login',
+            },
+            {
+              name: 'register-result',
+              icon: 'smile',
+              path: '/user/register-result',
+              component: './user/register-result',
+            },
+            {
+              name: 'register',
+              icon: 'smile',
+              path: '/user/register',
+              component: './user/register',
+            },
+            {
+              component: '404',
+            },
+          ],
+        },
+        {
+          path: '/',
+          component: '../layouts/BasicLayout',
+          Routes: ['src/pages/Authorized'],
+          authority: ['admin', 'user'],
+          routes: [
+            {
+              path: '/dashboard',
+              name: 'dashboard',
+              icon: 'dashboard',
+              routes: [
+                {
+                  name: 'analysis',
+                  icon: 'smile',
+                  path: '/dashboard/analysis',
+                  component: './dashboard/analysis',
+                },
+                {
+                  name: 'monitor',
+                  icon: 'smile',
+                  path: '/dashboard/monitor',
+                  component: './dashboard/monitor',
+                },
+                {
+                  name: 'workplace',
+                  icon: 'smile',
+                  path: '/dashboard/workplace',
+                  component: './dashboard/workplace',
+                },
+              ],
+            },
+            {
+              path: '/form',
+              icon: 'form',
+              name: 'form',
+              routes: [
+                {
+                  name: 'basic-form',
+                  icon: 'smile',
+                  path: '/form/basic-form',
+                  component: './form/basic-form',
+                },
+                {
+                  name: 'step-form',
+                  icon: 'smile',
+                  path: '/form/step-form',
+                  component: './form/step-form',
+                },
+                {
+                  name: 'advanced-form',
+                  icon: 'smile',
+                  path: '/form/advanced-form',
+                  component: './form/advanced-form',
+                },
+              ],
+            },
+            {
+              path: '/list',
+              icon: 'table',
+              name: 'list',
+              routes: [
+                {
+                  path: '/list/search',
+                  name: 'search-list',
+                  component: './list/search',
+                  routes: [
+                    {
+                      path: '/list/search',
+                      redirect: '/list/search/articles',
+                    },
+                    {
+                      name: 'articles',
+                      icon: 'smile',
+                      path: '/list/search/articles',
+                      component: './list/search/articles',
+                    },
+                    {
+                      name: 'projects',
+                      icon: 'smile',
+                      path: '/list/search/projects',
+                      component: './list/search/projects',
+                    },
+                    {
+                      name: 'applications',
+                      icon: 'smile',
+                      path: '/list/search/applications',
+                      component: './list/search/applications',
+                    },
+                  ],
+                },
+                {
+                  name: 'table-list',
+                  icon: 'smile',
+                  path: '/list/table-list',
+                  component: './list/table-list',
+                },
+                {
+                  name: 'basic-list',
+                  icon: 'smile',
+                  path: '/list/basic-list',
+                  component: './list/basic-list',
+                },
+                {
+                  name: 'card-list',
+                  icon: 'smile',
+                  path: '/list/card-list',
+                  component: './list/card-list',
+                },
+              ],
+            },
+            {
+              path: '/profile',
+              name: 'profile',
+              icon: 'profile',
+              routes: [
+                {
+                  name: 'basic',
+                  icon: 'smile',
+                  path: '/profile/basic',
+                  component: './profile/basic',
+                },
+                {
+                  name: 'advanced',
+                  icon: 'smile',
+                  path: '/profile/advanced',
+                  component: './profile/advanced',
+                },
+              ],
+            },
+            {
+              name: 'result',
+              icon: 'check-circle-o',
+              path: '/result',
+              routes: [
+                {
+                  name: 'success',
+                  icon: 'smile',
+                  path: '/result/success',
+                  component: './result/success',
+                },
+              ],
+            },
+            {
+              name: 'exception',
+              icon: 'warning',
+              path: '/exception',
+              routes: [],
+            },
+            {
+              name: 'account',
+              icon: 'user',
+              path: '/account',
+              routes: [],
+            },
+            {
+              name: 'editor',
+              icon: 'highlight',
+              path: '/editor',
+              routes: [],
+            },
+            {
+              path: '/',
+              redirect: '/dashboard/analysis',
+              authority: ['admin', 'user'],
+            },
+            {
+              component: '404',
+            },
+          ],
+        },
+      ],
+    },
+  ],
+  // Theme for antd: https://ant.design/docs/react/customize-theme-cn
+  theme: {
+    'primary-color': primaryColor,
+  },
+  define: {
+    ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION:
+      ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION || '', // preview.pro.ant.design only do not use in your production ; preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。
+  },
+  ignoreMomentLocale: true,
+  lessLoaderOptions: {
+    javascriptEnabled: true,
+  },
+  disableRedirectHoist: true,
+  cssLoaderOptions: {
+    modules: true,
+    getLocalIdent: (context, _, localName) => {
+      if (
+        context.resourcePath.includes('node_modules') ||
+        context.resourcePath.includes('ant.design.pro.less') ||
+        context.resourcePath.includes('global.less')
+      ) {
+        return localName;
+      }
+
+      const match = context.resourcePath.match(/src(.*)/);
+
+      if (match && match[1]) {
+        const antdProPath = match[1].replace('.less', '');
+        const arr = slash(antdProPath)
+          .split('/')
+          .map(a => a.replace(/([A-Z])/g, '-$1'))
+          .map(a => a.toLowerCase());
+        return `antd-pro${arr.join('-')}-${localName}`.replace(/--/g, '-');
+      }
+
+      return localName;
+    },
+  },
+  manifest: {
+    basePath: '/',
+  },
+  chainWebpack: webpackPlugin,
+
+  /*
+    proxy: {
+      '/server/api/': {
+        target: 'https://preview.pro.ant.design/',
+        changeOrigin: true,
+        pathRewrite: { '^/server': '' },
+      },
+    },
+    */
+  treeShaking: true,
+  minimizer: 'terserjs',
+};

+ 16 - 0
config/defaultSettings.js

@@ -0,0 +1,16 @@
+export default {
+  navTheme: 'dark',
+  primaryColor: '#1890FF',
+  layout: 'sidemenu',
+  contentWidth: 'Fluid',
+  fixedHeader: false,
+  autoHideHeader: false,
+  fixSiderbar: false,
+  colorWeak: false,
+  menu: {
+    locale: true,
+  },
+  title: 'Ant Design Pro',
+  pwa: false,
+  iconfontUrl: '',
+};

+ 113 - 0
config/plugin.config.js

@@ -0,0 +1,113 @@
+// Change theme plugin
+// eslint-disable-next-line eslint-comments/abdeils - enable - pair;
+
+/* eslint-disable import/no-extraneous-dependencies */
+import ThemeColorReplacer from 'webpack-theme-color-replacer';
+import generate from '@ant-design/colors/lib/generate';
+import path from 'path';
+
+function getModulePackageName(module) {
+  if (!module.context) return null;
+  const nodeModulesPath = path.join(__dirname, '../node_modules/');
+
+  if (module.context.substring(0, nodeModulesPath.length) !== nodeModulesPath) {
+    return null;
+  }
+
+  const moduleRelativePath = module.context.substring(nodeModulesPath.length);
+  const [moduleDirName] = moduleRelativePath.split(path.sep);
+  let packageName = moduleDirName; // handle tree shaking
+
+  if (packageName && packageName.match('^_')) {
+    // eslint-disable-next-line prefer-destructuring
+    packageName = packageName.match(/^_(@?[^@]+)/)[1];
+  }
+
+  return packageName;
+}
+
+export default config => {
+  // preview.pro.ant.design only do not use in your production;
+  if (
+    process.env.ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site' ||
+    process.env.NODE_ENV !== 'production'
+  ) {
+    config.plugin('webpack-theme-color-replacer').use(ThemeColorReplacer, [
+      {
+        fileName: 'css/theme-colors-[contenthash:8].css',
+        matchColors: getAntdSerials('#1890ff'),
+
+        // 主色系列
+        // 改变样式选择器,解决样式覆盖问题
+        changeSelector(selector) {
+          switch (selector) {
+            case '.ant-calendar-today .ant-calendar-date':
+              return ':not(.ant-calendar-selected-date)' + selector;
+
+            case '.ant-btn:focus,.ant-btn:hover':
+              return '.ant-btn:focus:not(.ant-btn-primary),.ant-btn:hover:not(.ant-btn-primary)';
+
+            case '.ant-btn.active,.ant-btn:active':
+              return '.ant-btn.active:not(.ant-btn-primary),.ant-btn:active:not(.ant-btn-primary)';
+
+            default:
+              return selector;
+          }
+        }, // isJsUgly: true,
+      },
+    ]);
+  } // optimize chunks
+
+  config.optimization // share the same chunks across different modules
+    .runtimeChunk(false)
+    .splitChunks({
+      chunks: 'async',
+      name: 'vendors',
+      maxInitialRequests: Infinity,
+      minSize: 0,
+      cacheGroups: {
+        vendors: {
+          test: module => {
+            const packageName = getModulePackageName(module) || '';
+
+            if (packageName) {
+              return [
+                'bizcharts',
+                'gg-editor',
+                'g6',
+                '@antv',
+                'gg-editor-core',
+                'bizcharts-plugin-slider',
+              ].includes(packageName);
+            }
+
+            return false;
+          },
+
+          name(module) {
+            const packageName = getModulePackageName(module);
+
+            if (packageName) {
+              if (['bizcharts', '@antv_data-set'].indexOf(packageName) >= 0) {
+                return 'viz'; // visualization package
+              }
+            }
+
+            return 'misc';
+          },
+        },
+      },
+    });
+};
+
+const getAntdSerials = color => {
+  const lightNum = 9;
+  const devide10 = 10; // 淡化(即less的tint)
+
+  const lightens = new Array(lightNum).fill(undefined).map((_, i) => {
+    return ThemeColorReplacer.varyColor.lighten(color, i / devide10);
+  });
+  const colorPalettes = generate(color);
+  const rgb = ThemeColorReplacer.varyColor.toNum3(color.replace('#', '')).join(',');
+  return lightens.concat(colorPalettes).concat(rgb);
+};

+ 12 - 0
jest-puppeteer.config.js

@@ -0,0 +1,12 @@
+// ps https://github.com/GoogleChrome/puppeteer/issues/3120
+module.exports = {
+  launch: {
+    args: [
+      '--disable-gpu',
+      '--disable-dev-shm-usage',
+      '--no-first-run',
+      '--no-zygote',
+      '--no-sandbox',
+    ],
+  },
+};

+ 8 - 0
jest.config.js

@@ -0,0 +1,8 @@
+module.exports = {
+  testURL: 'http://localhost:8000',
+  preset: 'jest-puppeteer',
+  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/*"]
+    }
+  }
+}

+ 103 - 0
mock/notices.js

@@ -0,0 +1,103 @@
+const getNotices = (req, res) => {
+  res.json([
+    {
+      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,
+};

+ 7 - 0
mock/route.js

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

+ 148 - 0
mock/user.js

@@ -0,0 +1,148 @@
+function getFakeCaptcha(req, res) {
+  return res.json('captcha-xxx');
+} // 代码中会兼容本地 service mock 以及部署站点的静态数据
+
+export default {
+  // 支持值为 Object 和 Array
+  'GET /api/currentUser': {
+    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',
+    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': (req, res) => {
+    const { password, userName, type } = req.body;
+
+    if (password === 'ant.design' && userName === 'admin') {
+      res.send({
+        status: 'ok',
+        type,
+        currentAuthority: 'admin',
+      });
+      return;
+    }
+
+    if (password === 'ant.design' && userName === 'user') {
+      res.send({
+        status: 'ok',
+        type,
+        currentAuthority: 'user',
+      });
+      return;
+    }
+
+    res.send({
+      status: 'error',
+      type,
+      currentAuthority: 'guest',
+    });
+  },
+  'POST /api/register': (req, res) => {
+    res.send({
+      status: 'ok',
+      currentAuthority: 'user',
+    });
+  },
+  'GET /api/500': (req, res) => {
+    res.status(500).send({
+      timestamp: 1513932555104,
+      status: 500,
+      error: 'error',
+      message: 'error',
+      path: '/base/category/list',
+    });
+  },
+  'GET /api/404': (req, res) => {
+    res.status(404).send({
+      timestamp: 1513932643431,
+      status: 404,
+      error: 'Not Found',
+      message: 'No message available',
+      path: '/base/category/list/2121212',
+    });
+  },
+  'GET /api/403': (req, res) => {
+    res.status(403).send({
+      timestamp: 1513932555104,
+      status: 403,
+      error: 'Unauthorized',
+      message: 'Unauthorized',
+      path: '/base/category/list',
+    });
+  },
+  'GET /api/401': (req, res) => {
+    res.status(401).send({
+      timestamp: 1513932555104,
+      status: 401,
+      error: 'Unauthorized',
+      message: 'Unauthorized',
+      path: '/base/category/list',
+    });
+  },
+  'GET  /api/login/captcha': getFakeCaptcha,
+};

+ 137 - 0
package.json

@@ -0,0 +1,137 @@
+{
+  "name": "ant-design-pro",
+  "version": "1.0.0",
+  "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",
+    "fetch:blocks": "pro fetch-blocks && npm run prettier",
+    "format-imports": "cross-env import-sort --write '**/*.{js,jsx,ts,tsx}'",
+    "gh-pages": "cp CNAME ./dist/ && gh-pages -d dist",
+    "i18n-remove": "pro i18n-remove --locale=zh-CN --write",
+    "lint": "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": "npm run prettier&&check-prettier lint",
+    "lint:style": "stylelint --fix \"src/**/*.less\" --syntax less",
+    "prettier": "prettier -c --write \"**/*\"",
+    "start": "umi dev HOST=0.0.0.0",
+    "start:no-mock": "cross-env MOCK=none umi dev",
+    "start:no-ui": "cross-env UMI_UI=none umi dev",
+    "test": "umi test",
+    "test:all": "node ./tests/run-tests.js",
+    "test:component": "umi test ./src/components",
+    "ui": "umi ui"
+  },
+  "husky": {
+    "hooks": {
+      "pre-commit": "npm run lint-staged"
+    }
+  },
+  "lint-staged": {
+    "**/*.less": "stylelint --syntax less",
+    "**/*.{js,jsx,tsx,ts,less,md,json}": [
+      "prettier --write",
+      "git add"
+    ],
+    "**/*.{js,jsx}": "npm run lint-staged:js",
+    "**/*.{js,ts,tsx}": "npm run lint-staged:js"
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not ie <= 10"
+  ],
+  "dependencies": {
+    "@ant-design/colors": "^3.1.0",
+    "@ant-design/pro-layout": "^4.5.16",
+    "@antv/data-set": "^0.10.2",
+    "@types/lodash.debounce": "^4.0.6",
+    "@types/lodash.isequal": "^4.5.5",
+    "@types/react-router": "^5.0.2",
+    "antd": "^3.23.6",
+    "bizcharts": "^3.5.3-beta.0",
+    "bizcharts-plugin-slider": "^2.1.1-beta.1",
+    "classnames": "^2.2.6",
+    "dva": "^2.4.1",
+    "gg-editor": "^2.0.2",
+    "lodash": "^4.17.11",
+    "lodash-decorators": "^6.0.0",
+    "lodash.debounce": "^4.0.8",
+    "lodash.isequal": "^4.5.0",
+    "mockjs": "^1.0.1-beta3",
+    "moment": "^2.24.0",
+    "numeral": "^2.0.6",
+    "nzh": "^1.0.3",
+    "omit.js": "^1.0.2",
+    "path-to-regexp": "2.4.0",
+    "prop-types": "^15.5.10",
+    "qs": "^6.9.0",
+    "react": "^16.8.6",
+    "react-copy-to-clipboard": "^5.0.1",
+    "react-dom": "^16.8.6",
+    "react-fittext": "^1.0.0",
+    "react-helmet": "^5.2.1",
+    "react-router": "^4.3.1",
+    "redux": "^4.0.1",
+    "slash2": "^2.0.0",
+    "umi": "^2.9.6",
+    "umi-plugin-pro-block": "^1.3.4",
+    "umi-plugin-react": "^1.10.1",
+    "umi-request": "^1.2.7",
+    "webpack-theme-color-replacer": "^1.2.15"
+  },
+  "devDependencies": {
+    "@ant-design/pro-cli": "^1.0.13",
+    "@types/classnames": "^2.2.7",
+    "@types/express": "^4.17.0",
+    "@types/history": "^4.7.2",
+    "@types/jest": "^24.0.13",
+    "@types/lodash": "^4.14.144",
+    "@types/qs": "^6.5.3",
+    "@types/react": "^16.8.19",
+    "@types/react-dom": "^16.8.4",
+    "@types/react-helmet": "^5.0.13",
+    "@umijs/fabric": "^1.2.1",
+    "chalk": "^3.0.0",
+    "check-prettier": "^1.0.3",
+    "cross-env": "^6.0.0",
+    "cross-port-killer": "^1.1.1",
+    "enzyme": "^3.9.0",
+    "eslint": "^5.16.0",
+    "express": "^4.17.1",
+    "gh-pages": "^2.0.1",
+    "husky": "^3.0.0",
+    "import-sort-cli": "^6.0.0",
+    "import-sort-parser-babylon": "^6.0.0",
+    "import-sort-parser-typescript": "^6.0.0",
+    "import-sort-style-module": "^6.0.0",
+    "jest-puppeteer": "^4.2.0",
+    "lint-staged": "^9.0.0",
+    "mockjs": "^1.0.1-beta3",
+    "node-fetch": "^2.6.0",
+    "prettier": "^1.17.1",
+    "pro-download": "1.0.1",
+    "stylelint": "^12.0.0",
+    "umi-plugin-ga": "^1.1.3",
+    "umi-plugin-pro": "^1.0.2",
+    "umi-types": "^0.5.0"
+  },
+  "optionalDependencies": {
+    "puppeteer": "^1.17.0"
+  },
+  "engines": {
+    "node": ">=10.0.0"
+  },
+  "checkFiles": [
+    "src/**/*.js*",
+    "src/**/*.ts*",
+    "src/**/*.less",
+    "config/**/*.js*",
+    "scripts/**/*.js"
+  ]
+}

二进制
public/favicon.png


二进制
public/icons/icon-128x128.png


二进制
public/icons/icon-192x192.png


二进制
public/icons/icon-512x512.png


文件差异内容过多而无法显示
+ 43 - 0
src/assets/logo.svg


+ 21 - 0
src/components/Authorized/Authorized.jsx

@@ -0,0 +1,21 @@
+import React from 'react';
+import { Result } from 'antd';
+import check from './CheckPermissions';
+
+const Authorized = ({
+  children,
+  authority,
+  noMatch = (
+    <Result
+      status="403"
+      title="403"
+      subTitle="Sorry, you are not authorized to access this page."
+    />
+  ),
+}) => {
+  const childrenRender = typeof children === 'undefined' ? null : children;
+  const dom = check(authority, childrenRender, noMatch);
+  return <>{dom}</>;
+};
+
+export default Authorized;

+ 25 - 0
src/components/Authorized/AuthorizedRoute.jsx

@@ -0,0 +1,25 @@
+import { Redirect, Route } from 'umi';
+import React from 'react';
+import Authorized from './Authorized';
+
+const AuthorizedRoute = ({ component: Component, render, authority, redirectPath, ...rest }) => (
+  <Authorized
+    authority={authority}
+    noMatch={
+      <Route
+        {...rest}
+        render={() => (
+          <Redirect
+            to={{
+              pathname: redirectPath,
+            }}
+          />
+        )}
+      />
+    }
+  >
+    <Route {...rest} render={props => (Component ? <Component {...props} /> : render(props))} />
+  </Authorized>
+);
+
+export default AuthorizedRoute;

+ 76 - 0
src/components/Authorized/CheckPermissions.jsx

@@ -0,0 +1,76 @@
+import React from 'react';
+import { CURRENT } from './renderAuthorize'; // eslint-disable-next-line import/no-cycle
+
+import PromiseRender from './PromiseRender';
+
+/**
+ * 通用权限检查方法
+ * Common check permissions method
+ * @param { 权限判定 | Permission judgment } authority
+ * @param { 你的权限 | Your permission description } currentAuthority
+ * @param { 通过的组件 | Passing components } target
+ * @param { 未通过的组件 | no pass components } Exception
+ */
+const checkPermissions = (authority, currentAuthority, target, Exception) => {
+  // 没有判定权限.默认查看所有
+  // Retirement authority, return target;
+  if (!authority) {
+    return target;
+  } // 数组处理
+
+  if (Array.isArray(authority)) {
+    if (Array.isArray(currentAuthority)) {
+      if (currentAuthority.some(item => authority.includes(item))) {
+        return target;
+      }
+    } else if (authority.includes(currentAuthority)) {
+      return target;
+    }
+
+    return Exception;
+  } // string 处理
+
+  if (typeof authority === 'string') {
+    if (Array.isArray(currentAuthority)) {
+      if (currentAuthority.some(item => authority === item)) {
+        return target;
+      }
+    } else if (authority === currentAuthority) {
+      return target;
+    }
+
+    return Exception;
+  } // Promise 处理
+
+  if (authority instanceof Promise) {
+    return <PromiseRender ok={target} error={Exception} promise={authority} />;
+  } // Function 处理
+
+  if (typeof authority === 'function') {
+    try {
+      const bool = authority(currentAuthority); // 函数执行后返回值是 Promise
+
+      if (bool instanceof Promise) {
+        return <PromiseRender ok={target} error={Exception} promise={bool} />;
+      }
+
+      if (bool) {
+        return target;
+      }
+
+      return Exception;
+    } catch (error) {
+      throw error;
+    }
+  }
+
+  throw new Error('unsupported parameters');
+};
+
+export { checkPermissions };
+
+function check(authority, target, Exception) {
+  return checkPermissions(authority, CURRENT, target, Exception);
+}
+
+export default check;

+ 78 - 0
src/components/Authorized/PromiseRender.jsx

@@ -0,0 +1,78 @@
+import React from 'react';
+import { Spin } from 'antd';
+import isEqual from 'lodash/isEqual';
+import { isComponentClass } from './Secured'; // eslint-disable-next-line import/no-cycle
+
+export default class PromiseRender extends React.Component {
+  state = {
+    component: () => null,
+  };
+
+  componentDidMount() {
+    this.setRenderComponent(this.props);
+  }
+
+  shouldComponentUpdate = (nextProps, nextState) => {
+    const { component } = this.state;
+
+    if (!isEqual(nextProps, this.props)) {
+      this.setRenderComponent(nextProps);
+    }
+
+    if (nextState.component !== component) return true;
+    return false;
+  }; // set render Component : ok or error
+
+  setRenderComponent(props) {
+    const ok = this.checkIsInstantiation(props.ok);
+    const error = this.checkIsInstantiation(props.error);
+    props.promise
+      .then(() => {
+        this.setState({
+          component: ok,
+        });
+        return true;
+      })
+      .catch(() => {
+        this.setState({
+          component: error,
+        });
+      });
+  } // Determine whether the incoming component has been instantiated
+  // AuthorizedRoute is already instantiated
+  // Authorized  render is already instantiated, children is no instantiated
+  // Secured is not instantiated
+
+  checkIsInstantiation = target => {
+    if (isComponentClass(target)) {
+      const Target = target;
+      return props => <Target {...props} />;
+    }
+
+    if (React.isValidElement(target)) {
+      return props => React.cloneElement(target, props);
+    }
+
+    return () => target;
+  };
+
+  render() {
+    const { component: Component } = this.state;
+    const { ok, error, promise, ...rest } = this.props;
+    return Component ? (
+      <Component {...rest} />
+    ) : (
+      <div
+        style={{
+          width: '100%',
+          height: '100%',
+          margin: 'auto',
+          paddingTop: 50,
+          textAlign: 'center',
+        }}
+      >
+        <Spin size="large" />
+      </div>
+    );
+  }
+}

+ 70 - 0
src/components/Authorized/Secured.jsx

@@ -0,0 +1,70 @@
+import React from 'react';
+import CheckPermissions from './CheckPermissions';
+/**
+ * 默认不能访问任何页面
+ * default is "NULL"
+ */
+
+const Exception403 = () => 403;
+
+export const isComponentClass = component => {
+  if (!component) return false;
+  const proto = Object.getPrototypeOf(component);
+  if (proto === React.Component || proto === Function.prototype) return true;
+  return isComponentClass(proto);
+}; // Determine whether the incoming component has been instantiated
+// AuthorizedRoute is already instantiated
+// Authorized  render is already instantiated, children is no instantiated
+// Secured is not instantiated
+
+const checkIsInstantiation = target => {
+  if (isComponentClass(target)) {
+    const Target = target;
+    return props => <Target {...props} />;
+  }
+
+  if (React.isValidElement(target)) {
+    return props => React.cloneElement(target, props);
+  }
+
+  return () => target;
+};
+/**
+ * 用于判断是否拥有权限访问此 view 权限
+ * authority 支持传入 string, () => boolean | Promise
+ * e.g. 'user' 只有 user 用户能访问
+ * e.g. 'user,admin' user 和 admin 都能访问
+ * e.g. ()=>boolean 返回true能访问,返回false不能访问
+ * e.g. Promise  then 能访问   catch不能访问
+ * e.g. authority support incoming string, () => boolean | Promise
+ * e.g. 'user' only user user can access
+ * e.g. 'user, admin' user and admin can access
+ * e.g. () => boolean true to be able to visit, return false can not be accessed
+ * e.g. Promise then can not access the visit to catch
+ * @param {string | function | Promise} authority
+ * @param {ReactNode} error 非必需参数
+ */
+
+const authorize = (authority, error) => {
+  /**
+   * conversion into a class
+   * 防止传入字符串时找不到staticContext造成报错
+   * String parameters can cause staticContext not found error
+   */
+  let classError = false;
+
+  if (error) {
+    classError = () => error;
+  }
+
+  if (!authority) {
+    throw new Error('authority is required');
+  }
+
+  return function decideAuthority(target) {
+    const component = CheckPermissions(authority, target, classError || Exception403);
+    return checkIsInstantiation(component);
+  };
+};
+
+export default authorize;

+ 9 - 0
src/components/Authorized/index.jsx

@@ -0,0 +1,9 @@
+import Authorized from './Authorized';
+import Secured from './Secured';
+import check from './CheckPermissions';
+import renderAuthorize from './renderAuthorize';
+
+Authorized.Secured = Secured;
+Authorized.check = check;
+const RenderAuthorize = renderAuthorize(Authorized);
+export default RenderAuthorize;

+ 30 - 0
src/components/Authorized/renderAuthorize.js

@@ -0,0 +1,30 @@
+/* eslint-disable eslint-comments/disable-enable-pair */
+
+/* eslint-disable import/no-mutable-exports */
+let CURRENT = 'NULL';
+
+/**
+ * use  authority or getAuthority
+ * @param {string|()=>String} currentAuthority
+ */
+const renderAuthorize = Authorized => currentAuthority => {
+  if (currentAuthority) {
+    if (typeof currentAuthority === 'function') {
+      CURRENT = currentAuthority();
+    }
+
+    if (
+      Object.prototype.toString.call(currentAuthority) === '[object String]' ||
+      Array.isArray(currentAuthority)
+    ) {
+      CURRENT = currentAuthority;
+    }
+  } else {
+    CURRENT = 'NULL';
+  }
+
+  return Authorized;
+};
+
+export { CURRENT };
+export default Authorized => renderAuthorize(Authorized);

+ 71 - 0
src/components/CopyBlock/index.jsx

@@ -0,0 +1,71 @@
+import { Icon, Popover, Typography } from 'antd';
+import React, { useRef } from 'react';
+import { FormattedMessage } from 'umi-plugin-react/locale';
+import { connect } from 'dva';
+import { isAntDesignPro } from '@/utils/utils';
+import styles from './index.less';
+
+const firstUpperCase = pathString =>
+  pathString
+    .replace('.', '')
+    .split(/\/|-/)
+    .map(s => s.toLowerCase().replace(/( |^)[a-z]/g, L => L.toUpperCase()))
+    .filter(s => !!s)
+    .join(''); // when  click block copy, send block url to  ga
+
+const onBlockCopy = label => {
+  if (!isAntDesignPro()) {
+    return;
+  }
+
+  const ga = window && window.ga;
+
+  if (ga) {
+    ga('send', 'event', {
+      eventCategory: 'block',
+      eventAction: 'copy',
+      eventLabel: label,
+    });
+  }
+};
+
+const BlockCodeView = ({ url }) => {
+  const blockUrl = `npx umi block add ${firstUpperCase(url)} --path=${url}`;
+  return (
+    <div className={styles['copy-block-view']}>
+      <Typography.Paragraph
+        copyable={{
+          text: blockUrl,
+          onCopy: () => onBlockCopy(url),
+        }}
+        style={{
+          display: 'flex',
+        }}
+      >
+        <pre>
+          <code className={styles['copy-block-code']}>{blockUrl}</code>
+        </pre>
+      </Typography.Paragraph>
+    </div>
+  );
+};
+
+export default connect(({ routing }) => ({
+  location: routing.location,
+}))(({ location }) => {
+  const url = location.pathname;
+  const divDom = useRef(null);
+  return (
+    <Popover
+      title={<FormattedMessage id="app.preview.down.block" defaultMessage="下载此页面到本地项目" />}
+      placement="topLeft"
+      content={<BlockCodeView url={url} />}
+      trigger="click"
+      getPopupContainer={dom => (divDom.current ? divDom.current : dom)}
+    >
+      <div className={styles['copy-block']} ref={divDom}>
+        <Icon type="download" />
+      </div>
+    </Popover>
+  );
+});

+ 29 - 0
src/components/CopyBlock/index.less

@@ -0,0 +1,29 @@
+.copy-block {
+  position: fixed;
+  right: 80px;
+  bottom: 40px;
+  z-index: 99;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  width: 40px;
+  height: 40px;
+  font-size: 20px;
+  background: #fff;
+  border-radius: 40px;
+  box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14),
+    0 1px 10px 0 rgba(0, 0, 0, 0.12);
+  cursor: pointer;
+}
+
+.copy-block-view {
+  position: relative;
+  .copy-block-code {
+    display: inline-block;
+    margin: 0 0.2em;
+    padding: 0.2em 0.4em 0.1em;
+    font-size: 85%;
+    border-radius: 3px;
+  }
+}

+ 79 - 0
src/components/GlobalHeader/AvatarDropdown.jsx

@@ -0,0 +1,79 @@
+import { Avatar, Icon, Menu, Spin } from 'antd';
+import { FormattedMessage } from 'umi-plugin-react/locale';
+import React from 'react';
+import { connect } from 'dva';
+import router from 'umi/router';
+import HeaderDropdown from '../HeaderDropdown';
+import styles from './index.less';
+
+class AvatarDropdown extends React.Component {
+  onMenuClick = event => {
+    const { key } = event;
+
+    if (key === 'logout') {
+      const { dispatch } = this.props;
+
+      if (dispatch) {
+        dispatch({
+          type: 'login/logout',
+        });
+      }
+
+      return;
+    }
+
+    router.push(`/account/${key}`);
+  };
+
+  render() {
+    const {
+      currentUser = {
+        avatar: '',
+        name: '',
+      },
+      menu,
+    } = this.props;
+    const menuHeaderDropdown = (
+      <Menu className={styles.menu} selectedKeys={[]} onClick={this.onMenuClick}>
+        {menu && (
+          <Menu.Item key="center">
+            <Icon type="user" />
+            <FormattedMessage id="menu.account.center" defaultMessage="account center" />
+          </Menu.Item>
+        )}
+        {menu && (
+          <Menu.Item key="settings">
+            <Icon type="setting" />
+            <FormattedMessage id="menu.account.settings" defaultMessage="account settings" />
+          </Menu.Item>
+        )}
+        {menu && <Menu.Divider />}
+
+        <Menu.Item key="logout">
+          <Icon type="logout" />
+          <FormattedMessage id="menu.account.logout" defaultMessage="logout" />
+        </Menu.Item>
+      </Menu>
+    );
+    return currentUser && currentUser.name ? (
+      <HeaderDropdown overlay={menuHeaderDropdown}>
+        <span className={`${styles.action} ${styles.account}`}>
+          <Avatar size="small" className={styles.avatar} src={currentUser.avatar} alt="avatar" />
+          <span className={styles.name}>{currentUser.name}</span>
+        </span>
+      </HeaderDropdown>
+    ) : (
+      <Spin
+        size="small"
+        style={{
+          marginLeft: 8,
+          marginRight: 8,
+        }}
+      />
+    );
+  }
+}
+
+export default connect(({ user }) => ({
+  currentUser: user.currentUser,
+}))(AvatarDropdown);

+ 177 - 0
src/components/GlobalHeader/NoticeIconView.jsx

@@ -0,0 +1,177 @@
+import React, { Component } from 'react';
+import { Tag, message } from 'antd';
+import { connect } from 'dva';
+import { formatMessage } from 'umi-plugin-react/locale';
+import groupBy from 'lodash/groupBy';
+import moment from 'moment';
+import NoticeIcon from '../NoticeIcon';
+import styles from './index.less';
+
+class GlobalHeaderRight extends Component {
+  componentDidMount() {
+    const { dispatch } = this.props;
+
+    if (dispatch) {
+      dispatch({
+        type: 'global/fetchNotices',
+      });
+    }
+  }
+
+  changeReadState = clickedItem => {
+    const { id } = clickedItem;
+    const { dispatch } = this.props;
+
+    if (dispatch) {
+      dispatch({
+        type: 'global/changeNoticeReadState',
+        payload: id,
+      });
+    }
+  };
+
+  handleNoticeClear = (title, key) => {
+    const { dispatch } = this.props;
+    message.success(
+      `${formatMessage({
+        id: 'component.noticeIcon.cleared',
+      })} ${title}`,
+    );
+
+    if (dispatch) {
+      dispatch({
+        type: 'global/clearNotices',
+        payload: key,
+      });
+    }
+  };
+
+  getNoticeData = () => {
+    const { notices = [] } = this.props;
+
+    if (notices.length === 0) {
+      return {};
+    }
+
+    const newNotices = notices.map(notice => {
+      const newNotice = { ...notice };
+
+      if (newNotice.datetime) {
+        newNotice.datetime = moment(notice.datetime).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');
+  };
+
+  getUnreadData = noticeData => {
+    const unreadMsg = {};
+    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;
+  };
+
+  render() {
+    const { currentUser, fetchingNotices, onNoticeVisibleChange } = this.props;
+    const noticeData = this.getNoticeData();
+    const unreadMsg = this.getUnreadData(noticeData);
+    return (
+      <NoticeIcon
+        className={styles.action}
+        count={currentUser && currentUser.unreadCount}
+        onItemClick={item => {
+          this.changeReadState(item);
+        }}
+        loading={fetchingNotices}
+        clearText={formatMessage({
+          id: 'component.noticeIcon.clear',
+        })}
+        viewMoreText={formatMessage({
+          id: 'component.noticeIcon.view-more',
+        })}
+        onClear={this.handleNoticeClear}
+        onPopupVisibleChange={onNoticeVisibleChange}
+        onViewMore={() => message.info('Click on view more')}
+        clearClose
+      >
+        <NoticeIcon.Tab
+          tabKey="notification"
+          count={unreadMsg.notification}
+          list={noticeData.notification}
+          title={formatMessage({
+            id: 'component.globalHeader.notification',
+          })}
+          emptyText={formatMessage({
+            id: 'component.globalHeader.notification.empty',
+          })}
+          showViewMore
+        />
+        <NoticeIcon.Tab
+          tabKey="message"
+          count={unreadMsg.message}
+          list={noticeData.message}
+          title={formatMessage({
+            id: 'component.globalHeader.message',
+          })}
+          emptyText={formatMessage({
+            id: 'component.globalHeader.message.empty',
+          })}
+          showViewMore
+        />
+        <NoticeIcon.Tab
+          tabKey="event"
+          title={formatMessage({
+            id: 'component.globalHeader.event',
+          })}
+          emptyText={formatMessage({
+            id: 'component.globalHeader.event.empty',
+          })}
+          count={unreadMsg.event}
+          list={noticeData.event}
+          showViewMore
+        />
+      </NoticeIcon>
+    );
+  }
+}
+
+export default connect(({ user, global, loading }) => ({
+  currentUser: user.currentUser,
+  collapsed: global.collapsed,
+  fetchingMoreNotices: loading.effects['global/fetchMoreNotices'],
+  fetchingNotices: loading.effects['global/fetchNotices'],
+  notices: global.notices,
+}))(GlobalHeaderRight);

+ 69 - 0
src/components/GlobalHeader/RightContent.jsx

@@ -0,0 +1,69 @@
+import { Icon, Tooltip } from 'antd';
+import React from 'react';
+import { connect } from 'dva';
+import { formatMessage } from 'umi-plugin-react/locale';
+import Avatar from './AvatarDropdown';
+import HeaderSearch from '../HeaderSearch';
+import SelectLang from '../SelectLang';
+import styles from './index.less';
+import NoticeIconView from './NoticeIconView';
+
+const GlobalHeaderRight = props => {
+  const { theme, layout } = props;
+  let className = styles.right;
+
+  if (theme === 'dark' && layout === 'topmenu') {
+    className = `${styles.right}  ${styles.dark}`;
+  }
+
+  return (
+    <div className={className}>
+      <HeaderSearch
+        className={`${styles.action} ${styles.search}`}
+        placeholder={formatMessage({
+          id: 'component.globalHeader.search',
+        })}
+        defaultValue="umi ui"
+        dataSource={[
+          formatMessage({
+            id: 'component.globalHeader.search.example1',
+          }),
+          formatMessage({
+            id: 'component.globalHeader.search.example2',
+          }),
+          formatMessage({
+            id: 'component.globalHeader.search.example3',
+          }),
+        ]}
+        onSearch={value => {
+          console.log('input', value);
+        }}
+        onPressEnter={value => {
+          console.log('enter', value);
+        }}
+      />
+      <Tooltip
+        title={formatMessage({
+          id: 'component.globalHeader.help',
+        })}
+      >
+        <a
+          target="_blank"
+          href="https://pro.ant.design/docs/getting-started"
+          rel="noopener noreferrer"
+          className={styles.action}
+        >
+          <Icon type="question-circle-o" />
+        </a>
+      </Tooltip>
+      <NoticeIconView />
+      <Avatar menu />
+      <SelectLang className={styles.action} />
+    </div>
+  );
+};
+
+export default connect(({ settings }) => ({
+  theme: settings.navTheme,
+  layout: settings.layout,
+}))(GlobalHeaderRight);

+ 101 - 0
src/components/GlobalHeader/index.less

@@ -0,0 +1,101 @@
+@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 {
+  float: right;
+  height: 100%;
+  margin-left: auto;
+  overflow: hidden;
+  .action {
+    display: inline-block;
+    height: 100%;
+    padding: 0 12px;
+    cursor: pointer;
+    transition: all 0.3s;
+    > i {
+      color: @text-color;
+      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: ~'calc((@{layout-header-height} - 24px) / 2)' 0;
+      margin-right: 8px;
+      color: @primary-color;
+      vertical-align: top;
+      background: rgba(255, 255, 255, 0.85);
+    }
+  }
+}
+
+.dark {
+  height: @layout-header-height;
+  .action {
+    color: rgba(255, 255, 255, 0.85);
+    > i {
+      color: rgba(255, 255, 255, 0.85);
+    }
+    &:hover,
+    &:global(.opened) {
+      background: @primary-color;
+    }
+  }
+}
+
+:global(.ant-pro-global-header) {
+  .dark {
+    .action {
+      color: @text-color;
+      > i {
+        color: @text-color;
+      }
+      &:hover {
+        color: rgba(255, 255, 255, 0.85);
+        > i {
+          color: rgba(255, 255, 255, 0.85);
+        }
+      }
+    }
+  }
+}
+
+@media only screen and (max-width: @screen-md) {
+  :global(.ant-divider-vertical) {
+    vertical-align: unset;
+  }
+  .name {
+    display: none;
+  }
+  .right {
+    position: absolute;
+    top: 0;
+    right: 12px;
+    .account {
+      .avatar {
+        margin-right: 0;
+      }
+    }
+  }
+}

+ 10 - 0
src/components/HeaderDropdown/index.jsx

@@ -0,0 +1,10 @@
+import { Dropdown } from 'antd';
+import React from 'react';
+import classNames from 'classnames';
+import styles from './index.less';
+
+const HeaderDropdown = ({ overlayClassName: cls, ...restProps }) => (
+  <Dropdown overlayClassName={classNames(styles.container, cls)} {...restProps} />
+);
+
+export default HeaderDropdown;

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

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

+ 137 - 0
src/components/HeaderSearch/index.jsx

@@ -0,0 +1,137 @@
+import { AutoComplete, Icon, Input } from 'antd';
+import React, { Component } from 'react';
+import classNames from 'classnames';
+import debounce from 'lodash/debounce';
+import styles from './index.less';
+
+export default class HeaderSearch extends Component {
+  static defaultProps = {
+    defaultActiveFirstOption: false,
+    onPressEnter: () => {},
+    onSearch: () => {},
+    onChange: () => {},
+    className: '',
+    placeholder: '',
+    dataSource: [],
+    defaultOpen: false,
+    onVisibleChange: () => {},
+  };
+
+  static getDerivedStateFromProps(props) {
+    if ('open' in props) {
+      return {
+        searchMode: props.open,
+      };
+    }
+
+    return null;
+  }
+
+  inputRef = null;
+
+  constructor(props) {
+    super(props);
+    this.state = {
+      searchMode: props.defaultOpen,
+      value: props.defaultValue,
+    };
+    this.debouncePressEnter = debounce(this.debouncePressEnter, 500, {
+      leading: true,
+      trailing: false,
+    });
+  }
+
+  onKeyDown = e => {
+    if (e.key === 'Enter') {
+      this.debouncePressEnter();
+    }
+  };
+
+  onChange = value => {
+    if (typeof value === 'string') {
+      const { onSearch, onChange } = this.props;
+      this.setState({
+        value,
+      });
+
+      if (onSearch) {
+        onSearch(value);
+      }
+
+      if (onChange) {
+        onChange(value);
+      }
+    }
+  };
+
+  enterSearchMode = () => {
+    const { onVisibleChange } = this.props;
+    onVisibleChange(true);
+    this.setState(
+      {
+        searchMode: true,
+      },
+      () => {
+        const { searchMode } = this.state;
+
+        if (searchMode && this.inputRef) {
+          this.inputRef.focus();
+        }
+      },
+    );
+  };
+
+  leaveSearchMode = () => {
+    this.setState({
+      searchMode: false,
+    });
+  };
+
+  debouncePressEnter = () => {
+    const { onPressEnter } = this.props;
+    const { value } = this.state;
+    onPressEnter(value || '');
+  };
+
+  render() {
+    const { className, defaultValue, placeholder, open, ...restProps } = this.props;
+    const { searchMode, value } = this.state;
+    delete restProps.defaultOpen; // for rc-select not affected
+
+    const inputClass = classNames(styles.input, {
+      [styles.show]: searchMode,
+    });
+    return (
+      <span
+        className={classNames(className, styles.headerSearch)}
+        onClick={this.enterSearchMode}
+        onTransitionEnd={({ propertyName }) => {
+          if (propertyName === 'width' && !searchMode) {
+            const { onVisibleChange } = this.props;
+            onVisibleChange(searchMode);
+          }
+        }}
+      >
+        <Icon type="search" key="Icon" />
+        <AutoComplete
+          key="AutoComplete"
+          {...restProps}
+          className={inputClass}
+          value={value}
+          onChange={this.onChange}
+        >
+          <Input
+            ref={node => {
+              this.inputRef = node;
+            }}
+            defaultValue={defaultValue}
+            aria-label={placeholder}
+            placeholder={placeholder}
+            onKeyDown={this.onKeyDown}
+            onBlur={this.leaveSearchMode}
+          />
+        </AutoComplete>
+      </span>
+    );
+  }
+}

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

@@ -0,0 +1,32 @@
+@import '~antd/es/style/themes/default.less';
+
+.headerSearch {
+  :global(.anticon-search) {
+    font-size: 16px;
+    cursor: pointer;
+  }
+  .input {
+    width: 0;
+    background: transparent;
+    border-radius: 0;
+    transition: width 0.3s, margin-left 0.3s;
+    :global(.ant-select-selection) {
+      background: transparent;
+    }
+    input {
+      padding-right: 0;
+      padding-left: 0;
+      border: 0;
+      box-shadow: none !important;
+    }
+    &,
+    &:hover,
+    &:focus {
+      border-bottom: 1px solid @border-color-base;
+    }
+    &.show {
+      width: 210px;
+      margin-left: 8px;
+    }
+  }
+}

+ 94 - 0
src/components/NoticeIcon/NoticeList.jsx

@@ -0,0 +1,94 @@
+import { Avatar, List } from 'antd';
+import React from 'react';
+import classNames from 'classnames';
+import styles from './NoticeList.less';
+
+const NoticeList = ({
+  data = [],
+  onClick,
+  onClear,
+  title,
+  onViewMore,
+  emptyText,
+  showClear = true,
+  clearText,
+  viewMoreText,
+  showViewMore = false,
+}) => {
+  if (data.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
+        className={styles.list}
+        dataSource={data}
+        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.key || 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;

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

@@ -0,0 +1,105 @@
+@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: #fff;
+    }
+    .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;
+    &:hover {
+      color: @heading-color;
+    }
+    &:only-child {
+      width: 100%;
+    }
+    &:not(:only-child):last-child {
+      border-left: 1px solid @border-color-split;
+    }
+  }
+}

+ 162 - 0
src/components/NoticeIcon/index.jsx

@@ -0,0 +1,162 @@
+import { Badge, Icon, Spin, Tabs } from 'antd';
+import React, { Component } from 'react';
+import classNames from 'classnames';
+import NoticeList from './NoticeList';
+import HeaderDropdown from '../HeaderDropdown';
+import styles from './index.less';
+
+const { TabPane } = Tabs;
+export default class NoticeIcon extends Component {
+  static Tab = NoticeList;
+
+  static defaultProps = {
+    onItemClick: () => {},
+    onPopupVisibleChange: () => {},
+    onTabChange: () => {},
+    onClear: () => {},
+    onViewMore: () => {},
+    loading: false,
+    clearClose: false,
+    emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg',
+  };
+
+  state = {
+    visible: false,
+  };
+
+  onItemClick = (item, tabProps) => {
+    const { onItemClick } = this.props;
+
+    if (onItemClick) {
+      onItemClick(item, tabProps);
+    }
+  };
+
+  onClear = (name, key) => {
+    const { onClear } = this.props;
+
+    if (onClear) {
+      onClear(name, key);
+    }
+  };
+
+  onTabChange = tabType => {
+    const { onTabChange } = this.props;
+
+    if (onTabChange) {
+      onTabChange(tabType);
+    }
+  };
+
+  onViewMore = (tabProps, event) => {
+    const { onViewMore } = this.props;
+
+    if (onViewMore) {
+      onViewMore(tabProps, event);
+    }
+  };
+
+  getNotificationBox() {
+    const { children, loading, clearText, viewMoreText } = this.props;
+
+    if (!children) {
+      return null;
+    }
+
+    const panes = React.Children.map(children, child => {
+      if (!child) {
+        return null;
+      }
+
+      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 = msgCount > 0 ? `${title} (${msgCount})` : title;
+      return (
+        <TabPane tab={tabTitle} key={title}>
+          <NoticeList
+            clearText={clearText}
+            viewMoreText={viewMoreText}
+            data={list}
+            onClear={() => this.onClear(title, tabKey)}
+            onClick={item => this.onItemClick(item, child.props)}
+            onViewMore={event => this.onViewMore(child.props, event)}
+            showClear={showClear}
+            showViewMore={showViewMore}
+            title={title}
+            {...child.props}
+          />
+        </TabPane>
+      );
+    });
+    return (
+      <>
+        <Spin spinning={loading} delay={300}>
+          <Tabs className={styles.tabs} onChange={this.onTabChange}>
+            {panes}
+          </Tabs>
+        </Spin>
+      </>
+    );
+  }
+
+  handleVisibleChange = visible => {
+    const { onPopupVisibleChange } = this.props;
+    this.setState({
+      visible,
+    });
+
+    if (onPopupVisibleChange) {
+      onPopupVisibleChange(visible);
+    }
+  };
+
+  render() {
+    const { className, count, popupVisible, bell } = this.props;
+    const { visible } = this.state;
+    const noticeButtonClass = classNames(className, styles.noticeButton);
+    const notificationBox = this.getNotificationBox();
+    const NoticeBellIcon = bell || <Icon type="bell" 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;
+    }
+
+    const popoverProps = {};
+
+    if ('popupVisible' in this.props) {
+      popoverProps.visible = popupVisible;
+    }
+
+    return (
+      <HeaderDropdown
+        placement="bottomRight"
+        overlay={notificationBox}
+        overlayClassName={styles.popover}
+        trigger={['click']}
+        visible={visible}
+        onVisibleChange={this.handleVisibleChange}
+        {...popoverProps}
+      >
+        {trigger}
+      </HeaderDropdown>
+    );
+  }
+}

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

@@ -0,0 +1,31 @@
+@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-scroll {
+      text-align: center;
+    }
+    .ant-tabs-bar {
+      margin-bottom: 0;
+    }
+  }
+}

+ 16 - 0
src/components/PageLoading/index.jsx

@@ -0,0 +1,16 @@
+import React from 'react';
+import { Spin } from 'antd'; // loading components from code split
+// https://umijs.org/plugin/umi-plugin-react.html#dynamicimport
+
+const PageLoading = () => (
+  <div
+    style={{
+      paddingTop: 100,
+      textAlign: 'center',
+    }}
+  >
+    <Spin size="large" />
+  </div>
+);
+
+export default PageLoading;

+ 53 - 0
src/components/SelectLang/index.jsx

@@ -0,0 +1,53 @@
+import { Icon, Menu } from 'antd';
+import { formatMessage, getLocale, setLocale } from 'umi-plugin-react/locale';
+import React from 'react';
+import classNames from 'classnames';
+import HeaderDropdown from '../HeaderDropdown';
+import styles from './index.less';
+
+const SelectLang = props => {
+  const { className } = props;
+  const selectedLang = getLocale();
+
+  const changeLang = ({ key }) => setLocale(key);
+
+  const locales = ['zh-CN', 'zh-TW', 'en-US', 'pt-BR'];
+  const languageLabels = {
+    'zh-CN': '简体中文',
+    'zh-TW': '繁体中文',
+    'en-US': 'English',
+    'pt-BR': 'Português',
+  };
+  const languageIcons = {
+    'zh-CN': '🇨🇳',
+    'zh-TW': '🇭🇰',
+    'en-US': '🇺🇸',
+    'pt-BR': '🇧🇷',
+  };
+  const langMenu = (
+    <Menu className={styles.menu} selectedKeys={[selectedLang]} onClick={changeLang}>
+      {locales.map(locale => (
+        <Menu.Item key={locale}>
+          <span role="img" aria-label={languageLabels[locale]}>
+            {languageIcons[locale]}
+          </span>{' '}
+          {languageLabels[locale]}
+        </Menu.Item>
+      ))}
+    </Menu>
+  );
+  return (
+    <HeaderDropdown overlay={langMenu} placement="bottomRight">
+      <span className={classNames(styles.dropDown, className)}>
+        <Icon
+          type="global"
+          title={formatMessage({
+            id: 'navBar.lang',
+          })}
+        />
+      </span>
+    </HeaderDropdown>
+  );
+};
+
+export default SelectLang;

+ 24 - 0
src/components/SelectLang/index.less

@@ -0,0 +1,24 @@
+@import '~antd/es/style/themes/default.less';
+
+.menu {
+  :global(.anticon) {
+    margin-right: 8px;
+  }
+  :global(.ant-dropdown-menu-item) {
+    min-width: 160px;
+  }
+}
+
+.dropDown {
+  line-height: @layout-header-height;
+  vertical-align: top;
+  cursor: pointer;
+  > i {
+    font-size: 16px !important;
+    transform: none !important;
+    svg {
+      position: relative;
+      top: -1px;
+    }
+  }
+}

+ 35 - 0
src/components/SettingDrawer/themeColorClient.js

@@ -0,0 +1,35 @@
+// eslint-disable-next-line eslint-comments/disable-enable-pair
+
+/* eslint-disable import/no-extraneous-dependencies */
+import client from 'webpack-theme-color-replacer/client';
+import generate from '@ant-design/colors/lib/generate';
+
+export default {
+  getAntdSerials(color) {
+    const lightCount = 9;
+    const divide = 10; // 淡化(即less的tint)
+
+    let lightens = new Array(lightCount).fill(0);
+    lightens = lightens.map((_, i) => client.varyColor.lighten(color, i / divide));
+    const colorPalettes = generate(color);
+    const rgb = client.varyColor.toNum3(color.replace('#', '')).join(',');
+    return lightens.concat(colorPalettes).concat(rgb);
+  },
+
+  changeColor(color) {
+    if (!color) {
+      return Promise.resolve();
+    }
+
+    const options = {
+      // new colors array, one-to-one corresponde with `matchColors`
+      newColors: this.getAntdSerials(color),
+
+      changeUrl(cssUrl) {
+        // while router is not `hash` mode, it needs absolute path
+        return `/${cssUrl}`;
+      },
+    };
+    return client.changer.changeColor(options, Promise);
+  },
+};

+ 101 - 0
src/global.jsx

@@ -0,0 +1,101 @@
+import { Button, message, notification } from 'antd';
+import React from 'react';
+import { formatMessage } from 'umi-plugin-react/locale';
+import defaultSettings from '../config/defaultSettings';
+
+const { pwa } = defaultSettings; // if pwa is true
+
+if (pwa) {
+  // Notify user if offline now
+  window.addEventListener('sw.offline', () => {
+    message.warning(
+      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 => {
+    const e = event;
+
+    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],
+        );
+      });
+      window.location.reload(true);
+      return true;
+    };
+
+    const key = `open${Date.now()}`;
+    const btn = (
+      <Button
+        type="primary"
+        onClick={() => {
+          notification.close(key);
+          reloadSW();
+        }}
+      >
+        {formatMessage({
+          id: 'app.pwa.serviceworker.updated.ok',
+        })}
+      </Button>
+    );
+    notification.open({
+      message: formatMessage({
+        id: 'app.pwa.serviceworker.updated',
+      }),
+      description: formatMessage({
+        id: 'app.pwa.serviceworker.updated.hint',
+      }),
+      btn,
+      key,
+      onClose: async () => {},
+    });
+  });
+} else if ('serviceWorker' in navigator) {
+  // 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);
+      });
+    });
+  }
+}

+ 47 - 0
src/global.less

@@ -0,0 +1,47 @@
+@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;
+        }
+      }
+    }
+  }
+}

+ 193 - 0
src/layouts/BasicLayout.jsx

@@ -0,0 +1,193 @@
+/**
+ * Ant Design Pro v4 use `@ant-design/pro-layout` to handle Layout.
+ * You can view component api by:
+ * https://github.com/ant-design/ant-design-pro-layout
+ */
+import ProLayout, { DefaultFooter, SettingDrawer } from '@ant-design/pro-layout';
+import React, { useEffect } from 'react';
+import Link from 'umi/link';
+import { connect } from 'dva';
+import { Icon, Result, Button } from 'antd';
+import { formatMessage } from 'umi-plugin-react/locale';
+import Authorized from '@/utils/Authorized';
+import RightContent from '@/components/GlobalHeader/RightContent';
+import { isAntDesignPro, getAuthorityFromRouter } from '@/utils/utils';
+import logo from '../assets/logo.svg';
+
+const noMatch = (
+  <Result
+    status="403"
+    title="403"
+    subTitle="Sorry, you are not authorized to access this page."
+    extra={
+      <Button type="primary">
+        <Link to="/user/login">Go Login</Link>
+      </Button>
+    }
+  />
+);
+/**
+ * use Authorized check all menu item
+ */
+
+const menuDataRender = menuList =>
+  menuList.map(item => {
+    const localItem = { ...item, children: item.children ? menuDataRender(item.children) : [] };
+    return Authorized.check(item.authority, localItem, null);
+  });
+
+const defaultFooterDom = (
+  <DefaultFooter
+    copyright="2019 蚂蚁金服体验技术部出品"
+    links={[
+      {
+        key: 'Ant Design Pro',
+        title: 'Ant Design Pro',
+        href: 'https://pro.ant.design',
+        blankTarget: true,
+      },
+      {
+        key: 'github',
+        title: <Icon type="github" />,
+        href: 'https://github.com/ant-design/ant-design-pro',
+        blankTarget: true,
+      },
+      {
+        key: 'Ant Design',
+        title: 'Ant Design',
+        href: 'https://ant.design',
+        blankTarget: true,
+      },
+    ]}
+  />
+);
+
+const footerRender = () => {
+  if (!isAntDesignPro()) {
+    return defaultFooterDom;
+  }
+
+  return (
+    <>
+      {defaultFooterDom}
+      <div
+        style={{
+          padding: '0px 24px 24px',
+          textAlign: 'center',
+        }}
+      >
+        <a href="https://www.netlify.com" target="_blank" rel="noopener noreferrer">
+          <img
+            src="https://www.netlify.com/img/global/badges/netlify-color-bg.svg"
+            width="82px"
+            alt="netlify logo"
+          />
+        </a>
+      </div>
+    </>
+  );
+};
+
+const BasicLayout = props => {
+  const {
+    dispatch,
+    children,
+    settings,
+    location = {
+      pathname: '/',
+    },
+  } = props;
+  /**
+   * constructor
+   */
+
+  useEffect(() => {
+    if (dispatch) {
+      dispatch({
+        type: 'user/fetchCurrent',
+      });
+      dispatch({
+        type: 'settings/getSetting',
+      });
+    }
+  }, []);
+  /**
+   * init variables
+   */
+
+  const handleMenuCollapse = payload => {
+    if (dispatch) {
+      dispatch({
+        type: 'global/changeLayoutCollapsed',
+        payload,
+      });
+    }
+  }; // get children authority
+
+  const authorized = getAuthorityFromRouter(props.route.routes, location.pathname || '/') || {
+    authority: undefined,
+  };
+  return (
+    <>
+      <ProLayout
+        logo={logo}
+        menuHeaderRender={(logoDom, titleDom) => (
+          <Link to="/">
+            {logoDom}
+            {titleDom}
+          </Link>
+        )}
+        onCollapse={handleMenuCollapse}
+        menuItemRender={(menuItemProps, defaultDom) => {
+          if (menuItemProps.isUrl || menuItemProps.children) {
+            return defaultDom;
+          }
+
+          return <Link to={menuItemProps.path}>{defaultDom}</Link>;
+        }}
+        breadcrumbRender={(routers = []) => [
+          {
+            path: '/',
+            breadcrumbName: formatMessage({
+              id: 'menu.home',
+              defaultMessage: 'Home',
+            }),
+          },
+          ...routers,
+        ]}
+        itemRender={(route, params, routes, paths) => {
+          const first = routes.indexOf(route) === 0;
+          return first ? (
+            <Link to={paths.join('/')}>{route.breadcrumbName}</Link>
+          ) : (
+            <span>{route.breadcrumbName}</span>
+          );
+        }}
+        footerRender={footerRender}
+        menuDataRender={menuDataRender}
+        formatMessage={formatMessage}
+        rightContentRender={rightProps => <RightContent {...rightProps} />}
+        {...props}
+        {...settings}
+      >
+        <Authorized authority={authorized.authority} noMatch={noMatch}>
+          {children}
+        </Authorized>
+      </ProLayout>
+      <SettingDrawer
+        settings={settings}
+        onSettingChange={config =>
+          dispatch({
+            type: 'settings/changeSetting',
+            payload: config,
+          })
+        }
+      />
+    </>
+  );
+};
+
+export default connect(({ global, settings }) => ({
+  collapsed: global.collapsed,
+  settings,
+}))(BasicLayout);

+ 11 - 0
src/layouts/BlankLayout.jsx

@@ -0,0 +1,11 @@
+import React from 'react';
+import CopyBlock from '@/components/CopyBlock';
+
+const Layout = ({ children }) => (
+  <>
+    <div>{children}</div>
+    <CopyBlock id={Date.now()} />
+  </>
+);
+
+export default Layout;

+ 51 - 0
src/layouts/SecurityLayout.jsx

@@ -0,0 +1,51 @@
+import React from 'react';
+import { connect } from 'dva';
+import { Redirect } from 'umi';
+import { stringify } from 'querystring';
+import PageLoading from '@/components/PageLoading';
+
+class SecurityLayout extends React.Component {
+  state = {
+    isReady: false,
+  };
+
+  componentDidMount() {
+    this.setState({
+      isReady: true,
+    });
+    const { dispatch } = this.props;
+
+    if (dispatch) {
+      dispatch({
+        type: 'user/fetchCurrent',
+      });
+    }
+  }
+
+  render() {
+    const { isReady } = this.state;
+    const { children, loading, currentUser } = this.props;
+    // You can replace it to your authentication rule (such as check token exists)
+    // 你可以把它替换成你自己的登录认证规则(比如判断 token 是否存在)
+
+    const isLogin = currentUser && currentUser.userid;
+    const queryString = stringify({
+      redirect: window.location.href,
+    });
+
+    if ((!isLogin && loading) || !isReady) {
+      return <PageLoading />;
+    }
+
+    if (!isLogin) {
+      return <Redirect to={`/user/login?${queryString}`}></Redirect>;
+    }
+
+    return children;
+  }
+}
+
+export default connect(({ user, loading }) => ({
+  currentUser: user.currentUser,
+  loading: loading.models.user,
+}))(SecurityLayout);

+ 60 - 0
src/layouts/UserLayout.jsx

@@ -0,0 +1,60 @@
+import { DefaultFooter, getMenuData, getPageTitle } from '@ant-design/pro-layout';
+import { Helmet } from 'react-helmet';
+import Link from 'umi/link';
+import React from 'react';
+import { connect } from 'dva';
+import { formatMessage } from 'umi-plugin-react/locale';
+import SelectLang from '@/components/SelectLang';
+import logo from '../assets/logo.svg';
+import styles from './UserLayout.less';
+
+const UserLayout = props => {
+  const {
+    route = {
+      routes: [],
+    },
+  } = props;
+  const { routes = [] } = route;
+  const {
+    children,
+    location = {
+      pathname: '',
+    },
+  } = props;
+  const { breadcrumb } = getMenuData(routes);
+  const title = getPageTitle({
+    pathname: location.pathname,
+    breadcrumb,
+    formatMessage,
+    ...props,
+  });
+  return (
+    <>
+      <Helmet>
+        <title>{title}</title>
+        <meta name="description" content={title} />
+      </Helmet>
+
+      <div className={styles.container}>
+        <div className={styles.lang}>
+          <SelectLang />
+        </div>
+        <div className={styles.content}>
+          <div className={styles.top}>
+            <div className={styles.header}>
+              <Link to="/">
+                <img alt="logo" className={styles.logo} src={logo} />
+                <span className={styles.title}>Ant Design</span>
+              </Link>
+            </div>
+            <div className={styles.desc}>Ant Design 是西湖区最具影响力的 Web 设计规范</div>
+          </div>
+          {children}
+        </div>
+        <DefaultFooter />
+      </div>
+    </>
+  );
+};
+
+export default connect(({ settings }) => ({ ...settings }))(UserLayout);

+ 71 - 0
src/layouts/UserLayout.less

@@ -0,0 +1,71 @@
+@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;
+}

+ 22 - 0
src/locales/en-US.js

@@ -0,0 +1,22 @@
+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';
+
+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,
+};

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

@@ -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.js

@@ -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',
+};

+ 51 - 0
src/locales/en-US/menu.js

@@ -0,0 +1,51 @@
+export default {
+  'menu.welcome': 'Welcome',
+  'menu.more-blocks': 'More Blocks',
+  'menu.home': 'Home',
+  'menu.admin': 'admin',
+  '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',
+};

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

@@ -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.js

@@ -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.js

@@ -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',
+};

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

@@ -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.js

@@ -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.js

@@ -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',
+};

+ 51 - 0
src/locales/pt-BR/menu.js

@@ -0,0 +1,51 @@
+export default {
+  'menu.welcome': 'Welcome',
+  'menu.more-blocks': 'More Blocks',
+  'menu.home': 'Início',
+  'menu.login': 'Login',
+  'menu.admin': 'admin',
+  '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.js

@@ -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.js

@@ -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.js

@@ -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',
+};

+ 22 - 0
src/locales/zh-CN.js

@@ -0,0 +1,22 @@
+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';
+
+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 开发,快速构建标准页面',
+  ...globalHeader,
+  ...menu,
+  ...settingDrawer,
+  ...settings,
+  ...pwa,
+  ...component,
+};

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

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

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

@@ -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': '查看更多',
+};

+ 51 - 0
src/locales/zh-CN/menu.js

@@ -0,0 +1,51 @@
+export default {
+  'menu.welcome': '欢迎',
+  'menu.more-blocks': '更多区块',
+  'menu.home': '首页',
+  'menu.admin': '管理页',
+  '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': '拓扑编辑器',
+};

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

@@ -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.js

@@ -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.js

@@ -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.js

@@ -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.js

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

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

@@ -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': '查看更多',
+};

+ 51 - 0
src/locales/zh-TW/menu.js

@@ -0,0 +1,51 @@
+export default {
+  'menu.welcome': '歡迎',
+  'menu.more-blocks': '更多區塊',
+  'menu.home': '首頁',
+  'menu.login': '登錄',
+  'menu.admin': '权限',
+  '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.js

@@ -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.js

@@ -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.js

@@ -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"
+    }
+  ]
+}

+ 115 - 0
src/models/global.js

@@ -0,0 +1,115 @@
+import { queryNotices } from '@/services/user';
+
+const GlobalModel = {
+  namespace: 'global',
+  state: {
+    collapsed: false,
+    notices: [],
+  },
+  effects: {
+    *fetchNotices(_, { call, put, select }) {
+      const data = yield call(queryNotices);
+      yield put({
+        type: 'saveNotices',
+        payload: data,
+      });
+      const unreadCount = yield select(
+        state => state.global.notices.filter(item => !item.read).length,
+      );
+      yield put({
+        type: 'user/changeNotifyCount',
+        payload: {
+          totalCount: data.length,
+          unreadCount,
+        },
+      });
+    },
+
+    *clearNotices({ payload }, { put, select }) {
+      yield put({
+        type: 'saveClearedNotices',
+        payload,
+      });
+      const count = yield select(state => state.global.notices.length);
+      const unreadCount = yield select(
+        state => state.global.notices.filter(item => !item.read).length,
+      );
+      yield put({
+        type: 'user/changeNotifyCount',
+        payload: {
+          totalCount: count,
+          unreadCount,
+        },
+      });
+    },
+
+    *changeNoticeReadState({ payload }, { put, select }) {
+      const notices = yield select(state =>
+        state.global.notices.map(item => {
+          const notice = { ...item };
+
+          if (notice.id === payload) {
+            notice.read = true;
+          }
+
+          return notice;
+        }),
+      );
+      yield put({
+        type: 'saveNotices',
+        payload: notices,
+      });
+      yield put({
+        type: 'user/changeNotifyCount',
+        payload: {
+          totalCount: notices.length,
+          unreadCount: notices.filter(item => !item.read).length,
+        },
+      });
+    },
+  },
+  reducers: {
+    changeLayoutCollapsed(
+      state = {
+        notices: [],
+        collapsed: true,
+      },
+      { payload },
+    ) {
+      return { ...state, collapsed: payload };
+    },
+
+    saveNotices(state, { payload }) {
+      return {
+        collapsed: false,
+        ...state,
+        notices: payload,
+      };
+    },
+
+    saveClearedNotices(
+      state = {
+        notices: [],
+        collapsed: true,
+      },
+      { payload },
+    ) {
+      return {
+        collapsed: false,
+        ...state,
+        notices: state.notices.filter(item => item.type !== payload),
+      };
+    },
+  },
+  subscriptions: {
+    setup({ history }) {
+      // Subscribe history(url) change, trigger `load` action if pathname is `/`
+      history.listen(({ pathname, search }) => {
+        if (typeof window.ga !== 'undefined') {
+          window.ga('send', 'pageview', pathname + search);
+        }
+      });
+    },
+  },
+};
+export default GlobalModel;

+ 70 - 0
src/models/login.js

@@ -0,0 +1,70 @@
+import { routerRedux } from 'dva/router';
+import { stringify } from 'querystring';
+import { fakeAccountLogin, getFakeCaptcha } from '@/services/login';
+import { setAuthority } from '@/utils/authority';
+import { getPageQuery } from '@/utils/utils';
+
+const Model = {
+  namespace: 'login',
+  state: {
+    status: undefined,
+  },
+  effects: {
+    *login({ payload }, { call, put }) {
+      const response = yield call(fakeAccountLogin, payload);
+      yield put({
+        type: 'changeLoginStatus',
+        payload: response,
+      }); // Login successfully
+
+      if (response.status === 'ok') {
+        const urlParams = new URL(window.location.href);
+        const params = getPageQuery();
+        let { redirect } = params;
+
+        if (redirect) {
+          const redirectUrlParams = new URL(redirect);
+
+          if (redirectUrlParams.origin === urlParams.origin) {
+            redirect = redirect.substr(urlParams.origin.length);
+
+            if (redirect.match(/^\/.*#/)) {
+              redirect = redirect.substr(redirect.indexOf('#') + 1);
+            }
+          } else {
+            window.location.href = '/';
+            return;
+          }
+        }
+
+        yield put(routerRedux.replace(redirect || '/'));
+      }
+    },
+
+    *getCaptcha({ payload }, { call }) {
+      yield call(getFakeCaptcha, payload);
+    },
+
+    *logout(_, { put }) {
+      const { redirect } = getPageQuery(); // redirect
+
+      if (window.location.pathname !== '/user/login' && !redirect) {
+        yield put(
+          routerRedux.replace({
+            pathname: '/user/login',
+            search: stringify({
+              redirect: window.location.href,
+            }),
+          }),
+        );
+      }
+    },
+  },
+  reducers: {
+    changeLoginStatus(state, { payload }) {
+      setAuthority(payload.currentAuthority);
+      return { ...state, status: payload.status, type: payload.type };
+    },
+  },
+};
+export default Model;

+ 82 - 0
src/models/setting.js

@@ -0,0 +1,82 @@
+import { message } from 'antd';
+import defaultSettings from '../../config/defaultSettings';
+import themeColorClient from '../components/SettingDrawer/themeColorClient';
+
+const updateTheme = newPrimaryColor => {
+  if (newPrimaryColor) {
+    const timeOut = 0;
+    const hideMessage = message.loading('正在切换主题!', timeOut);
+    themeColorClient.changeColor(newPrimaryColor).finally(() => hideMessage());
+  }
+};
+
+const updateColorWeak = colorWeak => {
+  const root = document.getElementById('root');
+
+  if (root) {
+    root.className = colorWeak ? 'colorWeak' : '';
+  }
+};
+
+const SettingModel = {
+  namespace: 'settings',
+  state: defaultSettings,
+  reducers: {
+    getSetting(state = defaultSettings) {
+      const setting = {};
+      const urlParams = new URL(window.location.href);
+      Object.keys(state).forEach(key => {
+        if (urlParams.searchParams.has(key)) {
+          const value = urlParams.searchParams.get(key);
+          setting[key] = value === '1' ? true : value;
+        }
+      });
+      const { primaryColor, colorWeak } = setting;
+
+      if (primaryColor && state.primaryColor !== primaryColor) {
+        updateTheme(primaryColor);
+      }
+
+      updateColorWeak(!!colorWeak);
+      return { ...state, ...setting };
+    },
+
+    changeSetting(state = defaultSettings, { payload }) {
+      const urlParams = new URL(window.location.href);
+      Object.keys(defaultSettings).forEach(key => {
+        if (urlParams.searchParams.has(key)) {
+          urlParams.searchParams.delete(key);
+        }
+      });
+      Object.keys(payload).forEach(key => {
+        if (key === 'collapse') {
+          return;
+        }
+
+        let value = payload[key];
+
+        if (value === true) {
+          value = 1;
+        }
+
+        if (defaultSettings[key] !== value) {
+          urlParams.searchParams.set(key, value);
+        }
+      });
+      const { primaryColor, colorWeak, contentWidth } = payload;
+
+      if (primaryColor && state.primaryColor !== primaryColor) {
+        updateTheme(primaryColor);
+      }
+
+      if (state.contentWidth !== contentWidth && window.dispatchEvent) {
+        window.dispatchEvent(new Event('resize'));
+      }
+
+      updateColorWeak(!!colorWeak);
+      window.history.replaceState(null, 'setting', urlParams.href);
+      return { ...state, ...payload };
+    },
+  },
+};
+export default SettingModel;

+ 47 - 0
src/models/user.js

@@ -0,0 +1,47 @@
+import { queryCurrent, query as queryUsers } from '@/services/user';
+
+const UserModel = {
+  namespace: 'user',
+  state: {
+    currentUser: {},
+  },
+  effects: {
+    *fetch(_, { call, put }) {
+      const response = yield call(queryUsers);
+      yield put({
+        type: 'save',
+        payload: response,
+      });
+    },
+
+    *fetchCurrent(_, { call, put }) {
+      const response = yield call(queryCurrent);
+      yield put({
+        type: 'saveCurrentUser',
+        payload: response,
+      });
+    },
+  },
+  reducers: {
+    saveCurrentUser(state, action) {
+      return { ...state, currentUser: action.payload || {} };
+    },
+
+    changeNotifyCount(
+      state = {
+        currentUser: {},
+      },
+      action,
+    ) {
+      return {
+        ...state,
+        currentUser: {
+          ...state.currentUser,
+          notifyCount: action.payload.totalCount,
+          unreadCount: action.payload.unreadCount,
+        },
+      };
+    },
+  },
+};
+export default UserModel;

+ 19 - 0
src/pages/404.jsx

@@ -0,0 +1,19 @@
+import { Button, Result } from 'antd';
+import React from 'react';
+import router from 'umi/router'; // 这里应该使用 antd 的 404 result 组件,
+// 但是还没发布,先来个简单的。
+
+const NoFoundPage = () => (
+  <Result
+    status="404"
+    title="404"
+    subTitle="Sorry, the page you visited does not exist."
+    extra={
+      <Button type="primary" onClick={() => router.push('/')}>
+        Back Home
+      </Button>
+    }
+  ></Result>
+);
+
+export default NoFoundPage;

+ 41 - 0
src/pages/Admin.jsx

@@ -0,0 +1,41 @@
+import React from 'react';
+import { Card, Typography, Alert, Icon } from 'antd';
+import { PageHeaderWrapper } from '@ant-design/pro-layout';
+
+export default () => (
+  <PageHeaderWrapper content=" 这个页面只有 admin 权限才能查看">
+    <Card>
+      <Alert
+        message="umi ui 现已发布,欢迎使用 npm run ui 启动体验。"
+        type="success"
+        showIcon
+        banner
+        style={{
+          margin: -12,
+          marginBottom: 48,
+        }}
+      />
+      <Typography.Title
+        level={2}
+        style={{
+          textAlign: 'center',
+        }}
+      >
+        <Icon type="smile" theme="twoTone" /> Ant Design Pro{' '}
+        <Icon type="heart" theme="twoTone" 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>
+);

+ 53 - 0
src/pages/Authorized.jsx

@@ -0,0 +1,53 @@
+import React from 'react';
+import Redirect from 'umi/redirect';
+import { connect } from 'dva';
+import pathToRegexp from 'path-to-regexp';
+import Authorized from '@/utils/Authorized';
+
+const getRouteAuthority = (path, routeData) => {
+  let authorities;
+  routeData.forEach(route => {
+    if (route.authority) {
+      authorities = route.authority;
+    } // match prefix
+
+    if (pathToRegexp(`${route.path}(.*)`).test(path)) {
+      // exact match
+      if (route.path === path) {
+        authorities = route.authority || authorities;
+      } // get children authority recursively
+
+      if (route.routes) {
+        authorities = getRouteAuthority(path, route.routes) || authorities;
+      }
+    }
+  });
+  return authorities;
+};
+
+const AuthComponent = ({
+  children,
+  route = {
+    routes: [],
+  },
+  location = {
+    pathname: '',
+  },
+  user,
+}) => {
+  const { currentUser } = user;
+  const { routes = [] } = route;
+  const isLogin = currentUser && currentUser.name;
+  return (
+    <Authorized
+      authority={getRouteAuthority(location.pathname, routes) || ''}
+      noMatch={isLogin ? <Redirect to="/exception/403" /> : <Redirect to="/user/login" />}
+    >
+      {children}
+    </Authorized>
+  );
+};
+
+export default connect(({ user }) => ({
+  user,
+}))(AuthComponent);

+ 71 - 0
src/pages/Welcome.jsx

@@ -0,0 +1,71 @@
+import React from 'react';
+import { Card, Typography, Alert } from 'antd';
+import { PageHeaderWrapper } from '@ant-design/pro-layout';
+import { FormattedMessage } from 'umi-plugin-react/locale';
+
+const CodePreview = ({ children }) => (
+  <pre
+    style={{
+      background: '#f2f4f5',
+      padding: '12px 20px',
+      margin: '12px 0',
+    }}
+  >
+    <code>
+      <Typography.Text copyable>{children}</Typography.Text>
+    </code>
+  </pre>
+);
+
+export default () => (
+  <PageHeaderWrapper>
+    <Card>
+      <Alert
+        message="umi ui 现已发布,欢迎使用 npm run ui 启动体验。"
+        type="success"
+        showIcon
+        banner
+        style={{
+          margin: -12,
+          marginBottom: 24,
+        }}
+      />
+      <Typography.Text strong>
+        <a target="_blank" rel="noopener noreferrer" href="https://pro.ant.design/docs/block">
+          <FormattedMessage
+            id="app.welcome.link.block-list"
+            defaultMessage="基于 block 开发,快速构建标准页面"
+          />
+        </a>
+      </Typography.Text>
+      <CodePreview>npx umi block list</CodePreview>
+      <Typography.Text
+        strong
+        style={{
+          marginBottom: 12,
+        }}
+      >
+        <a
+          target="_blank"
+          rel="noopener noreferrer"
+          href="https://pro.ant.design/docs/available-script#npm-run-fetchblocks"
+        >
+          <FormattedMessage id="app.welcome.link.fetch-blocks" defaultMessage="获取全部区块" />
+        </a>
+      </Typography.Text>
+      <CodePreview> npm run fetch:blocks</CodePreview>
+    </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>
+);

+ 99 - 0
src/pages/account/center/Center.less

@@ -0,0 +1,99 @@
+@import '~antd/es/style/themes/default.less';
+
+.avatarHolder {
+  margin-bottom: 24px;
+  text-align: center;
+
+  & > img {
+    width: 104px;
+    height: 104px;
+    margin-bottom: 20px;
+  }
+
+  .name {
+    margin-bottom: 4px;
+    color: @heading-color;
+    font-weight: 500;
+    font-size: 20px;
+    line-height: 28px;
+  }
+}
+
+.detail {
+  p {
+    position: relative;
+    margin-bottom: 8px;
+    padding-left: 26px;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+
+  i {
+    position: absolute;
+    top: 4px;
+    left: 0;
+    width: 14px;
+    height: 14px;
+    background: url(https://gw.alipayobjects.com/zos/rmsportal/pBjWzVAHnOOtAUvZmZfy.svg);
+
+    &.title {
+      background-position: 0 0;
+    }
+
+    &.group {
+      background-position: 0 -22px;
+    }
+
+    &.address {
+      background-position: 0 -44px;
+    }
+  }
+}
+
+.tagsTitle,
+.teamTitle {
+  margin-bottom: 12px;
+  color: @heading-color;
+  font-weight: 500;
+}
+
+.tags {
+  :global {
+    .ant-tag {
+      margin-bottom: 8px;
+    }
+  }
+}
+
+.team {
+  :global {
+    .ant-avatar {
+      margin-right: 12px;
+    }
+  }
+
+  a {
+    display: block;
+    margin-bottom: 24px;
+    overflow: hidden;
+    color: @text-color;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    word-break: break-all;
+    transition: color 0.3s;
+
+    &:hover {
+      color: @primary-color;
+    }
+  }
+}
+
+.tabsCard {
+  :global {
+    .ant-card-head {
+      padding: 0 16px;
+    }
+  }
+}

+ 218 - 0
src/pages/account/center/_mock.js

@@ -0,0 +1,218 @@
+const titles = [
+  'Alipay',
+  'Angular',
+  'Ant Design',
+  'Ant Design Pro',
+  'Bootstrap',
+  'React',
+  'Vue',
+  'Webpack',
+];
+const avatars = [
+  'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png', // Alipay
+  'https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png', // Angular
+  'https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png', // Ant Design
+  'https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png', // Ant Design Pro
+  'https://gw.alipayobjects.com/zos/rmsportal/siCrBXXhmvTQGWPNLBow.png', // Bootstrap
+  'https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png', // React
+  'https://gw.alipayobjects.com/zos/rmsportal/ComBAopevLwENQdKWiIn.png', // Vue
+  'https://gw.alipayobjects.com/zos/rmsportal/nxkuOJlFJuAUhzlMTCEe.png', // Webpack
+];
+const covers = [
+  'https://gw.alipayobjects.com/zos/rmsportal/uMfMFlvUuceEyPpotzlq.png',
+  'https://gw.alipayobjects.com/zos/rmsportal/iZBVOIhGJiAnhplqjvZW.png',
+  'https://gw.alipayobjects.com/zos/rmsportal/iXjVmWVHbCJAyqvDxdtx.png',
+  'https://gw.alipayobjects.com/zos/rmsportal/gLaIAoVWTtLbBWZNYEMg.png',
+];
+const desc = [
+  '那是一种内在的东西, 他们到达不了,也无法触及的',
+  '希望是一个好东西,也许是最好的,好东西是不会消亡的',
+  '生命就像一盒巧克力,结果往往出人意料',
+  '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
+  '那时候我只会想自己想要什么,从不想自己拥有什么',
+];
+const user = [
+  '付小小',
+  '曲丽丽',
+  '林东东',
+  '周星星',
+  '吴加好',
+  '朱偏右',
+  '鱼酱',
+  '乐哥',
+  '谭小仪',
+  '仲尼',
+];
+
+function fakeList(count) {
+  const list = [];
+
+  for (let i = 0; i < count; i += 1) {
+    list.push({
+      id: `fake-list-${i}`,
+      owner: user[i % 10],
+      title: titles[i % 8],
+      avatar: avatars[i % 8],
+      cover: parseInt(`${i / 4}`, 10) % 2 === 0 ? covers[i % 4] : covers[3 - (i % 4)],
+      status: ['active', 'exception', 'normal'][i % 3],
+      percent: Math.ceil(Math.random() * 50) + 50,
+      logo: avatars[i % 8],
+      href: 'https://ant.design',
+      updatedAt: new Date(new Date().getTime() - 1000 * 60 * 60 * 2 * i).getTime(),
+      createdAt: new Date(new Date().getTime() - 1000 * 60 * 60 * 2 * i).getTime(),
+      subDescription: desc[i % 5],
+      description:
+        '在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。',
+      activeUser: Math.ceil(Math.random() * 100000) + 100000,
+      newUser: Math.ceil(Math.random() * 1000) + 1000,
+      star: Math.ceil(Math.random() * 100) + 100,
+      like: Math.ceil(Math.random() * 100) + 100,
+      message: Math.ceil(Math.random() * 10) + 10,
+      content:
+        '段落示意:蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。',
+      members: [
+        {
+          avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ZiESqWwCXBRQoaPONSJe.png',
+          name: '曲丽丽',
+          id: 'member1',
+        },
+        {
+          avatar: 'https://gw.alipayobjects.com/zos/rmsportal/tBOxZPlITHqwlGjsJWaF.png',
+          name: '王昭君',
+          id: 'member2',
+        },
+        {
+          avatar: 'https://gw.alipayobjects.com/zos/rmsportal/sBxjgqiuHMGRkIjqlQCd.png',
+          name: '董娜娜',
+          id: 'member3',
+        },
+      ],
+    });
+  }
+
+  return list;
+}
+
+function getFakeList(req, res) {
+  const params = req.query;
+  const count = params.count * 1 || 5;
+  const result = fakeList(count);
+  return res.json(result);
+}
+
+export default {
+  'GET  /api/fake_list': getFakeList,
+  // 支持值为 Object 和 Array
+  'GET  /api/currentUser': {
+    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: '海纳百川',
+      },
+    ],
+    notice: [
+      {
+        id: 'xxx1',
+        title: titles[0],
+        logo: avatars[0],
+        description: '那是一种内在的东西,他们到达不了,也无法触及的',
+        updatedAt: new Date(),
+        member: '科学搬砖组',
+        href: '',
+        memberLink: '',
+      },
+      {
+        id: 'xxx2',
+        title: titles[1],
+        logo: avatars[1],
+        description: '希望是一个好东西,也许是最好的,好东西是不会消亡的',
+        updatedAt: new Date('2017-07-24'),
+        member: '全组都是吴彦祖',
+        href: '',
+        memberLink: '',
+      },
+      {
+        id: 'xxx3',
+        title: titles[2],
+        logo: avatars[2],
+        description: '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
+        updatedAt: new Date(),
+        member: '中二少女团',
+        href: '',
+        memberLink: '',
+      },
+      {
+        id: 'xxx4',
+        title: titles[3],
+        logo: avatars[3],
+        description: '那时候我只会想自己想要什么,从不想自己拥有什么',
+        updatedAt: new Date('2017-07-23'),
+        member: '程序员日常',
+        href: '',
+        memberLink: '',
+      },
+      {
+        id: 'xxx5',
+        title: titles[4],
+        logo: avatars[4],
+        description: '凛冬将至',
+        updatedAt: new Date('2017-07-23'),
+        member: '高逼格设计天团',
+        href: '',
+        memberLink: '',
+      },
+      {
+        id: 'xxx6',
+        title: titles[5],
+        logo: avatars[5],
+        description: '生命就像一盒巧克力,结果往往出人意料',
+        updatedAt: new Date('2017-07-23'),
+        member: '骗你来学计算机',
+        href: '',
+        memberLink: '',
+      },
+    ],
+    notifyCount: 12,
+    unreadCount: 11,
+    country: 'China',
+    geographic: {
+      province: {
+        label: '浙江省',
+        key: '330000',
+      },
+      city: {
+        label: '杭州市',
+        key: '330100',
+      },
+    },
+    address: '西湖区工专路 77 号',
+    phone: '0752-268888888',
+  },
+};

+ 124 - 0
src/pages/account/center/components/Applications/index.jsx

@@ -0,0 +1,124 @@
+import { Avatar, Card, Dropdown, Icon, List, Menu, Tooltip } from 'antd';
+import React, { Component } from 'react';
+import { connect } from 'dva';
+import numeral from 'numeral';
+import stylesApplications from './index.less';
+
+export function formatWan(val) {
+  const v = val * 1;
+  if (!v || Number.isNaN(v)) return '';
+  let result = val;
+
+  if (val > 10000) {
+    result = (
+      <span>
+        {Math.floor(val / 10000)}
+        <span
+          style={{
+            position: 'relative',
+            top: -2,
+            fontSize: 14,
+            fontStyle: 'normal',
+            marginLeft: 2,
+          }}
+        >
+          万
+        </span>
+      </span>
+    );
+  }
+
+  return result;
+}
+
+@connect(({ accountAndcenter }) => ({
+  list: accountAndcenter.list,
+}))
+class Applications extends Component {
+  render() {
+    const { list } = this.props;
+    const itemMenu = (
+      <Menu>
+        <Menu.Item>
+          <a target="_blank" rel="noopener noreferrer" href="https://www.alipay.com/">
+            1st menu item
+          </a>
+        </Menu.Item>
+        <Menu.Item>
+          <a target="_blank" rel="noopener noreferrer" href="https://www.taobao.com/">
+            2nd menu item
+          </a>
+        </Menu.Item>
+        <Menu.Item>
+          <a target="_blank" rel="noopener noreferrer" href="https://www.tmall.com/">
+            3d menu item
+          </a>
+        </Menu.Item>
+      </Menu>
+    );
+
+    const CardInfo = ({ activeUser, newUser }) => (
+      <div className={stylesApplications.cardInfo}>
+        <div>
+          <p>活跃用户</p>
+          <p>{activeUser}</p>
+        </div>
+        <div>
+          <p>新增用户</p>
+          <p>{newUser}</p>
+        </div>
+      </div>
+    );
+
+    return (
+      <List
+        rowKey="id"
+        className={stylesApplications.filterCardList}
+        grid={{
+          gutter: 24,
+          xxl: 3,
+          xl: 2,
+          lg: 2,
+          md: 2,
+          sm: 2,
+          xs: 1,
+        }}
+        dataSource={list}
+        renderItem={item => (
+          <List.Item key={item.id}>
+            <Card
+              hoverable
+              bodyStyle={{
+                paddingBottom: 20,
+              }}
+              actions={[
+                <Tooltip key="download" title="下载">
+                  <Icon type="download" />
+                </Tooltip>,
+                <Tooltip title="编辑" key="edit">
+                  <Icon type="edit" />
+                </Tooltip>,
+                <Tooltip title="分享" key="share">
+                  <Icon type="share-alt" />
+                </Tooltip>,
+                <Dropdown overlay={itemMenu} key="ellipsis">
+                  <Icon type="ellipsis" />
+                </Dropdown>,
+              ]}
+            >
+              <Card.Meta avatar={<Avatar size="small" src={item.avatar} />} title={item.title} />
+              <div className={stylesApplications.cardItemContent}>
+                <CardInfo
+                  activeUser={formatWan(item.activeUser)}
+                  newUser={numeral(item.newUser).format('0,0')}
+                />
+              </div>
+            </Card>
+          </List.Item>
+        )}
+      />
+    );
+  }
+}
+
+export default Applications;

+ 51 - 0
src/pages/account/center/components/Applications/index.less

@@ -0,0 +1,51 @@
+@import '~antd/es/style/themes/default.less';
+
+.filterCardList {
+  margin-bottom: -24px;
+  :global {
+    .ant-card-meta-content {
+      margin-top: 0;
+    }
+    // disabled white space
+    .ant-card-meta-avatar {
+      font-size: 0;
+    }
+
+    .ant-list .ant-list-item-content-single {
+      max-width: 100%;
+    }
+  }
+  .cardInfo {
+    margin-top: 16px;
+    margin-left: 40px;
+    zoom: 1;
+    &::before,
+    &::after {
+      display: table;
+      content: ' ';
+    }
+    &::after {
+      clear: both;
+      height: 0;
+      font-size: 0;
+      visibility: hidden;
+    }
+    & > div {
+      position: relative;
+      float: left;
+      width: 50%;
+      text-align: left;
+      p {
+        margin: 0;
+        font-size: 24px;
+        line-height: 32px;
+      }
+      p:first-child {
+        margin-bottom: 4px;
+        color: @text-color-secondary;
+        font-size: 12px;
+        line-height: 20px;
+      }
+    }
+  }
+}

+ 17 - 0
src/pages/account/center/components/ArticleListContent/index.jsx

@@ -0,0 +1,17 @@
+import { Avatar } from 'antd';
+import React from 'react';
+import moment from 'moment';
+import styles from './index.less';
+
+const ArticleListContent = ({ data: { content, updatedAt, avatar, owner, href } }) => (
+  <div className={styles.listContent}>
+    <div className={styles.description}>{content}</div>
+    <div className={styles.extra}>
+      <Avatar src={avatar} size="small" />
+      <a href={href}>{owner}</a> 发布在 <a href={href}>{href}</a>
+      <em>{moment(updatedAt).format('YYYY-MM-DD HH:mm')}</em>
+    </div>
+  </div>
+);
+
+export default ArticleListContent;

+ 38 - 0
src/pages/account/center/components/ArticleListContent/index.less

@@ -0,0 +1,38 @@
+@import '~antd/es/style/themes/default.less';
+
+.listContent {
+  .description {
+    max-width: 720px;
+    line-height: 22px;
+  }
+  .extra {
+    margin-top: 16px;
+    color: @text-color-secondary;
+    line-height: 22px;
+    & > :global(.ant-avatar) {
+      position: relative;
+      top: 1px;
+      width: 20px;
+      height: 20px;
+      margin-right: 8px;
+      vertical-align: top;
+    }
+    & > em {
+      margin-left: 16px;
+      color: @disabled-color;
+      font-style: normal;
+    }
+  }
+}
+
+@media screen and (max-width: @screen-xs) {
+  .listContent {
+    .extra {
+      & > em {
+        display: block;
+        margin-top: 8px;
+        margin-left: 0;
+      }
+    }
+  }
+}

+ 64 - 0
src/pages/account/center/components/Articles/index.jsx

@@ -0,0 +1,64 @@
+import { Icon, List, Tag } from 'antd';
+import React, { Component } from 'react';
+import { connect } from 'dva';
+import ArticleListContent from '../ArticleListContent';
+import styles from './index.less';
+
+@connect(({ accountAndcenter }) => ({
+  list: accountAndcenter.list,
+}))
+class Articles extends Component {
+  render() {
+    const { list } = this.props;
+
+    const IconText = ({ type, text }) => (
+      <span>
+        <Icon
+          type={type}
+          style={{
+            marginRight: 8,
+          }}
+        />
+        {text}
+      </span>
+    );
+
+    return (
+      <List
+        size="large"
+        className={styles.articleList}
+        rowKey="id"
+        itemLayout="vertical"
+        dataSource={list}
+        renderItem={item => (
+          <List.Item
+            key={item.id}
+            actions={[
+              <IconText key="star" type="star-o" text={item.star} />,
+              <IconText key="like" type="like-o" text={item.like} />,
+              <IconText key="message" type="message" text={item.message} />,
+            ]}
+          >
+            <List.Item.Meta
+              title={
+                <a className={styles.listItemMetaTitle} href={item.href}>
+                  {item.title}
+                </a>
+              }
+              description={
+                <span>
+                  <Tag>Ant Design</Tag>
+                  <Tag>设计语言</Tag>
+                  <Tag>蚂蚁金服</Tag>
+                </span>
+              }
+            />
+            <ArticleListContent data={item} />
+          </List.Item>
+        )}
+      />
+    );
+  }
+}
+
+export default Articles;

+ 0 - 0
src/pages/account/center/components/Articles/index.less


部分文件因为文件数量过多而无法显示