Przeglądaj źródła

V4 fetch block (#4238)

* first step

* fix local error

* commit

* test serve

* fix test

* sort package.json

* fix typo

* fix pageHeader error

* new style

* change script name

* fix copy block url style

* new copy code style

* add loginout

* auto insert pro code

* use new layout

* add locale to copy button
陈帅 6 lat temu
rodzic
commit
41719aa59f

+ 5 - 2
.stylelintrc.json

@@ -5,9 +5,12 @@
     "stylelint-config-rational-order",
     "stylelint-config-prettier"
   ],
-  "plugins": ["stylelint-order", "stylelint-declaration-block-no-ignored-properties"],
+  "plugins": [
+    "stylelint-order",
+    "stylelint-declaration-block-no-ignored-properties"
+  ],
   "rules": {
     "no-descending-specificity": null,
     "plugin/declaration-block-no-ignored-properties": true
   }
-}
+}

+ 11 - 10
config/plugin.config.ts

@@ -4,7 +4,7 @@ import MergeLessPlugin from 'antd-pro-merge-less';
 import AntDesignThemePlugin from 'antd-theme-webpack-plugin';
 import path from 'path';
 
-function getModulePackageName(module) {
+function getModulePackageName(module: { context: string }) {
   if (!module.context) return null;
 
   const nodeModulesPath = path.join(__dirname, '../node_modules/');
@@ -14,16 +14,16 @@ function getModulePackageName(module) {
 
   const moduleRelativePath = module.context.substring(nodeModulesPath.length);
   const [moduleDirName] = moduleRelativePath.split(path.sep);
-  let packageName = moduleDirName;
+  let packageName: string | null = moduleDirName;
   // handle tree shaking
-  if (packageName.match('^_')) {
+  if (packageName && packageName.match('^_')) {
     // eslint-disable-next-line prefer-destructuring
-    packageName = packageName.match(/^_(@?[^@]+)/)[1];
+    packageName = packageName.match(/^_(@?[^@]+)/)![1];
   }
   return packageName;
 }
 
-export default config => {
+export default (config: any) => {
   // preview.pro.ant.design only do not use in your production ; preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。
   if (
     process.env.ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site' ||
@@ -62,18 +62,19 @@ export default config => {
       minSize: 0,
       cacheGroups: {
         vendors: {
-          test: module => {
+          test: (module: { context: string }) => {
             const packageName = getModulePackageName(module);
             if (packageName) {
               return ['bizcharts', '@antv_data-set'].indexOf(packageName) >= 0;
             }
             return false;
           },
-          name(module) {
+          name(module: { context: string }) {
             const packageName = getModulePackageName(module);
-
-            if (['bizcharts', '@antv_data-set'].indexOf(packageName) >= 0) {
-              return 'viz'; // visualization package
+            if (packageName) {
+              if (['bizcharts', '@antv_data-set'].indexOf(packageName) >= 0) {
+                return 'viz'; // visualization package
+              }
             }
             return 'misc';
           },

+ 13 - 12
package.json

@@ -13,6 +13,7 @@
     "docker:dev": "docker-compose -f ./docker/docker-compose.dev.yml up",
     "docker:push": "npm run docker-hub:build && npm run docker:tag && docker push antdesign/ant-design-pro",
     "docker:tag": "docker tag ant-design-pro antdesign/ant-design-pro",
+    "fetch:blocks": "node ./scripts/fetch-blocks.js",
     "functions:build": "npm run generateMock && netlify-lambda build ./lambda",
     "functions:run": "npm run generateMock && cross-env NODE_ENV=dev netlify-lambda serve ./lambda",
     "generateMock": "node ./scripts/generateMock",
@@ -26,7 +27,7 @@
     "lint:style": "stylelint --fix 'src/**/*.less' --syntax less",
     "lint:ts": "tslint -p . -c tslint.yml",
     "prettier": " check-prettier write",
-    "site": "umi build && npm run functions:build",
+    "site": "npm run fetch:blocks && umi build && npm run functions:build",
     "start": "umi dev",
     "start:no-mock": "cross-env MOCK=none umi dev",
     "test": "umi test",
@@ -49,18 +50,17 @@
   "dependencies": {
     "@ant-design/pro-layout": "^4.2.0",
     "@antv/data-set": "^0.10.1",
+    "@types/qs": "^6.5.3",
     "antd": "^3.16.1",
-    "bizcharts": "^3.5.3-beta.0",
-    "bizcharts-plugin-slider": "^2.1.1-beta.1",
     "classnames": "^2.2.6",
     "dva": "^2.4.0",
     "lodash": "^4.17.10",
     "lodash-decorators": "^6.0.0",
     "memoize-one": "^5.0.0",
     "moment": "^2.22.2",
-    "numeral": "^2.0.6",
     "omit.js": "^1.0.0",
     "path-to-regexp": "^2.4.0",
+    "prop-types": "^15.7.2",
     "qs": "^6.7.0",
     "rc-animate": "^2.4.4",
     "react": "^16.8.5",
@@ -68,9 +68,14 @@
     "react-copy-to-clipboard": "^5.0.1",
     "react-document-title": "^2.0.3",
     "react-dom": "^16.7.0",
-    "react-fittext": "^1.0.0",
     "react-media": "^1.9.2",
-    "react-media-hook2": "^1.0.2"
+    "react-media-hook2": "^1.0.2",
+    "umi": "^2.7.0-beta.2",
+    "umi-plugin-ga": "^1.1.3",
+    "umi-plugin-locale": "^2.8.0-beta.1",
+    "umi-plugin-pro-block": "^1.3.0",
+    "umi-plugin-react": "^1.8.0-beta.1",
+    "umi-request": "^1.0.7"
   },
   "devDependencies": {
     "@types/classnames": "^2.2.7",
@@ -106,6 +111,7 @@
     "merge-umi-mock-data": "^1.0.4",
     "mockjs": "^1.0.1-beta3",
     "netlify-lambda": "^1.4.3",
+    "node-fetch": "^2.6.0",
     "prettier": "^1.17.0",
     "serverless-http": "^2.0.1",
     "slash2": "^2.0.0",
@@ -119,12 +125,7 @@
     "tslint": "^5.12.1",
     "tslint-config-prettier": "^1.17.0",
     "tslint-eslint-rules": "^5.4.0",
-    "tslint-react": "^3.6.0",
-    "umi": "^2.7.0-beta.2",
-    "umi-plugin-ga": "^1.1.3",
-    "umi-plugin-pro-block": "^1.3.0",
-    "umi-plugin-react": "^1.8.0-beta.1",
-    "umi-request": "^1.0.0"
+    "tslint-react": "^3.6.0"
   },
   "optionalDependencies": {
     "puppeteer": "^1.12.1"

+ 129 - 0
scripts/fetch-blocks.js

@@ -0,0 +1,129 @@
+const path = require('path');
+const fs = require('fs');
+const fetch = require('node-fetch');
+const exec = require('child_process').exec;
+const getNewRouteCode = require('./repalceRouter');
+const router = require('./router.config');
+const chalk = require('chalk');
+const insertCode = require('./insertCode');
+
+const fetchGithubFiles = async () => {
+  const ignoreFile = ['_scripts'];
+  const data = await fetch(`https://api.github.com/repos/ant-design/pro-blocks/git/trees/master`);
+  if (data.status !== 200) {
+    return;
+  }
+  const { tree } = await data.json();
+  const files = tree.filter(file => file.type === 'tree' && !ignoreFile.includes(file.path));
+  return Promise.resolve(files);
+};
+
+const relativePath = path.join(__dirname, '../config/config.ts');
+
+const findAllInstallRouter = router => {
+  let routers = [];
+  router.forEach(item => {
+    if (item.component && item.path) {
+      if (item.path !== '/user' || item.path !== '/') {
+        routers.push({
+          ...item,
+          routes: !!item.routes,
+        });
+      }
+    }
+    if (item.routes) {
+      routers = routers.concat(findAllInstallRouter(item.routes));
+    }
+  });
+  return routers;
+};
+
+const filterParentRouter = (router, layout) => {
+  return [...router]
+    .map(item => {
+      if (item.routes && (!router.component || layout)) {
+        return { ...item, routes: filterParentRouter(item.routes, false) };
+      }
+      if (item.redirect) {
+        return item;
+      }
+      return null;
+    })
+    .filter(item => item);
+};
+const firstUpperCase = pathString => {
+  return pathString
+    .replace('.', '')
+    .split(/\/|\-/)
+    .map(s => s.toLowerCase().replace(/( |^)[a-z]/g, L => L.toUpperCase()))
+    .filter(s => s)
+    .join('');
+};
+
+const execCmd = shell => {
+  return new Promise((resolve, reject) => {
+    exec(shell, { encoding: 'utf8' }, (error, statusbar) => {
+      if (error) {
+        console.log(error);
+        return reject(error);
+      }
+      console.log(statusbar);
+      resolve();
+    });
+  });
+};
+
+// replace router config
+const parentRouter = filterParentRouter(router, true);
+const { routesPath, code } = getNewRouteCode(relativePath, parentRouter);
+// write ParentRouter
+fs.writeFileSync(routesPath, code);
+
+const installBlock = async () => {
+  let gitFiles = await fetchGithubFiles();
+  const installRouters = findAllInstallRouter(router);
+  const installBlockIteration = async i => {
+    const item = installRouters[i];
+
+    if (!item || !item.path) {
+      return Promise.resolve();
+    }
+    const gitPath = firstUpperCase(item.path);
+    // 如果这个区块在 git 上存在
+    if (gitFiles.find(file => file.path === gitPath)) {
+      console.log('install ' + chalk.green(item.name) + ' to: ' + chalk.yellow(item.path));
+      gitFiles = gitFiles.filter(file => file.path !== gitPath);
+      const skipModifyRouter = item.routes ? '--skip-modify-routes' : '';
+      const cmd = `umi block add https://github.com/ant-design/pro-blocks/tree/master/${gitPath} --npm-client=cnpm  --path=${
+        item.path
+      } ${skipModifyRouter}`;
+      try {
+        await execCmd(cmd);
+        console.log(`install ${chalk.hex('#1890ff')(item.name)} success`);
+      } catch (error) {
+        console.error(error);
+      }
+    }
+    return installBlockIteration(i + 1);
+  };
+  // 安装路由中设置的区块
+  await installBlockIteration(0);
+
+  const installGitFile = async i => {
+    const item = gitFiles[i];
+    if (!item || !item.path) {
+      return Promise.resolve();
+    }
+    console.log('install ' + chalk.green(item.path));
+    const cmd = `umi block add https://github.com/ant-design/pro-blocks/tree/master/${item.path}`;
+    await execCmd(cmd);
+    return installBlockIteration(1);
+  };
+
+  // 安装 router 中没有的剩余区块.
+  installGitFile(0);
+};
+installBlock();
+
+// 插入 pro 需要的演示代码
+insertCode();

+ 161 - 0
scripts/insertCode.js

@@ -0,0 +1,161 @@
+const parser = require('@babel/parser');
+const traverse = require('@babel/traverse');
+const generate = require('@babel/generator');
+const t = require('@babel/types');
+const fs = require('fs');
+const path = require('path');
+const prettier = require('prettier');
+const chalk = require('chalk');
+
+const parseCode = code => {
+  return parser.parse(code, {
+    sourceType: 'module',
+    plugins: ['typescript', 'jsx'],
+  }).program.body[0];
+};
+
+/**
+ * 生成代码
+ * @param {*} ast
+ */
+function generateCode(ast) {
+  const newCode = generate.default(ast, {}).code;
+  return prettier.format(newCode, {
+    // format same as ant-design-pro
+    singleQuote: true,
+    trailingComma: 'es5',
+    printWidth: 100,
+    parser: 'typescript',
+  });
+}
+
+const SettingCodeString = `
+  <SettingDrawer
+    settings={settings}
+    onSettingChange={config =>
+    dispatch!({
+        type: 'settings/changeSetting',
+        payload: config,
+    })
+    }
+  />
+`;
+
+const mapAst = (configPath, callBack) => {
+  const ast = parser.parse(fs.readFileSync(configPath, 'utf-8'), {
+    sourceType: 'module',
+    plugins: ['typescript', 'jsx'],
+  });
+  // 查询当前配置文件是否导出 routes 属性
+  traverse.default(ast, {
+    Program({ node }) {
+      const { body } = node;
+      callBack(body);
+    },
+  });
+  return generateCode(ast);
+};
+
+const insertBasicLayout = configPath => {
+  return mapAst(configPath, body => {
+    const index = body.findIndex(item => {
+      return item.type !== 'ImportDeclaration';
+    });
+
+    body.forEach(item => {
+      // 从包中导出 SettingDrawer
+      if (item.type === 'ImportDeclaration') {
+        if (item.source.value === '@ant-design/pro-layout') {
+          item.specifiers.push(parseCode(`SettingDrawer`).expression);
+        }
+      }
+      if (item.type === 'VariableDeclaration') {
+        const {
+          id,
+          init: { body },
+        } = item.declarations[0];
+        // 给 BasicLayout 中插入 button 和 设置抽屉
+        if (id.name === `BasicLayout`) {
+          body.body.forEach(node => {
+            if (node.type === 'ReturnStatement') {
+              const JSXFragment = parseCode(`<></>`).expression;
+              JSXFragment.children.push({ ...node.argument });
+              JSXFragment.children.push(parseCode(SettingCodeString).expression);
+              node.argument = JSXFragment;
+            }
+          });
+        }
+      }
+    });
+  });
+};
+
+const insertBlankLayout = configPath => {
+  return mapAst(configPath, body => {
+    const index = body.findIndex(item => {
+      return item.type !== 'ImportDeclaration';
+    });
+    // 从组件中导入 CopyBlock
+    body.splice(
+      index,
+      0,
+      parseCode(`import CopyBlock from '@/components/CopyBlock';
+    `),
+    );
+    body.forEach(item => {
+      if (item.type === 'VariableDeclaration') {
+        const { id, init } = item.declarations[0];
+        // 给 BasicLayout 中插入 button 和 设置抽屉
+        if (id.name === `Layout`) {
+          const JSXFragment = parseCode(`<></>`).expression;
+          JSXFragment.children.push({ ...init.body });
+          JSXFragment.children.push(parseCode(` <CopyBlock id={Date.now()}/>`).expression);
+          init.body = JSXFragment;
+        }
+      }
+    });
+  });
+};
+
+const insertRightContent = configPath => {
+  return mapAst(configPath, body => {
+    const index = body.findIndex(item => {
+      return item.type !== 'ImportDeclaration';
+    });
+    // 从组件中导入 CopyBlock
+    body.splice(index, 0, parseCode(`import NoticeIconView from './NoticeIconView';`));
+
+    body.forEach(item => {
+      if (item.type === 'ClassDeclaration') {
+        const classBody = item.body.body[0].body;
+        classBody.body.forEach(node => {
+          if (node.type === 'ReturnStatement') {
+            const index = node.argument.children.findIndex(item => {
+              if (item.type === 'JSXElement') {
+                if (item.openingElement.name.name === 'Avatar') {
+                  return true;
+                }
+              }
+            });
+            node.argument.children.splice(index, 1, parseCode(`<Avatar menu />`).expression);
+            node.argument.children.splice(index, 0, parseCode(`<NoticeIconView />`).expression);
+          }
+        });
+      }
+    });
+  });
+};
+
+module.exports = () => {
+  const basicLayoutPath = path.join(__dirname, '../src/layouts/BasicLayout.tsx');
+  fs.writeFileSync(basicLayoutPath, insertBasicLayout(basicLayoutPath));
+  console.log(`insert ${chalk.hex('#1890ff')('BasicLayout')} success`);
+
+  const rightContentPath = path.join(__dirname, '../src/components/GlobalHeader/RightContent.tsx');
+  fs.writeFileSync(rightContentPath, insertRightContent(rightContentPath));
+  console.log(`insert ${chalk.hex('#1890ff')('RightContent')} success`);
+
+  const blankLayoutPath = path.join(__dirname, '../src/layouts/BlankLayout.tsx');
+  fs.writeFileSync(blankLayoutPath, insertBlankLayout(blankLayoutPath));
+  console.log(`insert ${chalk.hex('#1890ff')('blankLayoutPath')} success`);
+};

+ 77 - 0
scripts/repalceRouter.js

@@ -0,0 +1,77 @@
+const parser = require('@babel/parser');
+const traverse = require('@babel/traverse');
+const generate = require('@babel/generator');
+const t = require('@babel/types');
+const fs = require('fs');
+const prettier = require('prettier');
+
+const getNewRouteCode = (configPath, newRoute) => {
+  const ast = parser.parse(fs.readFileSync(configPath, 'utf-8'), {
+    sourceType: 'module',
+    plugins: ['typescript'],
+  });
+  let routesNode = null;
+  const importModules = [];
+  // 查询当前配置文件是否导出 routes 属性
+  traverse.default(ast, {
+    Program({ node }) {
+      // find import
+      const { body } = node;
+      body.forEach(item => {
+        if (t.isImportDeclaration(item)) {
+          const { specifiers } = item;
+          const defaultEpecifier = specifiers.find(s => {
+            return t.isImportDefaultSpecifier(s) && t.isIdentifier(s.local);
+          });
+          if (defaultEpecifier && t.isStringLiteral(item.source)) {
+            importModules.push({
+              identifierName: defaultEpecifier.local.name,
+              modulePath: item.source.value,
+            });
+          }
+        }
+      });
+    },
+    ObjectExpression({ node, parent }) {
+      // find routes on object, like { routes: [] }
+      if (t.isArrayExpression(parent)) {
+        // children routes
+        return;
+      }
+      const { properties } = node;
+      properties.forEach(p => {
+        const { key, value } = p;
+        if (t.isObjectProperty(p) && t.isIdentifier(key) && key.name === 'routes') {
+          if (value) {
+            // find json file program expression
+            (p.value = parser.parse(JSON.stringify(newRoute)).program.body[0].expression),
+              (routesNode = value);
+          }
+        }
+      });
+    },
+  });
+  if (routesNode) {
+    const code = generateCode(ast);
+    return { code, routesPath: configPath };
+  } else {
+    throw new Error('route array config not found.');
+  }
+};
+
+/**
+ * 生成代码
+ * @param {*} ast
+ */
+function generateCode(ast) {
+  const newCode = generate.default(ast, {}).code;
+  return prettier.format(newCode, {
+    // format same as ant-design-pro
+    singleQuote: true,
+    trailingComma: 'es5',
+    printWidth: 100,
+    parser: 'typescript',
+  });
+}
+
+module.exports = getNewRouteCode;

+ 236 - 0
scripts/router.config.js

@@ -0,0 +1,236 @@
+module.exports = [
+  {
+    path: '/',
+    component: '../layouts/BlankLayout',
+    routes: [
+      // user
+      {
+        path: '/user',
+        component: '../layouts/UserLayout',
+        routes: [
+          { path: '/user/login', name: 'login', component: './User/Login' },
+          { path: '/user/register', name: 'register', component: './User/Register' },
+          {
+            path: '/user/register-result',
+            name: 'register.result',
+            component: './User/RegisterResult',
+          },
+          { path: '/user', redirect: '/user/login' },
+          {
+            component: '404',
+          },
+        ],
+      },
+      // app
+      {
+        path: '/',
+        component: '../layouts/BasicLayout',
+        Routes: ['src/pages/Authorized'],
+        authority: ['admin', 'user'],
+        routes: [
+          // dashboard
+          {
+            path: '/dashboard',
+            name: 'dashboard',
+            icon: 'dashboard',
+            routes: [
+              {
+                path: '/dashboard/analysis',
+                name: 'analysis',
+                component: './Dashboard/Analysis',
+              },
+              {
+                path: '/dashboard/monitor',
+                name: 'monitor',
+                component: './Dashboard/Monitor',
+              },
+              {
+                path: '/dashboard/workplace',
+                name: 'workplace',
+                component: './Dashboard/Workplace',
+              },
+            ],
+          },
+          // forms
+          {
+            path: '/form',
+            icon: 'form',
+            name: 'form',
+            routes: [
+              {
+                path: '/form/basic-form',
+                name: 'basicform',
+                component: './Form/BasicForm',
+              },
+              {
+                path: '/form/step-form',
+                name: 'stepform',
+                component: './Form/StepForm',
+              },
+              {
+                path: '/form/advanced-form',
+                name: 'advancedform',
+                authority: ['admin'],
+                component: './Form/AdvancedForm',
+              },
+            ],
+          },
+          // list
+          {
+            path: '/list',
+            icon: 'table',
+            name: 'list',
+            routes: [
+              {
+                path: '/list/table-list',
+                name: 'searchtable',
+                component: './list/Tablelist',
+              },
+              {
+                path: '/list/basic-list',
+                name: 'basiclist',
+                component: './list/Basiclist',
+              },
+              {
+                path: '/list/card-list',
+                name: 'cardlist',
+                component: './list/Cardlist',
+              },
+              {
+                path: '/list/search',
+                name: 'search-list',
+                component: './list/search',
+                routes: [
+                  {
+                    path: '/list/search/articles',
+                    name: 'articles',
+                    component: './list/Articles',
+                  },
+                  {
+                    path: '/list/search/projects',
+                    name: 'projects',
+                    component: './list/Projects',
+                  },
+                  {
+                    path: '/list/search/applications',
+                    name: 'applications',
+                    component: './list/Applications',
+                  },
+                  {
+                    path: '/list/search',
+                    redirect: '/list/search/articles',
+                  },
+                ],
+              },
+            ],
+          },
+          {
+            path: '/profile',
+            name: 'profile',
+            icon: 'profile',
+            routes: [
+              // profile
+              {
+                path: '/profile/basic',
+                name: 'basic',
+                component: './Profile/BasicProfile',
+              },
+              {
+                path: '/profile/basic/:id',
+                hideInMenu: true,
+                component: './Profile/BasicProfile',
+              },
+              {
+                path: '/profile/advanced',
+                name: 'advanced',
+                authority: ['admin'],
+                component: './Profile/AdvancedProfile',
+              },
+            ],
+          },
+          {
+            name: 'result',
+            icon: 'check-circle-o',
+            path: '/result',
+            routes: [
+              // result
+              {
+                path: '/result/success',
+                name: 'success',
+                component: './Result/Success',
+              },
+              { path: '/result/fail', name: 'fail', component: './Result/Error' },
+            ],
+          },
+          {
+            name: 'exception',
+            icon: 'warning',
+            path: '/exception',
+            routes: [
+              // exception
+              {
+                path: '/exception/403',
+                name: 'not-permission',
+                component: './Exception/403',
+              },
+              {
+                path: '/exception/404',
+                name: 'not-find',
+                component: './Exception/404',
+              },
+              {
+                path: '/exception/500',
+                name: 'server-error',
+                component: './Exception/500',
+              },
+            ],
+          },
+          {
+            name: 'account',
+            icon: 'user',
+            path: '/account',
+            routes: [
+              {
+                path: '/account/center',
+                name: 'center',
+                component: './Account/Center/Center',
+              },
+              {
+                path: '/account/settings',
+                name: 'settings',
+                component: './Account/Settings/Info',
+              },
+            ],
+          },
+          //  editor
+          {
+            name: 'editor',
+            icon: 'highlight',
+            path: '/editor',
+            routes: [
+              {
+                path: '/editor/flow',
+                name: 'flow',
+                component: './Editor/GGEditor/Flow',
+              },
+              {
+                path: '/editor/mind',
+                name: 'mind',
+                component: './Editor/GGEditor/Mind',
+              },
+              {
+                path: '/editor/koni',
+                name: 'koni',
+                component: './Editor/GGEditor/Koni',
+              },
+            ],
+          },
+          { path: '/', redirect: '/dashboard/analysis', authority: ['admin', 'user'] },
+          {
+            component: '404',
+          },
+        ],
+      },
+    ],
+  },
+];

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

+ 47 - 0
src/components/CopyBlock/index.tsx

@@ -0,0 +1,47 @@
+import React from 'react';
+import { Icon, Typography, Popover } from 'antd';
+import styles from './index.less';
+import { connect } from 'dva';
+import * as H from 'history';
+import { FormattedMessage } from 'umi-plugin-react/locale';
+
+const firstUpperCase = (pathString: string) => {
+  return pathString
+    .replace('.', '')
+    .split(/\/|\-/)
+    .map(s => s.toLowerCase().replace(/( |^)[a-z]/g, L => L.toUpperCase()))
+    .filter(s => s)
+    .join('');
+};
+const BlockCodeView: React.SFC<{
+  url: string;
+}> = ({ url }) => {
+  const blockUrl = `npx umi block add ant-design-pro/${firstUpperCase(url)}  --path=${url}`;
+  return (
+    <div className={styles['copy-block-view']}>
+      <Typography.Paragraph copyable>
+        <code className={styles['copy-block-code']}>{blockUrl}</code>
+      </Typography.Paragraph>
+    </div>
+  );
+};
+
+type RoutingType = { location: H.Location };
+
+export default connect(({ routing }: { routing: RoutingType }) => ({
+  location: routing.location,
+}))(({ location }: RoutingType) => {
+  const url = location.pathname;
+  return (
+    <Popover
+      title={<FormattedMessage id="app.preview.down.block" defaultMessage="下载此页面到本地项目" />}
+      placement="topLeft"
+      content={<BlockCodeView url={url} />}
+      trigger="click"
+    >
+      <div className={styles['copy-block']}>
+        <Icon type="download" />
+      </div>
+    </Popover>
+  );
+});

+ 72 - 0
src/components/GlobalHeader/AvatarDropdown.tsx

@@ -0,0 +1,72 @@
+import React from 'react';
+import { Avatar, Menu, Spin, Icon } from 'antd';
+import { FormattedMessage } from 'umi-plugin-react/locale';
+import { ClickParam } from 'antd/lib/menu';
+import { ConnectProps, ConnectState } from '@/models/connect';
+import { CurrentUser } from '@/models/user';
+import { connect } from 'dva';
+import router from 'umi/router';
+import HeaderDropdown from '../HeaderDropdown';
+import styles from './index.less';
+
+export interface GlobalHeaderRightProps extends ConnectProps {
+  currentUser?: CurrentUser;
+  menu?: boolean;
+}
+
+class AvatarDropdown extends React.Component<GlobalHeaderRightProps> {
+  onMenuClick = (event: ClickParam) => {
+    const { key } = event;
+
+    if (key === 'logout') {
+      const { dispatch } = this.props;
+      dispatch!({
+        type: 'login/logout',
+      });
+      return;
+    }
+    router.push(`/account/${key}`);
+  };
+  render() {
+    const { currentUser = {}, menu } = this.props;
+    if (!menu) {
+      return (
+        <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>
+      );
+    }
+    const menuHeaderDropdown = (
+      <Menu className={styles.menu} selectedKeys={[]} onClick={this.onMenuClick}>
+        <Menu.Item key="center">
+          <Icon type="user" />
+          <FormattedMessage id="menu.account.center" defaultMessage="account center" />
+        </Menu.Item>
+        <Menu.Item key="settings">
+          <Icon type="setting" />
+          <FormattedMessage id="menu.account.settings" defaultMessage="account settings" />
+        </Menu.Item>
+        <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 }: ConnectState) => ({
+  currentUser: user.currentUser,
+}))(AvatarDropdown);

+ 145 - 0
src/components/GlobalHeader/NoticeIconView.tsx

@@ -0,0 +1,145 @@
+import { ConnectProps, ConnectState } from '@/models/connect';
+import { NoticeItem } from '@/models/global';
+import { CurrentUser } from '@/models/user';
+import React, { Component } from 'react';
+import { Tag, message } from 'antd';
+import { formatMessage } from 'umi-plugin-react/locale';
+import moment from 'moment';
+import groupBy from 'lodash/groupBy';
+import NoticeIcon from '../NoticeIcon';
+import styles from './index.less';
+import { connect } from 'dva';
+
+export interface GlobalHeaderRightProps extends ConnectProps {
+  notices?: NoticeItem[];
+  currentUser?: CurrentUser;
+  fetchingNotices?: boolean;
+  onNoticeVisibleChange?: (visible: boolean) => void;
+  onNoticeClear?: (tabName?: string) => void;
+}
+
+class GlobalHeaderRight extends Component<GlobalHeaderRightProps> {
+  getNoticeData = (): { [key: string]: NoticeItem[] } => {
+    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 as string).fromNow();
+      }
+      if (newNotice.id) {
+        newNotice.key = newNotice.id;
+      }
+      if (newNotice.extra && newNotice.status) {
+        const color = {
+          todo: '',
+          processing: 'blue',
+          urgent: 'red',
+          doing: 'gold',
+        }[newNotice.status];
+        newNotice.extra = (
+          <Tag color={color} style={{ marginRight: 0 }}>
+            {newNotice.extra}
+          </Tag>
+        );
+      }
+      return newNotice;
+    });
+    return groupBy(newNotices, 'type');
+  };
+
+  getUnreadData = (noticeData: { [key: string]: NoticeItem[] }) => {
+    const unreadMsg: { [key: string]: number } = {};
+    Object.entries(noticeData).forEach(([key, value]) => {
+      if (!unreadMsg[key]) {
+        unreadMsg[key] = 0;
+      }
+      if (Array.isArray(value)) {
+        unreadMsg[key] = value.filter(item => !item.read).length;
+      }
+    });
+    return unreadMsg;
+  };
+
+  changeReadState = (clickedItem: NoticeItem) => {
+    const { id } = clickedItem;
+    const { dispatch } = this.props;
+    dispatch!({
+      type: 'global/changeNoticeReadState',
+      payload: id,
+    });
+  };
+  componentDidMount() {
+    const { dispatch } = this.props;
+    dispatch!({
+      type: 'global/fetchNotices',
+    });
+  }
+  handleNoticeClear = (title: string, key: string) => {
+    const { dispatch } = this.props;
+    message.success(`${formatMessage({ id: 'component.noticeIcon.cleared' })} ${title}`);
+    if (dispatch) {
+      dispatch({
+        type: 'global/clearNotices',
+        payload: key,
+      });
+    }
+  };
+  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 as NoticeItem);
+        }}
+        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 }: ConnectState) => ({
+  currentUser: user.currentUser,
+  collapsed: global.collapsed,
+  fetchingMoreNotices: loading.effects['global/fetchMoreNotices'],
+  fetchingNotices: loading.effects['global/fetchNotices'],
+  notices: global.notices,
+}))(GlobalHeaderRight);

+ 29 - 176
src/components/GlobalHeader/RightContent.tsx

@@ -1,138 +1,44 @@
 import { ConnectProps, ConnectState } from '@/models/connect';
-import { NoticeItem } from '@/models/global';
-import { CurrentUser } from '@/models/user';
 import React, { Component } from 'react';
-import { Spin, Tag, Menu, Icon, Avatar, Tooltip, message } from 'antd';
-import { ClickParam } from 'antd/es/menu';
-import { FormattedMessage, formatMessage } from 'umi-plugin-react/locale';
-import moment from 'moment';
-import groupBy from 'lodash/groupBy';
-import NoticeIcon from '../NoticeIcon';
+import { Icon, Tooltip } from 'antd';
+import { formatMessage } from 'umi-plugin-react/locale';
 import HeaderSearch from '../HeaderSearch';
-import HeaderDropdown from '../HeaderDropdown';
 import SelectLang from '../SelectLang';
 import styles from './index.less';
+import Avatar from './AvatarDropdown';
 import { connect } from 'dva';
-
 export type SiderTheme = 'light' | 'dark';
-
 export interface GlobalHeaderRightProps extends ConnectProps {
-  notices?: NoticeItem[];
-  currentUser?: CurrentUser;
-  fetchingNotices?: boolean;
-  onNoticeVisibleChange?: (visible: boolean) => void;
-  onMenuClick?: (param: ClickParam) => void;
-  onNoticeClear?: (tabName?: string) => void;
   theme?: SiderTheme;
+  layout: 'sidemenu' | 'topmenu';
 }
 
 class GlobalHeaderRight extends Component<GlobalHeaderRightProps> {
-  getNoticeData = (): { [key: string]: NoticeItem[] } => {
-    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 as string).fromNow();
-      }
-      if (newNotice.id) {
-        newNotice.key = newNotice.id;
-      }
-      if (newNotice.extra && newNotice.status) {
-        const color = {
-          todo: '',
-          processing: 'blue',
-          urgent: 'red',
-          doing: 'gold',
-        }[newNotice.status];
-        newNotice.extra = (
-          <Tag color={color} style={{ marginRight: 0 }}>
-            {newNotice.extra}
-          </Tag>
-        );
-      }
-      return newNotice;
-    });
-    return groupBy(newNotices, 'type');
-  };
-
-  getUnreadData = (noticeData: { [key: string]: NoticeItem[] }) => {
-    const unreadMsg: { [key: string]: number } = {};
-    Object.entries(noticeData).forEach(([key, value]) => {
-      if (!unreadMsg[key]) {
-        unreadMsg[key] = 0;
-      }
-      if (Array.isArray(value)) {
-        unreadMsg[key] = value.filter(item => !item.read).length;
-      }
-    });
-    return unreadMsg;
-  };
-
-  changeReadState = (clickedItem: NoticeItem) => {
-    const { id } = clickedItem;
-    const { dispatch } = this.props;
-    dispatch!({
-      type: 'global/changeNoticeReadState',
-      payload: id,
-    });
-  };
-  componentDidMount() {
-    const { dispatch } = this.props;
-    dispatch!({
-      type: 'global/fetchNotices',
-    });
-  }
-  handleNoticeClear = (title: string, key: string) => {
-    const { dispatch } = this.props;
-    message.success(`${formatMessage({ id: 'component.noticeIcon.cleared' })} ${title}`);
-    if (dispatch) {
-      dispatch({
-        type: 'global/clearNotices',
-        payload: key,
-      });
-    }
-  };
   render() {
-    const { currentUser, fetchingNotices, onNoticeVisibleChange, onMenuClick, theme } = this.props;
-    const menu = (
-      <Menu className={styles.menu} selectedKeys={[]} onClick={onMenuClick}>
-        <Menu.Item key="userCenter">
-          <Icon type="user" />
-          <FormattedMessage id="menu.account.center" defaultMessage="account center" />
-        </Menu.Item>
-        <Menu.Item key="userinfo">
-          <Icon type="setting" />
-          <FormattedMessage id="menu.account.settings" defaultMessage="account settings" />
-        </Menu.Item>
-        <Menu.Item key="triggerError">
-          <Icon type="close-circle" />
-          <FormattedMessage id="menu.account.trigger" defaultMessage="Trigger Error" />
-        </Menu.Item>
-        <Menu.Divider />
-        <Menu.Item key="logout">
-          <Icon type="logout" />
-          <FormattedMessage id="menu.account.logout" defaultMessage="logout" />
-        </Menu.Item>
-      </Menu>
-    );
-    const noticeData = this.getNoticeData();
-    const unreadMsg = this.getUnreadData(noticeData);
+    const { theme, layout } = this.props;
     let className = styles.right;
-    if (theme === 'dark') {
+
+    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' })}
+          placeholder={formatMessage({
+            id: 'component.globalHeader.search',
+          })}
           dataSource={[
-            formatMessage({ id: 'component.globalHeader.search.example1' }),
-            formatMessage({ id: 'component.globalHeader.search.example2' }),
-            formatMessage({ id: 'component.globalHeader.search.example3' }),
+            formatMessage({
+              id: 'component.globalHeader.search.example1',
+            }),
+            formatMessage({
+              id: 'component.globalHeader.search.example2',
+            }),
+            formatMessage({
+              id: 'component.globalHeader.search.example3',
+            }),
           ]}
           onSearch={value => {
             console.log('input', value); // tslint:disable-line no-console
@@ -141,7 +47,11 @@ class GlobalHeaderRight extends Component<GlobalHeaderRightProps> {
             console.log('enter', value); // tslint:disable-line no-console
           }}
         />
-        <Tooltip title={formatMessage({ id: 'component.globalHeader.help' })}>
+        <Tooltip
+          title={formatMessage({
+            id: 'component.globalHeader.help',
+          })}
+        >
           <a
             target="_blank"
             href="https://pro.ant.design/docs/getting-started"
@@ -151,71 +61,14 @@ class GlobalHeaderRight extends Component<GlobalHeaderRightProps> {
             <Icon type="question-circle-o" />
           </a>
         </Tooltip>
-
-        <NoticeIcon
-          className={styles.action}
-          count={currentUser && currentUser.unreadCount}
-          onItemClick={item => {
-            this.changeReadState(item as NoticeItem);
-          }}
-          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>
-        {currentUser && currentUser.name ? (
-          <HeaderDropdown overlay={menu}>
-            <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 }} />
-        )}
+        <Avatar />
         <SelectLang className={styles.action} />
       </div>
     );
   }
 }
 
-export default connect(({ user, global, loading }: ConnectState) => ({
-  currentUser: user.currentUser,
-  collapsed: global.collapsed,
-  fetchingMoreNotices: loading.effects['global/fetchMoreNotices'],
-  fetchingNotices: loading.effects['global/fetchNotices'],
-  notices: global.notices,
+export default connect(({ settings }: ConnectState) => ({
+  theme: settings.navTheme,
+  layout: settings.layout,
 }))(GlobalHeaderRight);

+ 1 - 1
src/e2e/baseLayout.e2e.js

@@ -33,7 +33,7 @@ describe('Homepage', () => {
     jest.setTimeout(1000000);
     await page.setCacheEnabled(false);
   });
-  const routers = formatter(RouterConfig[1].routes);
+  const routers = formatter(RouterConfig);
   routers.forEach(route => {
     it(`test pages ${route}`, testPage(route));
   });

+ 50 - 43
src/layouts/BasicLayout.tsx

@@ -16,74 +16,81 @@ import {
   BasicLayoutProps as BasicLayoutComponentsProps,
   MenuDataItem,
   Settings,
-  SettingDrawer,
 } from '@ant-design/pro-layout';
 import Link from 'umi/link';
-
 export interface BasicLayoutProps extends BasicLayoutComponentsProps, ConnectProps {
-  breadcrumbNameMap: { [path: string]: MenuDataItem };
+  breadcrumbNameMap: {
+    [path: string]: MenuDataItem;
+  };
   settings: Settings;
 }
-
 export type BasicLayoutContext = { [K in 'location']: BasicLayoutProps[K] } & {
-  breadcrumbNameMap: { [path: string]: MenuDataItem };
+  breadcrumbNameMap: {
+    [path: string]: MenuDataItem;
+  };
 };
-
 /**
- * default menuLocal
+ * use Authorized check all menu item
  */
-const filterMenuData = (menuList: MenuDataItem[], locale: boolean) => {
+
+const menuDataRender = (menuList: MenuDataItem[]): MenuDataItem[] => {
   return menuList.map(item => {
-    const localItem = {
-      ...item,
-      name: item.locale && locale ? formatMessage({ id: item.locale }) : item.name,
-    };
+    const localItem = { ...item, children: item.children ? menuDataRender(item.children) : [] };
     return Authorized.check(item.authority, localItem, null) as MenuDataItem;
   });
 };
 
 const BasicLayout: React.FC<BasicLayoutProps> = props => {
-  const { dispatch, children, settings } = props;
+  const { dispatch, children, settings, location } = props;
   /**
    * constructor
    */
+
   useState(() => {
-    dispatch!({ type: 'user/fetchCurrent' });
-    dispatch!({ type: 'settings/getSetting' });
+    dispatch!({
+      type: 'user/fetchCurrent',
+    });
+    dispatch!({
+      type: 'settings/getSetting',
+    });
   });
   /**
    * init variables
    */
+
   const handleMenuCollapse = (payload: boolean) =>
-    dispatch!({ type: 'global/changeLayoutCollapsed', payload });
-  const {
-    menu: { locale },
-  } = settings;
+    dispatch!({
+      type: 'global/changeLayoutCollapsed',
+      payload,
+    });
+
   return (
-    <>
-      <BasicLayoutComponents
-        logo={logo}
-        onCollapse={handleMenuCollapse}
-        menuItemRender={(menuItemProps, defaultDom) => {
-          return <Link to={menuItemProps.path}>{defaultDom}</Link>;
-        }}
-        filterMenuData={menuList => filterMenuData(menuList, locale)}
-        rightContentRender={rightProps => <RightContent {...rightProps} />}
-        {...props}
-        {...settings}
-      >
-        {children}
-      </BasicLayoutComponents>
-      <SettingDrawer
-        settings={settings}
-        onSettingChange={config =>
-          dispatch!({
-            type: 'settings/changeSetting',
-            payload: config,
-          })
-        }
-      />
-    </>
+    <BasicLayoutComponents
+      logo={logo}
+      onCollapse={handleMenuCollapse}
+      menuItemRender={(menuItemProps, defaultDom) => {
+        return <Link to={menuItemProps.path}>{defaultDom}</Link>;
+      }}
+      breadcrumbRender={(routers = []) => {
+        return [
+          {
+            path: '/',
+            breadcrumbName: formatMessage({
+              id: 'menu.home',
+              defaultMessage: 'Home',
+            }),
+          },
+          ...routers,
+        ];
+      }}
+      menuDataRender={menuDataRender}
+      formatMessage={formatMessage}
+      rightContentRender={rightProps => <RightContent {...rightProps} />}
+      {...props}
+      {...settings}
+    >
+      {children}
+    </BasicLayoutComponents>
   );
 };
 

+ 1 - 1
src/locales/en-US.ts

@@ -10,7 +10,7 @@ export default {
   'layout.user.link.help': 'Help',
   'layout.user.link.privacy': 'Privacy',
   'layout.user.link.terms': 'Terms',
-  'app.home.introduce': 'introduce',
+  'app.preview.down.block': 'Download this page to your local project',
   ...globalHeader,
   ...menu,
   ...settingDrawer,

+ 42 - 1
src/locales/en-US/menu.ts

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

+ 1 - 1
src/locales/pt-BR.ts

@@ -10,7 +10,7 @@ export default {
   'layout.user.link.help': 'ajuda',
   'layout.user.link.privacy': 'política de privacidade',
   'layout.user.link.terms': 'termos de serviços',
-  'app.home.introduce': 'introduzir',
+  'app.preview.down.block': 'Download this page to your local project',
   ...globalHeader,
   ...menu,
   ...settingDrawer,

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

@@ -2,8 +2,50 @@ export default {
   'menu.welcome': 'Welcome',
   'menu.more-blocks': 'More Blocks',
 
+  'menu.home': 'Início',
+  'menu.login': 'Login',
+  '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',
 };

+ 1 - 1
src/locales/zh-CN.ts

@@ -10,7 +10,7 @@ export default {
   'layout.user.link.help': '帮助',
   'layout.user.link.privacy': '隐私',
   'layout.user.link.terms': '条款',
-  'app.home.introduce': '介绍',
+  'app.preview.down.block': '下载此页面到本地项目',
   ...globalHeader,
   ...menu,
   ...settingDrawer,

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

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

+ 1 - 4
src/locales/zh-TW.ts

@@ -10,10 +10,7 @@ export default {
   'layout.user.link.help': '幫助',
   'layout.user.link.privacy': '隱私',
   'layout.user.link.terms': '條款',
-  'app.home.introduce': '介紹',
-  'app.forms.basic.title': '基礎表單',
-  'app.forms.basic.description':
-    '表單頁用於向用戶收集或驗證信息,基礎表單常見於數據項較少的表單場景。',
+  'app.preview.down.block': '下載此頁面到本地項目',
   ...globalHeader,
   ...menu,
   ...settingDrawer,

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

@@ -2,8 +2,50 @@ export default {
   'menu.welcome': '歡迎',
   'menu.more-blocks': '更多區塊',
 
+  'menu.home': '首頁',
+  'menu.login': '登錄',
+  'menu.exception.403': '403',
+  'menu.exception.404': '404',
+  'menu.exception.500': '500',
+  'menu.register': '註冊',
+  'menu.register.resultt': '註冊結果',
+  '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': '拓撲編輯器',
 };

+ 64 - 0
src/models/login.ts

@@ -0,0 +1,64 @@
+import { routerRedux } from 'dva/router';
+import { Reducer } from 'redux';
+import { EffectsCommandMap } from 'dva';
+import { AnyAction } from 'redux';
+import { stringify, parse } from 'qs';
+
+export function getPageQuery() {
+  return parse(window.location.href.split('?')[1]);
+}
+
+export interface IStateType {}
+
+export type Effect = (
+  action: AnyAction,
+  effects: EffectsCommandMap & { select: <T>(func: (state: IStateType) => T) => T },
+) => void;
+
+export interface ModelType {
+  namespace: string;
+  state: IStateType;
+  effects: {
+    logout: Effect;
+  };
+  reducers: {
+    changeLoginStatus: Reducer<IStateType>;
+  };
+}
+
+const Model: ModelType = {
+  namespace: 'login',
+
+  state: {
+    status: undefined,
+  },
+
+  effects: {
+    *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 }) {
+      return {
+        ...state,
+        status: payload.status,
+        type: payload.type,
+      };
+    },
+  },
+};
+
+export default Model;

+ 12 - 0
src/utils/utils.ts

@@ -4,3 +4,15 @@ const reg = /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(
 export function isUrl(path: string) {
   return reg.test(path);
 }
+
+// 给官方演示站点用,用于关闭真实开发环境不需要使用的特性
+export function isAntDesignProOrDev() {
+  const { NODE_ENV } = process.env;
+  if (ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site') {
+    return true;
+  }
+  if (NODE_ENV === 'development') {
+    return true;
+  }
+  return window.location.hostname === 'preview.pro.ant.design';
+}