Browse Source

渠道方管理平台

zhanghe 6 years ago
commit
231a5f4c16
100 changed files with 4695 additions and 0 deletions
  1. 16 0
      .editorconfig
  2. 67 0
      .eslintrc
  3. 3 0
      .ga
  4. 16 0
      .gitignore
  5. 24 0
      .roadhogrc
  6. 12 0
      .roadhogrc.mock.js
  7. 25 0
      .stylelintrc
  8. 127 0
      package.json
  9. BIN
      public/favicon.png
  10. 43 0
      src/assets/logo.svg
  11. 60 0
      src/common/menu.js
  12. 105 0
      src/common/router.js
  13. 58 0
      src/components/Animation/AnimTableBody.js
  14. 14 0
      src/components/Charts/Bar/index.d.ts
  15. 151 0
      src/components/Charts/Bar/index.js
  16. 11 0
      src/components/Charts/ChartCard/index.d.ts
  17. 60 0
      src/components/Charts/ChartCard/index.js
  18. 74 0
      src/components/Charts/ChartCard/index.less
  19. 7 0
      src/components/Charts/Field/index.d.ts
  20. 12 0
      src/components/Charts/Field/index.js
  21. 16 0
      src/components/Charts/Field/index.less
  22. 10 0
      src/components/Charts/Gauge/index.d.ts
  23. 202 0
      src/components/Charts/Gauge/index.js
  24. 29 0
      src/components/Charts/MiniArea/index.d.ts
  25. 125 0
      src/components/Charts/MiniArea/index.js
  26. 11 0
      src/components/Charts/MiniBar/index.d.ts
  27. 87 0
      src/components/Charts/MiniBar/index.js
  28. 12 0
      src/components/Charts/MiniProgress/index.d.ts
  29. 30 0
      src/components/Charts/MiniProgress/index.js
  30. 35 0
      src/components/Charts/MiniProgress/index.less
  31. 20 0
      src/components/Charts/Pie/index.d.ts
  32. 260 0
      src/components/Charts/Pie/index.js
  33. 94 0
      src/components/Charts/Pie/index.less
  34. 14 0
      src/components/Charts/Radar/index.d.ts
  35. 189 0
      src/components/Charts/Radar/index.js
  36. 46 0
      src/components/Charts/Radar/index.less
  37. 10 0
      src/components/Charts/TagCloud/index.d.ts
  38. 170 0
      src/components/Charts/TagCloud/index.js
  39. 6 0
      src/components/Charts/TagCloud/index.less
  40. 15 0
      src/components/Charts/TimelineChart/index.d.ts
  41. 125 0
      src/components/Charts/TimelineChart/index.js
  42. 3 0
      src/components/Charts/TimelineChart/index.less
  43. 9 0
      src/components/Charts/WaterWave/index.d.ts
  44. 200 0
      src/components/Charts/WaterWave/index.js
  45. 28 0
      src/components/Charts/WaterWave/index.less
  46. 26 0
      src/components/Charts/demo/bar.md
  47. 65 0
      src/components/Charts/demo/chart-card.md
  48. 18 0
      src/components/Charts/demo/gauge.md
  49. 28 0
      src/components/Charts/demo/mini-area.md
  50. 28 0
      src/components/Charts/demo/mini-bar.md
  51. 16 0
      src/components/Charts/demo/mini-pie.md
  52. 12 0
      src/components/Charts/demo/mini-progress.md
  53. 83 0
      src/components/Charts/demo/mix.md
  54. 47 0
      src/components/Charts/demo/pie.md
  55. 64 0
      src/components/Charts/demo/radar.md
  56. 25 0
      src/components/Charts/demo/tag-cloud.md
  57. 27 0
      src/components/Charts/demo/timeline-chart.md
  58. 20 0
      src/components/Charts/demo/waterwave.md
  59. 17 0
      src/components/Charts/equal.js
  60. 17 0
      src/components/Charts/index.d.ts
  61. 31 0
      src/components/Charts/index.js
  62. 19 0
      src/components/Charts/index.less
  63. 132 0
      src/components/Charts/index.md
  64. 127 0
      src/components/DataSearch/index.js
  65. 55 0
      src/components/DataSearch/index.less
  66. 17 0
      src/components/DescriptionList/Description.js
  67. 21 0
      src/components/DescriptionList/DescriptionList.js
  68. 35 0
      src/components/DescriptionList/demo/basic.md
  69. 35 0
      src/components/DescriptionList/demo/vertical.md
  70. 22 0
      src/components/DescriptionList/index.d.ts
  71. 5 0
      src/components/DescriptionList/index.js
  72. 75 0
      src/components/DescriptionList/index.less
  73. 39 0
      src/components/DescriptionList/index.md
  74. 6 0
      src/components/DescriptionList/responsive.js
  75. 25 0
      src/components/DropOption/index.js
  76. 21 0
      src/components/Exception/demo/403.md
  77. 14 0
      src/components/Exception/demo/404.md
  78. 14 0
      src/components/Exception/demo/500.md
  79. 33 0
      src/components/Exception/index.js
  80. 78 0
      src/components/Exception/index.less
  81. 19 0
      src/components/Exception/index.md
  82. 19 0
      src/components/Exception/typeConfig.js
  83. 36 0
      src/components/FooterToolbar/demo/basic.md
  84. 10 0
      src/components/FooterToolbar/index.d.ts
  85. 18 0
      src/components/FooterToolbar/index.js
  86. 33 0
      src/components/FooterToolbar/index.less
  87. 21 0
      src/components/FooterToolbar/index.md
  88. 33 0
      src/components/GlobalFooter/demo/basic.md
  89. 14 0
      src/components/GlobalFooter/index.d.ts
  90. 29 0
      src/components/GlobalFooter/index.js
  91. 51 0
      src/components/GlobalFooter/index.less
  92. 17 0
      src/components/GlobalFooter/index.md
  93. 170 0
      src/components/GlobalHeader/index.js
  94. 113 0
      src/components/GlobalHeader/index.less
  95. 34 0
      src/components/HeaderSearch/demo/basic.md
  96. 13 0
      src/components/HeaderSearch/index.d.ts
  97. 85 0
      src/components/HeaderSearch/index.js
  98. 32 0
      src/components/HeaderSearch/index.less
  99. 20 0
      src/components/HeaderSearch/index.md
  100. 0 0
      src/components/NoticeIcon/NoticeList.js

+ 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

+ 67 - 0
.eslintrc

@@ -0,0 +1,67 @@
+{
+  "parser": "babel-eslint",
+  "extends": "airbnb",
+  "plugins": ["compat"],
+  "env": {
+    "browser": true,
+    "node": true,
+    "es6": true,
+    "mocha": true,
+    "jest": true,
+    "jasmine": true
+  },
+  "rules": {
+    "generator-star-spacing": [0],
+    "consistent-return": [0],
+    "react/forbid-prop-types": [0],
+    "react/jsx-filename-extension": [1, { "extensions": [".js"] }],
+    "global-require": [1],
+    "import/prefer-default-export": [0],
+    "react/jsx-no-bind": [0],
+    "react/prop-types": [0],
+    "react/prefer-stateless-function": [0],
+    "react/jsx-wrap-multilines": ["error", {
+      "declaration": "parens-new-line",
+      "assignment": "parens-new-line",
+      "return": "parens-new-line",
+      "arrow": "parens-new-line",
+      "condition": "parens-new-line",
+      "logical": "parens-new-line",
+      "prop": "ignore"
+    }],
+    "no-else-return": [0],
+    "no-restricted-syntax": [0],
+    "import/no-extraneous-dependencies": [0],
+    "no-use-before-define": [0],
+    "jsx-a11y/no-static-element-interactions": [0],
+    "jsx-a11y/no-noninteractive-element-interactions": [0],
+    "jsx-a11y/click-events-have-key-events": [0],
+    "jsx-a11y/anchor-is-valid": [0],
+    "no-nested-ternary": [0],
+    "arrow-body-style": [0],
+    "import/extensions": [0],
+    "no-bitwise": [0],
+    "no-cond-assign": [0],
+    "import/no-unresolved": [0],
+    "comma-dangle": ["error", {
+      "arrays": "always-multiline",
+      "objects": "always-multiline",
+      "imports": "always-multiline",
+      "exports": "always-multiline",
+      "functions": "ignore"
+    }],
+    "object-curly-newline": [0],
+    "function-paren-newline": [0],
+    "no-restricted-globals": [0],
+    "require-yield": [1],
+    "compat/compat": "error"
+  },
+  "parserOptions": {
+    "ecmaFeatures": {
+      "experimentalObjectRestSpread": true
+    }
+  },
+  "settings": {
+    "polyfills": ["fetch", "promises"]
+  }
+}

+ 3 - 0
.ga

@@ -0,0 +1,3 @@
+{
+    "code":"UA-72788897-6"
+} 

+ 16 - 0
.gitignore

@@ -0,0 +1,16 @@
+# 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
+
+# misc
+.DS_Store
+npm-debug.log*
+
+/coverage

+ 24 - 0
.roadhogrc

@@ -0,0 +1,24 @@
+{
+  "entry": "src/index.js",
+  "extraBabelPlugins": [
+    "transform-runtime",
+    "transform-decorators-legacy",
+    "transform-class-properties",
+    ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": true }]
+  ],
+  "env": {
+    "development": {
+      "extraBabelPlugins": [
+        "dva-hmr"
+      ]
+    }
+  },
+  "externals": {
+    "g2": "G2",
+    "g-cloud": "Cloud",
+    "g2-plugin-slider": "G2.Plugin.slider"
+  },
+  "ignoreMomentLocale": true,
+  "theme": "./src/theme.js",
+  "hash": true
+}

+ 12 - 0
.roadhogrc.mock.js

@@ -0,0 +1,12 @@
+import mockjs from 'mockjs';
+import { format, delay } from 'roadhog-api-doc';
+
+// mock数据
+const proxy = {
+};
+
+// 是否禁用代理
+const noProxy = process.env.NO_PROXY === 'true';
+
+// 根据是否禁用代理来选择是mock数据还是真实接口
+export default noProxy ? {} : delay(proxy, 500);

+ 25 - 0
.stylelintrc

@@ -0,0 +1,25 @@
+{
+  "extends": "stylelint-config-standard",
+  "rules": {
+    "selector-pseudo-class-no-unknown": null,
+    "shorthand-property-no-redundant-values": null,
+    "at-rule-empty-line-before": null,
+    "at-rule-name-space-after": null,
+    "comment-empty-line-before": null,
+    "declaration-bang-space-before": null,
+    "declaration-empty-line-before": null,
+    "function-comma-newline-after": null,
+    "function-name-case": null,
+    "function-parentheses-newline-inside": null,
+    "function-max-empty-lines": null,
+    "function-whitespace-after": null,
+    "number-leading-zero": null,
+    "number-no-trailing-zeros": null,
+    "rule-empty-line-before": null,
+    "selector-combinator-space-after": null,
+    "selector-list-comma-newline-after": null,
+    "selector-pseudo-element-colon-notation": null,
+    "unit-no-unknown": null,
+    "value-list-max-empty-lines": null
+  }
+}

+ 127 - 0
package.json

@@ -0,0 +1,127 @@
+{
+  "name": "ant-design-pro",
+  "version": "0.3.1",
+  "description": "An out-of-box UI solution for enterprise applications",
+  "private": true,
+  "scripts": {
+    "precommit": "npm run lint-staged",
+    "start": "roadhog server",
+    "start:no-proxy": "cross-env NO_PROXY=true roadhog server",
+    "build": "roadhog build",
+    "site": "roadhog-api-doc static && gh-pages -d dist",
+    "analyze": "roadhog build --analyze",
+    "lint:style": "stylelint \"src/**/*.less\" --syntax less",
+    "lint": "eslint --ext .js src mock tests && npm run lint:style",
+    "lint:fix": "eslint --fix --ext .js src mock tests && npm run lint:style",
+    "lint-staged": "lint-staged",
+    "lint-staged:js": "eslint --ext .js",
+    "test": "jest",
+    "test:all": "node ./tests/run-tests.js"
+  },
+  "dependencies": {
+    "antd": "^3.0.0",
+    "babel-polyfill": "^6.26.0",
+    "babel-runtime": "^6.9.2",
+    "classnames": "^2.2.5",
+    "core-js": "^2.5.1",
+    "dva": "^2.1.0",
+    "enquire-js": "^0.1.1",
+    "g-cloud": "^1.0.2-beta",
+    "g2": "^2.3.13",
+    "g2-plugin-slider": "^1.2.1",
+    "hls.js": "^0.8.9",
+    "lodash": "^4.17.4",
+    "lodash-decorators": "^4.4.1",
+    "lodash.clonedeep": "^4.5.0",
+    "moment": "^2.19.1",
+    "numeral": "^2.0.6",
+    "prop-types": "^15.5.10",
+    "qs": "^6.5.0",
+    "query-string": "^5.0.1",
+    "rc-drawer-menu": "^0.5.0",
+    "rc-queue-anim": "^1.4.1",
+    "rc-tween-one": "^1.7.1",
+    "react": "^16.0.0",
+    "react-container-query": "^0.9.1",
+    "react-document-title": "^2.0.3",
+    "react-dom": "^16.0.0",
+    "react-fittext": "^1.0.0"
+  },
+  "devDependencies": {
+    "babel-eslint": "^8.0.1",
+    "babel-jest": "^21.0.0",
+    "babel-plugin-dva-hmr": "^0.3.2",
+    "babel-plugin-import": "^1.2.1",
+    "babel-plugin-transform-class-properties": "^6.24.1",
+    "babel-plugin-transform-decorators-legacy": "^1.3.4",
+    "babel-plugin-transform-runtime": "^6.9.0",
+    "babel-preset-env": "^1.6.1",
+    "babel-preset-react": "^6.24.1",
+    "cross-env": "^5.1.1",
+    "cross-port-killer": "^1.0.1",
+    "dva-model-extend": "^0.1.2",
+    "enzyme": "^3.1.0",
+    "enzyme-adapter-react-16": "^1.0.2",
+    "eslint": "^4.8.0",
+    "eslint-config-airbnb": "^16.0.0",
+    "eslint-plugin-babel": "^4.0.0",
+    "eslint-plugin-compat": "^2.1.0",
+    "eslint-plugin-import": "^2.2.0",
+    "eslint-plugin-jsx-a11y": "^6.0.0",
+    "eslint-plugin-markdown": "^1.0.0-beta.6",
+    "eslint-plugin-react": "^7.0.1",
+    "gh-pages": "^1.0.0",
+    "husky": "^0.14.3",
+    "jest": "^21.0.1",
+    "lint-staged": "^4.3.0",
+    "mockjs": "^1.0.1-beta3",
+    "prettier": "^1.9.0",
+    "pro-download": "^1.0.0",
+    "react-markdown": "^3.1.4",
+    "react-test-renderer": "^16.0.0",
+    "redbox-react": "^1.3.2",
+    "roadhog": "^1.3.1",
+    "roadhog-api-doc": "^0.3.3",
+    "rollbar": "^2.3.1",
+    "stylelint": "^8.1.0",
+    "stylelint-config-standard": "^17.0.0"
+  },
+  "optionalDependencies": {
+    "nightmare": "^2.10.0"
+  },
+  "babel": {
+    "presets": [
+      "env",
+      "react"
+    ],
+    "plugins": [
+      "transform-decorators-legacy",
+      "transform-class-properties"
+    ]
+  },
+  "jest": {
+    "setupFiles": [
+      "<rootDir>/tests/setupTests.js"
+    ],
+    "testMatch": [
+      "**/?(*.)(spec|test|e2e).js?(x)"
+    ],
+    "setupTestFrameworkScriptFile": "<rootDir>/tests/jasmine.js",
+    "moduleFileExtensions": [
+      "js",
+      "jsx"
+    ],
+    "moduleNameMapper": {
+      "\\.(css|less)$": "<rootDir>/tests/styleMock.js"
+    }
+  },
+  "lint-staged": {
+    "**/*.{js,jsx}": "lint-staged:js",
+    "**/*.less": "stylelint --syntax less"
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not ie <= 10"
+  ]
+}

BIN
public/favicon.png


File diff suppressed because it is too large
+ 43 - 0
src/assets/logo.svg


+ 60 - 0
src/common/menu.js

@@ -0,0 +1,60 @@
+const menuData = [
+  {
+    name: '主页',
+    icon: 'dashboard',
+    path: 'dashboard',
+  },{
+    name: '产品库',
+    icon: 'shop',
+    path: 'goods',
+  },{
+    name: '订单管理',
+    icon: 'trademark',
+    path: 'order',
+  },{
+    name: '销售统计',
+    icon: 'area-chart',
+    path: 'sold',
+  },{
+    name: '校区管理',
+    icon: 'home',
+    path: 'campus',
+  },{
+    name: '终端管理',
+    icon: 'desktop',
+    path: 'terminal',
+  },{
+    name: '前端展现',
+    icon: 'setting',
+    path: 'frontend',
+  },{
+    name: '账户信息',
+    icon: 'user',
+    path: 'merchant',
+  },{
+    name: '使用说明',
+    icon: 'question-circle-o',
+    path: 'help',
+  },
+];
+
+function formatter(data, parentPath = '') {
+  const list = [];
+  data.forEach((item) => {
+    if (item.children) {
+      list.push({
+        ...item,
+        path: `${parentPath}${item.path}`,
+        children: formatter(item.children, `${parentPath}${item.path}/`),
+      });
+    } else {
+      list.push({
+        ...item,
+        path: `${parentPath}${item.path}`,
+      });
+    }
+  });
+  return list;
+}
+
+export const getMenuData = () => formatter(menuData);

+ 105 - 0
src/common/router.js

@@ -0,0 +1,105 @@
+import React from 'react';
+import dynamic from 'dva/dynamic';
+import { getMenuData } from './menu';
+
+// wrapper of dynamic
+const dynamicWrapper = (app, models, component) => dynamic({
+  app,
+  // eslint-disable-next-line no-underscore-dangle
+  models: () => models.filter(m => !app._models.some(({ namespace }) => namespace === m)).map(m => import(`../models/${m}.js`)),
+  // add routerData prop
+  component: () => {
+    const p = component();
+    return new Promise((resolve, reject) => {
+      p.then((Comp) => {
+        resolve(props => <Comp {...props} routerData={getRouterData(app)} />);
+      }).catch(err => reject(err));
+    });
+  },
+});
+
+function getFlatMenuData(menus) {
+  let keys = {};
+  menus.forEach((item) => {
+    if (item.children) {
+      keys[item.path] = item.name;
+      keys = { ...keys, ...getFlatMenuData(item.children) };
+    } else {
+      keys[item.path] = item.name;
+    }
+  });
+  return keys;
+}
+
+export const getRouterData = (app) => {
+  const routerData = {
+    '/': {
+      component: dynamicWrapper(app, ['user', 'login'], () => import('../layouts/BasicLayout')),
+    },
+    '/help': {
+      component: dynamicWrapper(app, [], () => import('../routes/About')),
+    },
+    '/dashboard': {
+      component: dynamicWrapper(app, [], () => import('../routes/Dashboard')),
+    },
+    '/merchant': {
+      component: dynamicWrapper(app, ['merchant'], () => import('../routes/Merchant')),
+      name: '修改厂商信息',
+    },
+    '/frontend': {
+      component: dynamicWrapper(app, ['recommend', 'mproduct/mproduct'], () => import('../routes/Frontend')),
+      name: '前端展现配置',
+    },
+    '/campus': {
+      component: dynamicWrapper(app, ['campus'], () => import('../routes/Campus')),
+    },
+    '/terminal': {
+      component: dynamicWrapper(app, ['terminal/terminal'], () => import('../routes/Terminal/List')),
+    },
+    '/terminal/add': {
+      component: dynamicWrapper(app, ['terminal/detail', 'campus'], () => import('../routes/Terminal/Edit')),
+      name: '添加终端',
+    },
+    '/terminal/edit/:id': {
+      component: dynamicWrapper(app, ['terminal/detail', 'campus'], () => import('../routes/Terminal/Edit')),
+      name: '编辑终端',
+    },
+    '/goods': {
+      component: dynamicWrapper(app, ['mproduct/mproduct'], () => import('../routes/MProduct/List')),
+    },
+    '/order': {
+      component: dynamicWrapper(app, ['order/order'], () => import('../routes/Order/List')),
+    },
+    '/order/add': {
+      component: dynamicWrapper(app, ['order/detail', 'terminal/terminal', 'mproduct/mproduct'], () => import('../routes/Order/Add')),
+      name: '新建订单',
+    },
+    // '/order/edit/:id': {
+    //   component: dynamicWrapper(app, ['order/detail'], () => import('../routes/Order/detail')),
+    //   name: '修改订单',
+    // },
+    // '/order/profile/:id': {
+    //   component: dynamicWrapper(app, ['order/detail'], () => import('../routes/Order/detail/orderProfile')),
+    //   name: '订单详情',
+    // },
+    '/sold': {
+      component: dynamicWrapper(app, [], () => import('../routes/SoldProduct')),
+    },
+    '/user': {
+      component: dynamicWrapper(app, [], () => import('../layouts/UserLayout')),
+    },
+    '/user/login': {
+      component: dynamicWrapper(app, ['login'], () => import('../routes/Login')),
+    },
+  };
+  // Get name from ./menu.js or just set it in the router data.
+  const menuData = getFlatMenuData(getMenuData());
+  const routerDataWithName = {};
+  Object.keys(routerData).forEach((item) => {
+    routerDataWithName[item] = {
+      ...routerData[item],
+      name: routerData[item].name || menuData[item.replace(/^\//, '')],
+    };
+  });
+  return routerDataWithName;
+};

+ 58 - 0
src/components/Animation/AnimTableBody.js

@@ -0,0 +1,58 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { TweenOneGroup } from 'rc-tween-one';
+
+const enterAnim = [
+  {
+    opacity: 0,
+    x: 30,
+    backgroundColor: '#fffeee',
+    duration: 0,
+  }, {
+    height: 0,
+    duration: 200,
+    type: 'from',
+    delay: 250,
+    ease: 'easeOutQuad',
+    onComplete: (e) => {
+      e.target.style.height = 'auto'
+    },
+  }, {
+    opacity: 1,
+    x: 0,
+    duration: 250,
+    ease: 'easeOutQuad',
+  }, {
+    delay: 1000,
+    backgroundColor: '#fff',
+  },
+]
+
+const leaveAnim = [
+  {
+    duration: 250,
+    x: -30,
+    opacity: 0,
+  }, {
+    height: 0,
+    duration: 200,
+    ease: 'easeOutQuad',
+  },
+];
+
+const AnimTableBody = ({ className, children }) => {
+
+  return (
+    <TweenOneGroup
+      component="tbody"
+      className={className}
+      enter={enterAnim}
+      leave={leaveAnim}
+      appear={false}
+    >
+      {children}
+    </TweenOneGroup>
+  );
+}
+
+export default AnimTableBody;

+ 14 - 0
src/components/Charts/Bar/index.d.ts

@@ -0,0 +1,14 @@
+import * as React from "react";
+export interface BarProps {
+  title: React.ReactNode;
+  color?: string;
+  margin?: [number, number, number, number];
+  height: number;
+  data: Array<{
+    x: string;
+    y: number;
+  }>;
+  autoLabel?: boolean;
+}
+
+export default class Bar extends React.Component<BarProps, any> {}

+ 151 - 0
src/components/Charts/Bar/index.js

@@ -0,0 +1,151 @@
+import React, { PureComponent } from 'react';
+import G2 from 'g2';
+import Debounce from 'lodash-decorators/debounce';
+import Bind from 'lodash-decorators/bind';
+import equal from '../equal';
+import styles from '../index.less';
+
+class Bar extends PureComponent {
+  state = {
+    autoHideXLabels: false,
+  }
+
+  componentDidMount() {
+    this.renderChart(this.props.data);
+
+    window.addEventListener('resize', this.resize);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (!equal(this.props, nextProps)) {
+      this.renderChart(nextProps.data);
+    }
+  }
+
+  componentWillUnmount() {
+    window.removeEventListener('resize', this.resize);
+    if (this.chart) {
+      this.chart.destroy();
+    }
+    this.resize.cancel();
+  }
+
+  @Bind()
+  @Debounce(200)
+  resize() {
+    if (!this.node) {
+      return;
+    }
+    const canvasWidth = this.node.parentNode.clientWidth;
+    const { data = [], autoLabel = true } = this.props;
+    if (!autoLabel) {
+      return;
+    }
+    const minWidth = data.length * 30;
+    const { autoHideXLabels } = this.state;
+
+    if (canvasWidth <= minWidth) {
+      if (!autoHideXLabels) {
+        this.setState({
+          autoHideXLabels: true,
+        });
+        this.renderChart(data);
+      }
+    } else if (autoHideXLabels) {
+      this.setState({
+        autoHideXLabels: false,
+      });
+      this.renderChart(data);
+    }
+  }
+
+  handleRef = (n) => {
+    this.node = n;
+  }
+
+  renderChart(data) {
+    const { autoHideXLabels } = this.state;
+    const {
+      height = 0,
+      fit = true,
+      color = 'rgba(24, 144, 255, 0.85)',
+      margin = [32, 0, (autoHideXLabels ? 8 : 32), 40],
+    } = this.props;
+
+
+    if (!data || (data && data.length < 1)) {
+      return;
+    }
+
+    // clean
+    this.node.innerHTML = '';
+
+    const { Frame } = G2;
+    const frame = new Frame(data);
+
+    const chart = new G2.Chart({
+      container: this.node,
+      forceFit: fit,
+      height: height - 22,
+      legend: null,
+      plotCfg: {
+        margin,
+      },
+    });
+
+    if (autoHideXLabels) {
+      chart.axis('x', {
+        title: false,
+        tickLine: false,
+        labels: false,
+      });
+    } else {
+      chart.axis('x', {
+        title: false,
+      });
+    }
+    chart.axis('y', {
+      title: false,
+      line: false,
+      tickLine: false,
+    });
+
+    chart.source(frame, {
+      x: {
+        type: 'cat',
+      },
+      y: {
+        min: 0,
+      },
+    });
+
+    chart.tooltip({
+      title: null,
+      crosshairs: false,
+      map: {
+        name: 'x',
+      },
+    });
+    chart.interval().position('x*y').color(color).style({
+      fillOpacity: 1,
+    });
+    chart.render();
+
+    this.chart = chart;
+  }
+
+  render() {
+    const { height, title } = this.props;
+
+    return (
+      <div className={styles.chart} style={{ height }}>
+        <div>
+          { title && <h4>{title}</h4>}
+          <div ref={this.handleRef} />
+        </div>
+      </div>
+    );
+  }
+}
+
+export default Bar;

+ 11 - 0
src/components/Charts/ChartCard/index.d.ts

@@ -0,0 +1,11 @@
+import * as React from "react";
+export interface ChartCardProps {
+  title: React.ReactNode;
+  action?: React.ReactNode;
+  total?: React.ReactNode | number;
+  footer?: React.ReactNode;
+  contentHeight?: number;
+  avatar?: React.ReactNode;
+}
+
+export default class ChartCard extends React.Component<ChartCardProps, any> {}

+ 60 - 0
src/components/Charts/ChartCard/index.js

@@ -0,0 +1,60 @@
+import React from 'react';
+import { Card, Spin } from 'antd';
+import classNames from 'classnames';
+
+import styles from './index.less';
+
+const ChartCard = ({
+  loading = false, contentHeight, title, avatar, action, total, footer, children, ...rest
+}) => {
+  const content = (
+    <div className={styles.chartCard}>
+      <div
+        className={classNames(styles.chartTop, { [styles.chartTopMargin]: (!children && !footer) })}
+      >
+        <div className={styles.avatar}>
+          {
+            avatar
+          }
+        </div>
+        <div className={styles.metaWrap}>
+          <div className={styles.meta}>
+            <span className={styles.title}>{title}</span>
+            <span className={styles.action}>{action}</span>
+          </div>
+          {
+            // eslint-disable-next-line
+            (total !== undefined) && (<div className={styles.total} dangerouslySetInnerHTML={{ __html: total }} />)
+          }
+        </div>
+      </div>
+      {
+        children && (
+          <div className={styles.content} style={{ height: contentHeight || 'auto' }}>
+            <div className={contentHeight && styles.contentFixed}>
+              {children}
+            </div>
+          </div>
+        )
+      }
+      {
+        footer && (
+          <div className={classNames(styles.footer, { [styles.footerMargin]: !children })}>
+            {footer}
+          </div>
+        )
+      }
+    </div>
+  );
+
+  return (
+    <Card
+      bodyStyle={{ padding: '20px 24px 8px 24px' }}
+      {...rest}
+    >
+      {<Spin spinning={loading}>{content}</Spin>}
+    </Card>
+  );
+};
+
+export default ChartCard;

+ 74 - 0
src/components/Charts/ChartCard/index.less

@@ -0,0 +1,74 @@
+@import "~antd/lib/style/themes/default.less";
+
+.chartCard {
+  position: relative;
+  .chartTop {
+    position: relative;
+    overflow: hidden;
+    width: 100%;
+  }
+  .chartTopMargin {
+    margin-bottom: 12px;
+  }
+  .chartTopHasMargin {
+    margin-bottom: 20px;
+  }
+  .metaWrap {
+    float: left;
+  }
+  .avatar {
+    position: relative;
+    top: 4px;
+    float: left;
+    margin-right: 20px;
+    img {
+      border-radius: 100%;
+    }
+  }
+  .meta {
+    color: @text-color-secondary;
+    font-size: @font-size-base;
+    line-height: 22px;
+    height: 22px;
+  }
+  .action {
+    cursor: pointer;
+    position: absolute;
+    top: 0;
+    right: 0;
+  }
+  .total {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    word-break: break-all;
+    white-space: nowrap;
+    color: @heading-color;
+    margin-top: 4px;
+    margin-bottom: 0;
+    font-size: 30px;
+    line-height: 38px;
+    height: 38px;
+  }
+  .content {
+    margin-bottom: 12px;
+    position: relative;
+    width: 100%;
+  }
+  .contentFixed {
+    position: absolute;
+    left: 0;
+    bottom: 0;
+    width: 100%;
+  }
+  .footer {
+    border-top: 1px solid @border-color-split;
+    padding-top: 9px;
+    margin-top: 8px;
+    & > * {
+      position: relative;
+    }
+  }
+  .footerMargin {
+    margin-top: 20px;
+  }
+}

+ 7 - 0
src/components/Charts/Field/index.d.ts

@@ -0,0 +1,7 @@
+import * as React from "react";
+export interface FieldProps {
+  label: React.ReactNode;
+  value: React.ReactNode;
+}
+
+export default class Field extends React.Component<FieldProps, any> {}

+ 12 - 0
src/components/Charts/Field/index.js

@@ -0,0 +1,12 @@
+import React from 'react';
+
+import styles from './index.less';
+
+const Field = ({ label, value, ...rest }) => (
+  <div className={styles.field} {...rest}>
+    <span>{label}</span>
+    <span>{value}</span>
+  </div>
+);
+
+export default Field;

+ 16 - 0
src/components/Charts/Field/index.less

@@ -0,0 +1,16 @@
+@import "~antd/lib/style/themes/default.less";
+
+.field {
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  margin: 0;
+  span {
+    font-size: @font-size-base;
+    line-height: 22px;
+  }
+  span:last-child {
+    margin-left: 8px;
+    color: @heading-color;
+  }
+}

+ 10 - 0
src/components/Charts/Gauge/index.d.ts

@@ -0,0 +1,10 @@
+import * as React from "react";
+export interface GaugeProps {
+  title: React.ReactNode;
+  color?: string;
+  height: number;
+  bgColor?: number;
+  percent: number;
+}
+
+export default class Gauge extends React.Component<GaugeProps, any> {}

+ 202 - 0
src/components/Charts/Gauge/index.js

@@ -0,0 +1,202 @@
+import React, { PureComponent } from 'react';
+import G2 from 'g2';
+import equal from '../equal';
+
+const { Shape } = G2;
+
+const primaryColor = '#2F9CFF';
+const backgroundColor = '#F0F2F5';
+
+/* eslint no-underscore-dangle: 0 */
+class Gauge extends PureComponent {
+  componentDidMount() {
+    setTimeout(() => {
+      this.renderChart();
+    }, 10);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (!equal(this.props, nextProps)) {
+      setTimeout(() => {
+        this.renderChart(nextProps);
+      }, 10);
+    }
+  }
+
+  componentWillUnmount() {
+    if (this.chart) {
+      this.chart.destroy();
+    }
+  }
+
+  handleRef = (n) => {
+    this.node = n;
+  }
+
+  initChart(nextProps) {
+    const { title, color = primaryColor } = nextProps || this.props;
+
+    Shape.registShape('point', 'dashBoard', {
+      drawShape(cfg, group) {
+        const originPoint = cfg.points[0];
+        const point = this.parsePoint({ x: originPoint.x, y: 0.4 });
+
+        const center = this.parsePoint({
+          x: 0,
+          y: 0,
+        });
+
+        const shape = group.addShape('polygon', {
+          attrs: {
+            points: [
+              [center.x, center.y],
+              [point.x + 8, point.y],
+              [point.x + 8, point.y - 2],
+              [center.x, center.y - 2],
+            ],
+            radius: 2,
+            lineWidth: 2,
+            arrow: false,
+            fill: color,
+          },
+        });
+
+        group.addShape('Marker', {
+          attrs: {
+            symbol: 'circle',
+            lineWidth: 2,
+            fill: color,
+            radius: 8,
+            x: center.x,
+            y: center.y,
+          },
+        });
+        group.addShape('Marker', {
+          attrs: {
+            symbol: 'circle',
+            lineWidth: 2,
+            fill: '#fff',
+            radius: 5,
+            x: center.x,
+            y: center.y,
+          },
+        });
+
+        const { origin } = cfg;
+        group.addShape('text', {
+          attrs: {
+            x: center.x,
+            y: center.y + 80,
+            text: `${origin._origin.value}%`,
+            textAlign: 'center',
+            fontSize: 24,
+            fill: 'rgba(0, 0, 0, 0.85)',
+          },
+        });
+        group.addShape('text', {
+          attrs: {
+            x: center.x,
+            y: center.y + 45,
+            text: title,
+            textAlign: 'center',
+            fontSize: 14,
+            fill: 'rgba(0, 0, 0, 0.43)',
+          },
+        });
+
+        return shape;
+      },
+    });
+  }
+
+  renderChart(nextProps) {
+    const {
+      height, color = primaryColor, bgColor = backgroundColor, title, percent, format,
+    } = nextProps || this.props;
+    const data = [{ name: title, value: percent }];
+
+    if (this.chart) {
+      this.chart.clear();
+    }
+    if (this.node) {
+      this.node.innerHTML = '';
+    }
+
+    this.initChart(nextProps);
+
+    const chart = new G2.Chart({
+      container: this.node,
+      forceFit: true,
+      height,
+      animate: false,
+      plotCfg: {
+        margin: [10, 10, 30, 10],
+      },
+    });
+
+    chart.source(data);
+
+    chart.tooltip(false);
+
+    chart.coord('gauge', {
+      startAngle: -1.2 * Math.PI,
+      endAngle: 0.20 * Math.PI,
+    });
+    chart.col('value', {
+      type: 'linear',
+      nice: true,
+      min: 0,
+      max: 100,
+      tickCount: 6,
+    });
+    chart.axis('value', {
+      subTick: false,
+      tickLine: {
+        stroke: color,
+        lineWidth: 2,
+        value: -14,
+      },
+      labelOffset: -12,
+      formatter: format,
+    });
+    chart.point().position('value').shape('dashBoard');
+    draw(data);
+
+    /* eslint no-shadow: 0 */
+    function draw(data) {
+      const val = data[0].value;
+      const lineWidth = 12;
+      chart.guide().clear();
+
+      chart.guide().arc(() => {
+        return [0, 0.95];
+      }, () => {
+        return [val, 0.95];
+      }, {
+        stroke: color,
+        lineWidth,
+      });
+
+      chart.guide().arc(() => {
+        return [val, 0.95];
+      }, (arg) => {
+        return [arg.max, 0.95];
+      }, {
+        stroke: bgColor,
+        lineWidth,
+      });
+
+      chart.changeData(data);
+    }
+
+    this.chart = chart;
+  }
+
+  render() {
+    return (
+      <div ref={this.handleRef} />
+    );
+  }
+}
+
+export default Gauge;

+ 29 - 0
src/components/Charts/MiniArea/index.d.ts

@@ -0,0 +1,29 @@
+import * as React from "react";
+
+// g2已经更新到3.0
+// 不带的写了
+
+export interface Axis {
+  title: any;
+  line: any;
+  gridAlign: any;
+  labels: any;
+  tickLine: any;
+  grid: any;
+}
+
+export interface MiniAreaProps {
+  color?: string;
+  height: number;
+  borderColor?: string;
+  line?: boolean;
+  animate?: boolean;
+  xAxis?: Axis;
+  yAxis?: Axis;
+  data: Array<{
+    x: number;
+    y: number;
+  }>;
+}
+
+export default class MiniArea extends React.Component<MiniAreaProps, any> {}

+ 125 - 0
src/components/Charts/MiniArea/index.js

@@ -0,0 +1,125 @@
+import React, { PureComponent } from 'react';
+import G2 from 'g2';
+import equal from '../equal';
+import styles from '../index.less';
+
+class MiniArea extends PureComponent {
+  static defaultProps = {
+    borderColor: '#1890FF',
+    color: 'rgba(24, 144, 255, 0.2)',
+  };
+
+  componentDidMount() {
+    this.renderChart(this.props.data);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (!equal(this.props, nextProps)) {
+      this.renderChart(nextProps.data);
+    }
+  }
+
+  componentWillUnmount() {
+    if (this.chart) {
+      this.chart.destroy();
+    }
+  }
+
+  handleRef = (n) => {
+    this.node = n;
+  }
+
+  renderChart(data) {
+    const {
+      height = 0, fit = true, color, borderWidth = 2, line, xAxis, yAxis, animate = true,
+    } = this.props;
+    const borderColor = this.props.borderColor || color;
+
+    if (!data || (data && data.length < 1)) {
+      return;
+    }
+
+    // clean
+    this.node.innerHTML = '';
+
+    const chart = new G2.Chart({
+      container: this.node,
+      forceFit: fit,
+      height: height + 54,
+      animate,
+      plotCfg: {
+        margin: [36, 5, 30, 5],
+      },
+      legend: null,
+    });
+
+    if (!xAxis && !yAxis) {
+      chart.axis(false);
+    }
+
+    if (xAxis) {
+      chart.axis('x', xAxis);
+    } else {
+      chart.axis('x', false);
+    }
+
+    if (yAxis) {
+      chart.axis('y', yAxis);
+    } else {
+      chart.axis('y', false);
+    }
+
+    const dataConfig = {
+      x: {
+        type: 'cat',
+        range: [0, 1],
+        ...xAxis,
+      },
+      y: {
+        min: 0,
+        ...yAxis,
+      },
+    };
+
+    chart.tooltip({
+      title: null,
+      crosshairs: false,
+      map: {
+        title: null,
+        name: 'x',
+        value: 'y',
+      },
+    });
+
+    const view = chart.createView();
+    view.source(data, dataConfig);
+
+    view.area().position('x*y').color(color).shape('smooth')
+      .style({ fillOpacity: 1 });
+
+    if (line) {
+      const view2 = chart.createView();
+      view2.source(data, dataConfig);
+      view2.line().position('x*y').color(borderColor).size(borderWidth)
+        .shape('smooth');
+      view2.tooltip(false);
+    }
+    chart.render();
+
+    this.chart = chart;
+  }
+
+  render() {
+    const { height } = this.props;
+
+    return (
+      <div className={styles.miniChart} style={{ height }}>
+        <div className={styles.chartContent}>
+          <div ref={this.handleRef} />
+        </div>
+      </div>
+    );
+  }
+}
+
+export default MiniArea;

+ 11 - 0
src/components/Charts/MiniBar/index.d.ts

@@ -0,0 +1,11 @@
+import * as React from "react";
+export interface MiniBarProps {
+  color?: string;
+  height: number;
+  data: Array<{
+    x: number;
+    y: number;
+  }>;
+}
+
+export default class MiniBar extends React.Component<MiniBarProps, any> {}

+ 87 - 0
src/components/Charts/MiniBar/index.js

@@ -0,0 +1,87 @@
+import React, { PureComponent } from 'react';
+import G2 from 'g2';
+import equal from '../equal';
+import styles from '../index.less';
+
+class MiniBar extends PureComponent {
+  componentDidMount() {
+    this.renderChart(this.props.data);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (!equal(this.props, nextProps)) {
+      this.renderChart(nextProps.data);
+    }
+  }
+
+  componentWillUnmount() {
+    if (this.chart) {
+      this.chart.destroy();
+    }
+  }
+
+  handleRef = (n) => {
+    this.node = n;
+  }
+
+  renderChart(data) {
+    const { height = 0, fit = true, color = '#1890FF' } = this.props;
+
+    if (!data || (data && data.length < 1)) {
+      return;
+    }
+
+    // clean
+    this.node.innerHTML = '';
+
+    const { Frame } = G2;
+    const frame = new Frame(data);
+
+    const chart = new G2.Chart({
+      container: this.node,
+      forceFit: fit,
+      height: height + 54,
+      plotCfg: {
+        margin: [36, 5, 30, 5],
+      },
+      legend: null,
+    });
+
+    chart.axis(false);
+
+    chart.source(frame, {
+      x: {
+        type: 'cat',
+      },
+      y: {
+        min: 0,
+      },
+    });
+
+    chart.tooltip({
+      title: null,
+      crosshairs: false,
+      map: {
+        name: 'x',
+      },
+    });
+    chart.interval().position('x*y').color(color);
+    chart.render();
+
+    this.chart = chart;
+  }
+
+  render() {
+    const { height } = this.props;
+
+    return (
+      <div className={styles.miniChart} style={{ height }}>
+        <div className={styles.chartContent}>
+          <div ref={this.handleRef} />
+        </div>
+      </div>
+    );
+  }
+}
+
+export default MiniBar;

+ 12 - 0
src/components/Charts/MiniProgress/index.d.ts

@@ -0,0 +1,12 @@
+import * as React from "react";
+export interface MiniProgressProps {
+  target: number;
+  color?: string;
+  strokeWidth?: number;
+  percent?: number;
+}
+
+export default class MiniProgress extends React.Component<
+  MiniProgressProps,
+  any
+> {}

+ 30 - 0
src/components/Charts/MiniProgress/index.js

@@ -0,0 +1,30 @@
+import React from 'react';
+import { Tooltip } from 'antd';
+
+import styles from './index.less';
+
+const MiniProgress = ({ target, color = 'rgb(19, 194, 194)', strokeWidth, percent }) => (
+  <div className={styles.miniProgress}>
+    <Tooltip title={`目标值: ${target}%`}>
+      <div
+        className={styles.target}
+        style={{ left: (target ? `${target}%` : null) }}
+      >
+        <span style={{ backgroundColor: (color || null) }} />
+        <span style={{ backgroundColor: (color || null) }} />
+      </div>
+    </Tooltip>
+    <div className={styles.progressWrap}>
+      <div
+        className={styles.progress}
+        style={{
+          backgroundColor: (color || null),
+          width: (percent ? `${percent}%` : null),
+          height: (strokeWidth || null),
+        }}
+      />
+    </div>
+  </div>
+);
+
+export default MiniProgress;

+ 35 - 0
src/components/Charts/MiniProgress/index.less

@@ -0,0 +1,35 @@
+@import "~antd/lib/style/themes/default.less";
+
+.miniProgress {
+  padding: 5px 0;
+  position: relative;
+  width: 100%;
+  .progressWrap {
+    background-color: @background-color-base;
+    position: relative;
+  }
+  .progress {
+    transition: all .4s cubic-bezier(.08, .82, .17, 1) 0s;
+    border-radius: 1px 0 0 1px;
+    background-color: @primary-color;
+    width: 0;
+    height: 100%;
+  }
+  .target {
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    span {
+      border-radius: 100px;
+      position: absolute;
+      top: 0;
+      left: 0;
+      height: 4px;
+      width: 2px;
+    }
+    span:last-child {
+      top: auto;
+      bottom: 0;
+    }
+  }
+}

+ 20 - 0
src/components/Charts/Pie/index.d.ts

@@ -0,0 +1,20 @@
+import * as React from "react";
+export interface PieProps {
+  animate?: boolean;
+  color?: string;
+  height: number;
+  hasLegend?: boolean;
+  margin?: [number, number, number, number];
+  percent?: number;
+  data?: Array<{
+    x: string;
+    y: number;
+  }>;
+  total?: string;
+  title?: React.ReactNode;
+  tooltip?: boolean;
+  valueFormat?: (value: string) => string;
+  subTitle?: React.ReactNode;
+}
+
+export default class Pie extends React.Component<PieProps, any> {}

+ 260 - 0
src/components/Charts/Pie/index.js

@@ -0,0 +1,260 @@
+import React, { Component } from 'react';
+import G2 from 'g2';
+import { Divider } from 'antd';
+import classNames from 'classnames';
+import ReactFitText from 'react-fittext';
+import Debounce from 'lodash-decorators/debounce';
+import Bind from 'lodash-decorators/bind';
+import equal from '../equal';
+import styles from './index.less';
+
+/* eslint react/no-danger:0 */
+class Pie extends Component {
+  state = {
+    legendData: [],
+    legendBlock: true,
+  };
+
+  componentDidMount() {
+    this.renderChart();
+    this.resize();
+    window.addEventListener('resize', this.resize);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (!equal(this.props, nextProps)) {
+      this.renderChart(nextProps.data);
+    }
+  }
+
+  componentWillUnmount() {
+    window.removeEventListener('resize', this.resize);
+    if (this.chart) {
+      this.chart.destroy();
+    }
+    this.resize.cancel();
+  }
+
+  @Bind()
+  @Debounce(300)
+  resize() {
+    const { hasLegend } = this.props;
+    if (!hasLegend || !this.root) {
+      window.removeEventListener('resize', this.resize);
+      return;
+    }
+    if (this.root.parentNode.clientWidth <= 380) {
+      if (!this.state.legendBlock) {
+        this.setState({
+          legendBlock: true,
+        }, () => {
+          this.renderChart();
+        });
+      }
+    } else if (this.state.legendBlock) {
+      this.setState({
+        legendBlock: false,
+      }, () => {
+        this.renderChart();
+      });
+    }
+  }
+
+  handleRef = (n) => {
+    this.node = n;
+  }
+
+  handleRoot = (n) => {
+    this.root = n;
+  }
+
+  handleLegendClick = (item, i) => {
+    const newItem = item;
+    newItem.checked = !newItem.checked;
+
+    const { legendData } = this.state;
+    legendData[i] = newItem;
+
+    if (this.chart) {
+      const filterItem = legendData.filter(l => l.checked).map(l => l.x);
+      this.chart.filter('x', filterItem);
+      this.chart.repaint();
+    }
+
+    this.setState({
+      legendData,
+    });
+  }
+
+  renderChart(d) {
+    let data = d || this.props.data;
+
+    const {
+      height = 0,
+      hasLegend,
+      fit = true,
+      margin = [12, 0, 12, 0], percent, color,
+      inner = 0.75,
+      animate = true,
+      colors,
+      lineWidth = 0,
+    } = this.props;
+
+    const defaultColors = colors;
+
+    let selected = this.props.selected || true;
+    let tooltip = this.props.tooltips || true;
+
+    let formatColor;
+    if (percent) {
+      selected = false;
+      tooltip = false;
+      formatColor = (value) => {
+        if (value === '占比') {
+          return color || 'rgba(24, 144, 255, 0.85)';
+        } else {
+          return '#F0F2F5';
+        }
+      };
+
+      /* eslint no-param-reassign: */
+      data = [
+        {
+          x: '占比',
+          y: parseFloat(percent),
+        },
+        {
+          x: '反比',
+          y: 100 - parseFloat(percent),
+        },
+      ];
+    }
+
+    if (!data || (data && data.length < 1)) {
+      return;
+    }
+
+    // clean
+    this.node.innerHTML = '';
+
+    const { Stat } = G2;
+
+    const chart = new G2.Chart({
+      container: this.node,
+      forceFit: fit,
+      height,
+      plotCfg: {
+        margin,
+      },
+      animate,
+    });
+
+    if (!tooltip) {
+      chart.tooltip(false);
+    } else {
+      chart.tooltip({
+        title: null,
+      });
+    }
+
+    chart.axis(false);
+    chart.legend(false);
+
+    chart.source(data, {
+      x: {
+        type: 'cat',
+        range: [0, 1],
+      },
+      y: {
+        min: 0,
+      },
+    });
+
+    chart.coord('theta', {
+      inner,
+    });
+
+    chart
+      .intervalStack()
+      .position(Stat.summary.percent('y'))
+      .style({ lineWidth, stroke: '#fff' })
+      .color('x', percent ? formatColor : defaultColors)
+      .selected(selected);
+
+    chart.render();
+
+    this.chart = chart;
+
+    let legendData = [];
+    if (hasLegend) {
+      const geom = chart.getGeoms()[0]; // 获取所有的图形
+      const items = geom.getData(); // 获取图形对应的数据
+      legendData = items.map((item) => {
+        /* eslint no-underscore-dangle:0 */
+        const origin = item._origin;
+        origin.color = item.color;
+        origin.checked = true;
+        return origin;
+      });
+    }
+
+    this.setState({
+      legendData,
+    });
+  }
+
+  render() {
+    const { valueFormat, subTitle, total, hasLegend, className, style } = this.props;
+    const { legendData, legendBlock } = this.state;
+    const pieClassName = classNames(styles.pie, className, {
+      [styles.hasLegend]: !!hasLegend,
+      [styles.legendBlock]: legendBlock,
+    });
+
+    return (
+      <div ref={this.handleRoot} className={pieClassName} style={style}>
+        <ReactFitText maxFontSize={25}>
+          <div className={styles.chart}>
+            <div ref={this.handleRef} style={{ fontSize: 0 }} />
+            {
+              (subTitle || total) && (
+                <div className={styles.total}>
+                  {subTitle && <h4 className="pie-sub-title">{subTitle}</h4>}
+                  {
+                    // eslint-disable-next-line
+                    total && <div className="pie-stat" dangerouslySetInnerHTML={{ __html: total }} />
+                  }
+                </div>
+              )
+            }
+          </div>
+        </ReactFitText>
+
+        {
+          hasLegend && (
+            <ul className={styles.legend}>
+              {
+                legendData.map((item, i) => (
+                  <li key={item.x} onClick={() => this.handleLegendClick(item, i)}>
+                    <span className={styles.dot} style={{ backgroundColor: !item.checked ? '#aaa' : item.color }} />
+                    <span className={styles.legendTitle}>{item.x}</span>
+                    <Divider type="vertical" />
+                    <span className={styles.percent}>{`${(item['..percent'] * 100).toFixed(2)}%`}</span>
+                    <span
+                      className={styles.value}
+                      dangerouslySetInnerHTML={{
+                        __html: valueFormat ? valueFormat(item.y) : item.y,
+                      }}
+                    />
+                  </li>
+                ))
+              }
+            </ul>
+          )
+        }
+      </div>
+    );
+  }
+}
+
+export default Pie;

+ 94 - 0
src/components/Charts/Pie/index.less

@@ -0,0 +1,94 @@
+@import "~antd/lib/style/themes/default.less";
+
+.pie {
+  position: relative;
+  .chart {
+    position: relative;
+  }
+  &.hasLegend .chart {
+    width: ~"calc(100% - 240px)";
+  }
+  .legend {
+    position: absolute;
+    right: 0;
+    min-width: 200px;
+    top: 50%;
+    transform: translateY(-50%);
+    margin: 0 20px;
+    list-style: none;
+    padding: 0;
+    li {
+      cursor: pointer;
+      margin-bottom: 16px;
+      height: 22px;
+      line-height: 22px;
+      &:last-child {
+        margin-bottom: 0;
+      }
+    }
+  }
+  .dot {
+    border-radius: 8px;
+    display: inline-block;
+    margin-right: 8px;
+    position: relative;
+    top: -1px;
+    height: 8px;
+    width: 8px;
+  }
+  .line {
+    background-color: @border-color-split;
+    display: inline-block;
+    margin-right: 8px;
+    width: 1px;
+    height: 16px;
+  }
+  .legendTitle {
+    color: @text-color;
+  }
+  .percent {
+    color: @text-color-secondary;
+  }
+  .value {
+    position: absolute;
+    right: 0;
+  }
+  .title {
+    margin-bottom: 8px;
+  }
+  .total {
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    text-align: center;
+    height: 62px;
+    transform: translate(-50%, -50%);
+    & > h4 {
+      color: @text-color-secondary;
+      font-size: 14px;
+      line-height: 22px;
+      height: 22px;
+      margin-bottom: 8px;
+      font-weight: normal;
+    }
+    & > p {
+      color: @heading-color;
+      display: block;
+      font-size: 1.2em;
+      height: 32px;
+      line-height: 32px;
+      white-space: nowrap;
+    }
+  }
+}
+
+.legendBlock {
+  &.hasLegend .chart {
+    width: 100%;
+    margin: 0 0 32px 0;
+  }
+  .legend {
+    position: relative;
+    transform: none;
+  }
+}

+ 14 - 0
src/components/Charts/Radar/index.d.ts

@@ -0,0 +1,14 @@
+import * as React from "react";
+export interface RadarProps {
+  title?: React.ReactNode;
+  height: number;
+  margin?: [number, number, number, number];
+  hasLegend?: boolean;
+  data: Array<{
+    name: string;
+    label: string;
+    value: string;
+  }>;
+}
+
+export default class Radar extends React.Component<RadarProps, any> {}

+ 189 - 0
src/components/Charts/Radar/index.js

@@ -0,0 +1,189 @@
+import React, { PureComponent } from 'react';
+import G2 from 'g2';
+import { Row, Col } from 'antd';
+import equal from '../equal';
+import styles from './index.less';
+
+/* eslint react/no-danger:0 */
+class Radar extends PureComponent {
+  state = {
+    legendData: [],
+  }
+
+  componentDidMount() {
+    this.renderChart(this.props.data);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (!equal(this.props, nextProps)) {
+      this.renderChart(nextProps.data);
+    }
+  }
+
+  componentWillUnmount() {
+    if (this.chart) {
+      this.chart.destroy();
+    }
+  }
+
+  handleRef = (n) => {
+    this.node = n;
+  }
+
+  handleLegendClick = (item, i) => {
+    const newItem = item;
+    newItem.checked = !newItem.checked;
+
+    const { legendData } = this.state;
+    legendData[i] = newItem;
+
+    if (this.chart) {
+      const filterItem = legendData.filter(l => l.checked).map(l => l.name);
+      this.chart.filter('name', filterItem);
+      this.chart.repaint();
+    }
+
+    this.setState({
+      legendData,
+    });
+  }
+
+  renderChart(data) {
+    const { height = 0,
+      hasLegend = true,
+      fit = true,
+      tickCount = 4,
+      margin = [24, 30, 16, 30] } = this.props;
+
+    const colors = [
+      '#1890FF', '#FACC14', '#2FC25B', '#8543E0', '#F04864', '#13C2C2', '#fa8c16', '#a0d911',
+    ];
+
+    if (!data || (data && data.length < 1)) {
+      return;
+    }
+
+    // clean
+    this.node.innerHTML = '';
+
+    const chart = new G2.Chart({
+      container: this.node,
+      forceFit: fit,
+      height: height - (hasLegend ? 80 : 22),
+      plotCfg: {
+        margin,
+      },
+    });
+
+    this.chart = chart;
+
+    chart.source(data, {
+      value: {
+        min: 0,
+        tickCount,
+      },
+    });
+
+    chart.coord('polar');
+    chart.legend(false);
+
+    chart.axis('label', {
+      line: null,
+      labelOffset: 8,
+      labels: {
+        label: {
+          fill: 'rgba(0, 0, 0, .65)',
+        },
+      },
+      grid: {
+        line: {
+          stroke: '#e9e9e9',
+          lineWidth: 1,
+          lineDash: [0, 0],
+        },
+      },
+    });
+
+    chart.axis('value', {
+      grid: {
+        type: 'polygon',
+        line: {
+          stroke: '#e9e9e9',
+          lineWidth: 1,
+          lineDash: [0, 0],
+        },
+      },
+      labels: {
+        label: {
+          fill: 'rgba(0, 0, 0, .65)',
+        },
+      },
+    });
+
+    chart.line().position('label*value').color('name', colors);
+    chart.point().position('label*value').color('name', colors).shape('circle')
+      .size(3);
+
+    chart.render();
+
+    if (hasLegend) {
+      const geom = chart.getGeoms()[0]; // 获取所有的图形
+      const items = geom.getData(); // 获取图形对应的数据
+      const legendData = items.map((item) => {
+        /* eslint no-underscore-dangle:0 */
+        const origin = item._origin;
+        const result = {
+          name: origin[0].name,
+          color: item.color,
+          checked: true,
+          value: origin.reduce((p, n) => p + n.value, 0),
+        };
+
+        return result;
+      });
+
+      this.setState({
+        legendData,
+      });
+    }
+  }
+
+  render() {
+    const { height, title, hasLegend } = this.props;
+    const { legendData } = this.state;
+
+    return (
+      <div className={styles.radar} style={{ height }}>
+        <div>
+          {title && <h4>{title}</h4>}
+          <div ref={this.handleRef} />
+          {
+            hasLegend && (
+              <Row className={styles.legend}>
+                {
+                  legendData.map((item, i) => (
+                    <Col
+                      span={(24 / legendData.length)}
+                      key={item.name}
+                      onClick={() => this.handleLegendClick(item, i)}
+                    >
+                      <div className={styles.legendItem}>
+                        <p>
+                          <span className={styles.dot} style={{ backgroundColor: !item.checked ? '#aaa' : item.color }} />
+                          <span>{item.name}</span>
+                        </p>
+                        <h6>{item.value}</h6>
+                      </div>
+                    </Col>
+                  ))
+                }
+              </Row>
+            )
+          }
+        </div>
+      </div>
+    );
+  }
+}
+
+export default Radar;

+ 46 - 0
src/components/Charts/Radar/index.less

@@ -0,0 +1,46 @@
+@import "~antd/lib/style/themes/default.less";
+
+.radar {
+  .legend {
+    margin-top: 16px;
+    .legendItem {
+      position: relative;
+      text-align: center;
+      cursor: pointer;
+      color: @text-color-secondary;
+      line-height: 22px;
+      p {
+        margin: 0;
+      }
+      h6 {
+        color: @heading-color;
+        padding-left: 16px;
+        font-size: 24px;
+        line-height: 32px;
+        margin-top: 4px;
+        margin-bottom: 0;
+      }
+      &:after {
+        background-color: @border-color-split;
+        position: absolute;
+        top: 8px;
+        right: 0;
+        height: 40px;
+        width: 1px;
+        content: '';
+      }
+    }
+    > :last-child .legendItem:after {
+      display: none;
+    }
+    .dot {
+      border-radius: 6px;
+      display: inline-block;
+      margin-right: 6px;
+      position: relative;
+      top: -1px;
+      height: 6px;
+      width: 6px;
+    }
+  }
+}

+ 10 - 0
src/components/Charts/TagCloud/index.d.ts

@@ -0,0 +1,10 @@
+import * as React from "react";
+export interface TagCloudProps {
+  data: Array<{
+    name: string;
+    value: number;
+  }>;
+  height: number;
+}
+
+export default class TagCloud extends React.Component<TagCloudProps, any> {}

+ 170 - 0
src/components/Charts/TagCloud/index.js

@@ -0,0 +1,170 @@
+import React, { PureComponent } from 'react';
+import classNames from 'classnames';
+import G2 from 'g2';
+import Cloud from 'g-cloud';
+import Debounce from 'lodash-decorators/debounce';
+import Bind from 'lodash-decorators/bind';
+import styles from './index.less';
+
+/* eslint no-underscore-dangle: 0 */
+/* eslint no-param-reassign: 0 */
+
+const imgUrl = 'https://gw.alipayobjects.com/zos/rmsportal/gWyeGLCdFFRavBGIDzWk.png';
+
+class TagCloud extends PureComponent {
+  componentDidMount() {
+    this.initTagCloud();
+    this.renderChart();
+
+    window.addEventListener('resize', this.resize);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (this.props.data !== nextProps.data) {
+      this.renderChart(nextProps.data);
+    }
+  }
+
+  componentWillUnmount() {
+    window.removeEventListener('resize', this.resize);
+    this.renderChart.cancel();
+  }
+
+  resize = () => {
+    this.renderChart();
+  }
+
+  initTagCloud = () => {
+    const { Util, Shape } = G2;
+
+    function getTextAttrs(cfg) {
+      const textAttrs = Util.mix(true, {}, {
+        fillOpacity: cfg.opacity,
+        fontSize: cfg.size,
+        rotate: cfg.origin._origin.rotate,
+        // rotate: cfg.origin._origin.rotate,
+        text: cfg.origin._origin.text,
+        textAlign: 'center',
+        fill: cfg.color,
+        textBaseline: 'Alphabetic',
+      }, cfg.style);
+      return textAttrs;
+    }
+
+    // 给point注册一个词云的shape
+    Shape.registShape('point', 'cloud', {
+      drawShape(cfg, container) {
+        cfg.points = this.parsePoints(cfg.points);
+        const attrs = getTextAttrs(cfg);
+        const shape = container.addShape('text', {
+          attrs: Util.mix(attrs, {
+            x: cfg.points[0].x,
+            y: cfg.points[0].y,
+          }),
+        });
+        return shape;
+      },
+    });
+  }
+
+  saveRootRef = (node) => {
+    this.root = node;
+  }
+
+  saveNodeRef = (node) => {
+    this.node = node;
+  }
+
+  @Bind()
+  @Debounce(500)
+  renderChart(newData) {
+    const data = newData || this.props.data;
+    if (!data || data.length < 1) {
+      return;
+    }
+
+    const colors = ['#1890FF', '#41D9C7', '#2FC25B', '#FACC14', '#9AE65C'];
+
+    const height = this.props.height * 4;
+    let width = 0;
+    if (this.root) {
+      width = this.root.offsetWidth * 4;
+    }
+
+    data.sort((a, b) => b.value - a.value);
+
+    const max = data[0].value;
+    const min = data[data.length - 1].value;
+
+    // 构造一个词云布局对象
+    const layout = new Cloud({
+      words: data,
+      width,
+      height,
+
+      rotate: () => 0,
+
+      // 设定文字大小配置函数(默认为12-24px的随机大小)
+      size: words => (((words.value - min) / (max - min)) * 50) + 30,
+
+      // 设定文字内容
+      text: words => words.name,
+    });
+
+    layout.image(imgUrl, (imageCloud) => {
+      // clean
+      if (this.node) {
+        this.node.innerHTML = '';
+      }
+
+      // 执行词云布局函数,并在回调函数中调用G2对结果进行绘制
+      imageCloud.exec((texts) => {
+        const chart = new G2.Chart({
+          container: this.node,
+          width,
+          height,
+          plotCfg: {
+            margin: 0,
+          },
+        });
+
+        chart.legend(false);
+        chart.axis(false);
+        chart.tooltip(false);
+
+        chart.source(texts);
+
+        // 将词云坐标系调整为G2的坐标系
+        chart.coord().reflect();
+
+        chart
+          .point()
+          .position('x*y')
+          .color('text', colors)
+          .size('size', size => size)
+          .shape('cloud')
+          .style({
+            fontStyle: texts[0].style,
+            fontFamily: texts[0].font,
+            fontWeight: texts[0].weight,
+          });
+
+        chart.render();
+      });
+    });
+  }
+
+  render() {
+    return (
+      <div
+        className={classNames(styles.tagCloud, this.props.className)}
+        ref={this.saveRootRef}
+        style={{ width: '100%' }}
+      >
+        <div ref={this.saveNodeRef} style={{ height: this.props.height }} />
+      </div>
+    );
+  }
+}
+
+export default TagCloud;

+ 6 - 0
src/components/Charts/TagCloud/index.less

@@ -0,0 +1,6 @@
+.tagCloud {
+  canvas {
+    transform: scale(0.25);
+    transform-origin: 0 0;
+  }
+}

+ 15 - 0
src/components/Charts/TimelineChart/index.d.ts

@@ -0,0 +1,15 @@
+import * as React from "react";
+export interface TimelineChartProps {
+  data: Array<{
+    x: string;
+    y1: string;
+    y2: string;
+  }>;
+  titleMap: { y1: string; y2: string };
+  height?: number;
+}
+
+export default class TimelineChart extends React.Component<
+  TimelineChartProps,
+  any
+> {}

+ 125 - 0
src/components/Charts/TimelineChart/index.js

@@ -0,0 +1,125 @@
+import React, { Component } from 'react';
+import G2 from 'g2';
+import Slider from 'g2-plugin-slider';
+import styles from './index.less';
+
+class TimelineChart extends Component {
+  componentDidMount() {
+    this.renderChart(this.props.data);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (nextProps.data !== this.props.data) {
+      this.renderChart(nextProps.data);
+    }
+  }
+
+  componentWillUnmount() {
+    if (this.chart) {
+      this.chart.destroy();
+    }
+    if (this.slider) {
+      this.slider.destroy();
+    }
+  }
+
+  sliderId = `timeline-chart-slider-${Math.random() * 1000}`
+
+  handleRef = (n) => {
+    this.node = n;
+  }
+
+  renderChart(data) {
+    const { height = 400, margin = [60, 20, 40, 40], titleMap, borderWidth = 2 } = this.props;
+
+    if (!data || (data && data.length < 1)) {
+      return;
+    }
+
+    // clean
+    if (this.sliderId) {
+      document.getElementById(this.sliderId).innerHTML = '';
+    }
+    this.node.innerHTML = '';
+
+    const chart = new G2.Chart({
+      container: this.node,
+      forceFit: true,
+      height,
+      plotCfg: {
+        margin,
+      },
+    });
+
+    chart.axis('x', {
+      title: false,
+    });
+    chart.axis('y1', {
+      title: false,
+    });
+    chart.axis('y2', false);
+
+    chart.legend({
+      mode: false,
+      position: 'top',
+    });
+
+    let max;
+    if (data[0] && data[0].y1 && data[0].y2) {
+      max = Math.max(data.sort((a, b) => b.y1 - a.y1)[0].y1,
+        data.sort((a, b) => b.y2 - a.y2)[0].y2);
+    }
+
+    chart.source(data, {
+      x: {
+        type: 'timeCat',
+        tickCount: 16,
+        mask: 'HH:MM',
+        range: [0, 1],
+      },
+      y1: {
+        alias: titleMap.y1,
+        max,
+        min: 0,
+      },
+      y2: {
+        alias: titleMap.y2,
+        max,
+        min: 0,
+      },
+    });
+
+    chart.line().position('x*y1').color('#1890FF').size(borderWidth);
+    chart.line().position('x*y2').color('#2FC25B').size(borderWidth);
+
+    this.chart = chart;
+
+    /* eslint new-cap:0 */
+    const slider = new Slider({
+      domId: this.sliderId,
+      height: 26,
+      xDim: 'x',
+      yDim: 'y1',
+      charts: [chart],
+    });
+    slider.render();
+
+    this.slider = slider;
+  }
+
+  render() {
+    const { height, title } = this.props;
+
+    return (
+      <div className={styles.timelineChart} style={{ height }}>
+        <div>
+          { title && <h4>{title}</h4>}
+          <div ref={this.handleRef} />
+          <div id={this.sliderId} />
+        </div>
+      </div>
+    );
+  }
+}
+
+export default TimelineChart;

+ 3 - 0
src/components/Charts/TimelineChart/index.less

@@ -0,0 +1,3 @@
+.timelineChart {
+  background: #fff;
+}

+ 9 - 0
src/components/Charts/WaterWave/index.d.ts

@@ -0,0 +1,9 @@
+import * as React from "react";
+export interface WaterWaveProps {
+  title: React.ReactNode;
+  color?: string;
+  height: number;
+  percent: number;
+}
+
+export default class WaterWave extends React.Component<WaterWaveProps, any> {}

+ 200 - 0
src/components/Charts/WaterWave/index.js

@@ -0,0 +1,200 @@
+import React, { PureComponent } from 'react';
+import styles from './index.less';
+
+/* eslint no-return-assign: 0 */
+// riddle: https://riddle.alibaba-inc.com/riddles/2d9a4b90
+
+class WaterWave extends PureComponent {
+  static defaultProps = {
+    height: 160,
+  }
+  state = {
+    radio: 1,
+  }
+
+  componentDidMount() {
+    this.renderChart();
+    this.resize();
+
+    window.addEventListener('resize', this.resize);
+  }
+
+  componentWillUnmount() {
+    cancelAnimationFrame(this.timer);
+    if (this.node) {
+      this.node.innerHTML = '';
+    }
+    window.removeEventListener('resize', this.resize);
+  }
+
+  resize = () => {
+    const { height } = this.props;
+    const { offsetWidth } = this.root.parentNode;
+    this.setState({
+      radio: offsetWidth < height ? offsetWidth / height : 1,
+    });
+  }
+
+  renderChart() {
+    const { percent, color = '#1890FF' } = this.props;
+    const data = percent / 100;
+    const self = this;
+
+    if (!this.node || !data) {
+      return;
+    }
+
+    const canvas = this.node;
+    const ctx = canvas.getContext('2d');
+
+    const canvasWidth = canvas.width;
+    const canvasHeight = canvas.height;
+    const radius = canvasWidth / 2;
+    const lineWidth = 2;
+    const cR = radius - (lineWidth);
+
+    ctx.beginPath();
+    ctx.lineWidth = lineWidth * 2;
+
+    const axisLength = canvasWidth - (lineWidth);
+    const unit = axisLength / 8;
+    const range = 0.2; // 振幅
+    let currRange = range;
+    const xOffset = lineWidth;
+    let sp = 0; // 周期偏移量
+    let currData = 0;
+    const waveupsp = 0.005; // 水波上涨速度
+
+    let arcStack = [];
+    const bR = radius - (lineWidth);
+    const circleOffset = -(Math.PI / 2);
+    let circleLock = true;
+
+    for (let i = circleOffset; i < circleOffset + (2 * Math.PI); i += 1 / (8 * Math.PI)) {
+      arcStack.push([
+        radius + (bR * Math.cos(i)),
+        radius + (bR * Math.sin(i)),
+      ]);
+    }
+
+    const cStartPoint = arcStack.shift();
+    ctx.strokeStyle = color;
+    ctx.moveTo(cStartPoint[0], cStartPoint[1]);
+
+    function drawSin() {
+      ctx.beginPath();
+      ctx.save();
+
+      const sinStack = [];
+      for (let i = xOffset; i <= xOffset + axisLength; i += 20 / axisLength) {
+        const x = sp + ((xOffset + i) / unit);
+        const y = Math.sin(x) * currRange;
+        const dx = i;
+        const dy = ((2 * cR * (1 - currData)) + (radius - cR)) - (unit * y);
+
+        ctx.lineTo(dx, dy);
+        sinStack.push([dx, dy]);
+      }
+
+      const startPoint = sinStack.shift();
+
+      ctx.lineTo(xOffset + axisLength, canvasHeight);
+      ctx.lineTo(xOffset, canvasHeight);
+      ctx.lineTo(startPoint[0], startPoint[1]);
+
+      const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeight);
+      gradient.addColorStop(0, '#ffffff');
+      gradient.addColorStop(1, '#1890FF');
+      ctx.fillStyle = gradient;
+      ctx.fill();
+      ctx.restore();
+    }
+
+    function render() {
+      ctx.clearRect(0, 0, canvasWidth, canvasHeight);
+      if (circleLock) {
+        if (arcStack.length) {
+          const temp = arcStack.shift();
+          ctx.lineTo(temp[0], temp[1]);
+          ctx.stroke();
+        } else {
+          circleLock = false;
+          ctx.lineTo(cStartPoint[0], cStartPoint[1]);
+          ctx.stroke();
+          arcStack = null;
+
+          ctx.globalCompositeOperation = 'destination-over';
+          ctx.beginPath();
+          ctx.lineWidth = lineWidth;
+          ctx.arc(radius, radius, bR, 0, 2 * Math.PI, 1);
+
+          ctx.beginPath();
+          ctx.save();
+          ctx.arc(radius, radius, radius - (3 * lineWidth), 0, 2 * Math.PI, 1);
+
+          ctx.restore();
+          ctx.clip();
+          ctx.fillStyle = '#1890FF';
+        }
+      } else {
+        if (data >= 0.85) {
+          if (currRange > range / 4) {
+            const t = range * 0.01;
+            currRange -= t;
+          }
+        } else if (data <= 0.1) {
+          if (currRange < range * 1.5) {
+            const t = range * 0.01;
+            currRange += t;
+          }
+        } else {
+          if (currRange <= range) {
+            const t = range * 0.01;
+            currRange += t;
+          }
+          if (currRange >= range) {
+            const t = range * 0.01;
+            currRange -= t;
+          }
+        }
+        if ((data - currData) > 0) {
+          currData += waveupsp;
+        }
+        if ((data - currData) < 0) {
+          currData -= waveupsp;
+        }
+
+        sp += 0.07;
+        drawSin();
+      }
+      self.timer = requestAnimationFrame(render);
+    }
+
+    render();
+  }
+
+  render() {
+    const { radio } = this.state;
+    const { percent, title, height } = this.props;
+    return (
+      <div className={styles.waterWave} ref={n => (this.root = n)} style={{ transform: `scale(${radio})` }}>
+        <div style={{ width: height, height, overflow: 'hidden' }}>
+          <canvas
+            className={styles.waterWaveCanvasWrapper}
+            ref={n => (this.node = n)}
+            width={height * 2}
+            height={height * 2}
+          />
+        </div>
+        <div className={styles.text} style={{ width: height }}>
+          {
+            title && <span>{title}</span>
+          }
+          <h4>{percent}%</h4>
+        </div>
+      </div>
+    );
+  }
+}
+
+export default WaterWave;

+ 28 - 0
src/components/Charts/WaterWave/index.less

@@ -0,0 +1,28 @@
+@import "~antd/lib/style/themes/default.less";
+
+.waterWave {
+  display: inline-block;
+  position: relative;
+  transform-origin: left;
+  .text {
+    position: absolute;
+    left: 0;
+    top: 32px;
+    text-align: center;
+    width: 100%;
+    span {
+      color: @text-color-secondary;
+      font-size: 14px;
+      line-height: 22px;
+    }
+    h4 {
+      color: @heading-color;
+      line-height: 32px;
+      font-size: 24px;
+    }
+  }
+  .waterWaveCanvasWrapper {
+    transform: scale(.5);
+    transform-origin: 0 0;
+  }
+}

+ 26 - 0
src/components/Charts/demo/bar.md

@@ -0,0 +1,26 @@
+---
+order: 4
+title: 柱状图
+---
+
+通过设置 `x`,`y` 属性,可以快速的构建出一个漂亮的柱状图,各种纬度的关系则是通过自定义的数据展现。
+
+````jsx
+import { Bar } from 'ant-design-pro/lib/Charts';
+
+const salesData = [];
+for (let i = 0; i < 12; i += 1) {
+  salesData.push({
+    x: `${i + 1}月`,
+    y: Math.floor(Math.random() * 1000) + 200,
+  });
+}
+
+ReactDOM.render(
+  <Bar
+    height={200}
+    title="销售额趋势"
+    data={salesData}
+  />
+, mountNode);
+````

+ 65 - 0
src/components/Charts/demo/chart-card.md

@@ -0,0 +1,65 @@
+---
+order: 1
+title: 图表卡片
+---
+
+用于展示图表的卡片容器,可以方便的配合其它图表套件展示丰富信息。
+
+````jsx
+import { ChartCard, yuan, Field } from 'ant-design-pro/lib/Charts';
+import Trend from 'ant-design-pro/lib/Trend';
+import { Row, Col, Icon, Tooltip } from 'antd';
+import numeral from 'numeral';
+
+ReactDOM.render(
+  <Row>
+    <Col span={24}>
+      <ChartCard
+        title="销售额"
+        action={<Tooltip title="指标说明"><Icon type="info-circle-o" /></Tooltip>}
+        total={yuan(126560)}
+        footer={<Field label="日均销售额" value={numeral(12423).format('0,0')} />}
+        contentHeight={46}
+      >
+        <span>
+          周同比
+          <Trend flag="up" style={{ marginLeft: 8, color: 'rgba(0,0,0,.85)' }}>12%</Trend>
+        </span>
+        <span style={{ marginLeft: 16 }}>
+          日环比
+          <Trend flag="down" style={{ marginLeft: 8, color: 'rgba(0,0,0,.85)' }}>11%</Trend>
+        </span>
+      </ChartCard>
+    </Col>
+    <Col span={24} style={{ marginTop: 24 }}>
+      <ChartCard
+        title="移动指标"
+        avatar={
+          <img
+            style={{ width: 56, height: 56 }}
+            src="https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png"
+            alt="indicator"
+          />
+        }
+        action={<Tooltip title="指标说明"><Icon type="info-circle-o" /></Tooltip>}
+        total={yuan(126560)}
+        footer={<Field label="日均销售额" value={numeral(12423).format('0,0')} />}
+      />
+    </Col>
+    <Col span={24} style={{ marginTop: 24 }}>
+      <ChartCard
+        title="移动指标"
+        avatar={(
+          <img
+            alt="indicator"
+            style={{ width: 56, height: 56 }}
+            src="https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png"
+          />
+        )}
+        action={<Tooltip title="指标说明"><Icon type="info-circle-o" /></Tooltip>}
+        total={yuan(126560)}
+      />
+    </Col>
+  </Row>
+, mountNode);
+````

+ 18 - 0
src/components/Charts/demo/gauge.md

@@ -0,0 +1,18 @@
+---
+order: 7
+title: 仪表盘 
+---
+
+仪表盘是一种进度展示方式,可以更直观的展示当前的进展情况,通常也可表示占比。
+
+````jsx
+import { Gauge } from 'ant-design-pro/lib/Charts';
+
+ReactDOM.render(
+  <Gauge
+    title="核销率"
+    height={164}
+    percent={87}
+  />
+, mountNode);
+````

+ 28 - 0
src/components/Charts/demo/mini-area.md

@@ -0,0 +1,28 @@
+---
+order: 2
+col: 2
+title: 迷你区域图
+---
+
+````jsx
+import { MiniArea } from 'ant-design-pro/lib/Charts';
+import moment from 'moment';
+
+const visitData = [];
+const beginDay = new Date().getTime();
+for (let i = 0; i < 20; i += 1) {
+  visitData.push({
+    x: moment(new Date(beginDay + (1000 * 60 * 60 * 24 * i))).format('YYYY-MM-DD'),
+    y: Math.floor(Math.random() * 100) + 10,
+  });
+}
+
+ReactDOM.render(
+  <MiniArea
+    line
+    color="#cceafe"
+    height={45}
+    data={visitData}
+  />
+, mountNode);
+````

+ 28 - 0
src/components/Charts/demo/mini-bar.md

@@ -0,0 +1,28 @@
+---
+order: 2
+col: 2
+title: 迷你柱状图
+---
+
+迷你柱状图更适合展示简单的区间数据,简洁的表现方式可以很好的减少大数据量的视觉展现压力。
+
+````jsx
+import { MiniBar } from 'ant-design-pro/lib/Charts';
+import moment from 'moment';
+
+const visitData = [];
+const beginDay = new Date().getTime();
+for (let i = 0; i < 20; i += 1) {
+  visitData.push({
+    x: moment(new Date(beginDay + (1000 * 60 * 60 * 24 * i))).format('YYYY-MM-DD'),
+    y: Math.floor(Math.random() * 100) + 10,
+  });
+}
+
+ReactDOM.render(
+  <MiniBar
+    height={45}
+    data={visitData}
+  />
+, mountNode);
+````

+ 16 - 0
src/components/Charts/demo/mini-pie.md

@@ -0,0 +1,16 @@
+---
+order: 6
+title: 迷你饼状图
+---
+
+通过简化 `Pie` 属性的设置,可以快速的实现极简的饼状图,可配合 `ChartCard` 组合展
+现更多业务场景。
+
+```jsx
+import { Pie } from 'ant-design-pro/lib/Charts';
+
+ReactDOM.render(
+  <Pie percent={28} subTitle="中式快餐" total="28%" height={140} />,
+  mountNode
+);
+```

+ 12 - 0
src/components/Charts/demo/mini-progress.md

@@ -0,0 +1,12 @@
+---
+order: 3
+title: 迷你进度条
+---
+
+````jsx
+import { MiniProgress } from 'ant-design-pro/lib/Charts';
+
+ReactDOM.render(
+  <MiniProgress percent={78} strokeWidth={8} target={80} />
+, mountNode);
+````

+ 83 - 0
src/components/Charts/demo/mix.md

@@ -0,0 +1,83 @@
+---
+order: 0
+title: 图表套件组合展示
+---
+
+利用 Ant Design Pro 提供的图表套件,可以灵活组合符合设计规范的图表来满足复杂的业务需求。
+
+````jsx
+import { ChartCard, Field, MiniArea, MiniBar, MiniProgress } from 'ant-design-pro/lib/Charts';
+import Trend from 'ant-design-pro/lib/Trend';
+import NumberInfo from 'ant-design-pro/lib/NumberInfo';
+import { Row, Col, Icon, Tooltip } from 'antd';
+import numeral from 'numeral';
+import moment from 'moment';
+
+const visitData = [];
+const beginDay = new Date().getTime();
+for (let i = 0; i < 20; i += 1) {
+  visitData.push({
+    x: moment(new Date(beginDay + (1000 * 60 * 60 * 24 * i))).format('YYYY-MM-DD'),
+    y: Math.floor(Math.random() * 100) + 10,
+  });
+}
+
+ReactDOM.render(
+  <Row>
+    <Col span={24}>
+      <ChartCard
+        title="搜索用户数量"
+        contentHeight={134}
+      >
+        <NumberInfo
+          subTitle={<span>本周访问</span>}
+          total={numeral(12321).format('0,0')}
+          status="up"
+          subTotal={17.1}
+        />
+        <MiniArea
+          line
+          height={45}
+          data={visitData}
+        />
+      </ChartCard>
+    </Col>
+    <Col span={24} style={{ marginTop: 24 }}>
+      <ChartCard
+        title="访问量"
+        action={<Tooltip title="指标说明"><Icon type="info-circle-o" /></Tooltip>}
+        total={numeral(8846).format('0,0')}
+        footer={<Field label="日访问量" value={numeral(1234).format('0,0')} />}
+        contentHeight={46}
+      >
+        <MiniBar
+          height={46}
+          data={visitData}
+        />
+      </ChartCard>
+    </Col>
+    <Col span={24} style={{ marginTop: 24 }}>
+      <ChartCard
+        title="线上购物转化率"
+        action={<Tooltip title="指标说明"><Icon type="info-circle-o" /></Tooltip>}
+        total="78%"
+        footer={
+          <div>
+            <span>
+              周同比
+              <Trend flag="up" style={{ marginLeft: 8, color: 'rgba(0,0,0,.85)' }}>12%</Trend>
+            </span>
+            <span style={{ marginLeft: 16 }}>
+              日环比
+              <Trend flag="down" style={{ marginLeft: 8, color: 'rgba(0,0,0,.85)' }}>11%</Trend>
+            </span>
+          </div>
+        }
+        contentHeight={46}
+      >
+        <MiniProgress percent={78} strokeWidth={8} target={80} />
+      </ChartCard>
+    </Col>
+  </Row>
+, mountNode);
+````

+ 47 - 0
src/components/Charts/demo/pie.md

@@ -0,0 +1,47 @@
+---
+order: 5
+title: 饼状图
+---
+
+````jsx
+import { Pie, yuan } from 'ant-design-pro/lib/Charts';
+
+const salesPieData = [
+  {
+    x: '家用电器',
+    y: 4544,
+  },
+  {
+    x: '食用酒水',
+    y: 3321,
+  },
+  {
+    x: '个护健康',
+    y: 3113,
+  },
+  {
+    x: '服饰箱包',
+    y: 2341,
+  },
+  {
+    x: '母婴产品',
+    y: 1231,
+  },
+  {
+    x: '其他',
+    y: 1231,
+  },
+];
+
+ReactDOM.render(
+  <Pie
+    hasLegend
+    title="销售额"
+    subTitle="销售额"
+    total={yuan(salesPieData.reduce((pre, now) => now.y + pre, 0))}
+    data={salesPieData}
+    valueFormat={val => yuan(val)}
+    height={294}
+  />
+, mountNode);
+````

+ 64 - 0
src/components/Charts/demo/radar.md

@@ -0,0 +1,64 @@
+---
+order: 7
+title: 雷达图
+---
+
+````jsx
+import { Radar, ChartCard } from 'ant-design-pro/lib/Charts';
+
+const radarOriginData = [
+  {
+    name: '个人',
+    ref: 10,
+    koubei: 8,
+    output: 4,
+    contribute: 5,
+    hot: 7,
+  },
+  {
+    name: '团队',
+    ref: 3,
+    koubei: 9,
+    output: 6,
+    contribute: 3,
+    hot: 1,
+  },
+  {
+    name: '部门',
+    ref: 4,
+    koubei: 1,
+    output: 6,
+    contribute: 5,
+    hot: 7,
+  },
+];
+const radarData = [];
+const radarTitleMap = {
+  ref: '引用',
+  koubei: '口碑',
+  output: '产量',
+  contribute: '贡献',
+  hot: '热度',
+};
+radarOriginData.forEach((item) => {
+  Object.keys(item).forEach((key) => {
+    if (key !== 'name') {
+      radarData.push({
+        name: item.name,
+        label: radarTitleMap[key],
+        value: item[key],
+      });
+    }
+  });
+});
+
+ReactDOM.render(
+  <ChartCard title="数据比例">
+    <Radar
+      hasLegend
+      height={286}
+      data={radarData}
+    />
+  </ChartCard>
+, mountNode);
+````

+ 25 - 0
src/components/Charts/demo/tag-cloud.md

@@ -0,0 +1,25 @@
+---
+order: 9
+title: 标签云
+---
+
+标签云是一套相关的标签以及与此相应的权重展示方式,一般典型的标签云有 30 至 150 个标签,而权重影响使用的字体大小或其他视觉效果。
+
+````jsx
+import { TagCloud } from 'ant-design-pro/lib/Charts';
+
+const tags = [];
+for (let i = 0; i < 50; i += 1) {
+  tags.push({
+    name: `TagClout-Title-${i}`,
+    value: Math.floor((Math.random() * 50)) + 20,
+  });
+}
+
+ReactDOM.render(
+  <TagCloud
+    data={tags}
+    height={200}
+  />
+, mountNode);
+````

+ 27 - 0
src/components/Charts/demo/timeline-chart.md

@@ -0,0 +1,27 @@
+---
+order: 9
+title: 带有时间轴的图表
+---
+
+使用 `TimelineChart` 组件可以实现带有时间轴的柱状图展现,而其中的 `x` 属性,则是时间值的指向,默认最多支持同时展现两个指标,分别是 `y1` 和 `y2`。
+
+````jsx
+import { TimelineChart } from 'ant-design-pro/lib/Charts';
+
+const chartData = [];
+for (let i = 0; i < 20; i += 1) {
+  chartData.push({
+    x: (new Date().getTime()) + (1000 * 60 * 30 * i),
+    y1: Math.floor(Math.random() * 100) + 1000,
+    y2: Math.floor(Math.random() * 100) + 10,
+  });
+}
+
+ReactDOM.render(
+  <TimelineChart
+    height={200}
+    data={chartData}
+    titleMap={{ y1: '客流量', y2: '支付笔数' }}
+  />
+, mountNode);
+````

+ 20 - 0
src/components/Charts/demo/waterwave.md

@@ -0,0 +1,20 @@
+---
+order: 8
+title: 水波图 
+---
+
+水波图是一种比例的展示方式,可以更直观的展示关键值的占比。
+
+````jsx
+import { WaterWave } from 'ant-design-pro/lib/Charts';
+
+ReactDOM.render(
+  <div style={{ textAlign: 'center' }}>
+    <WaterWave
+      height={161}
+      title="补贴资金剩余"
+      percent={34}
+    />
+  </div>
+, mountNode);
+````

+ 17 - 0
src/components/Charts/equal.js

@@ -0,0 +1,17 @@
+/* eslint eqeqeq: 0 */
+
+function equal(old, target) {
+  let r = true;
+  for (const prop in old) {
+    if (typeof old[prop] === 'function' && typeof target[prop] === 'function') {
+      if (old[prop].toString() != target[prop].toString()) {
+        r = false;
+      }
+    } else if (old[prop] != target[prop]) {
+      r = false;
+    }
+  }
+  return r;
+}
+
+export default equal;

+ 17 - 0
src/components/Charts/index.d.ts

@@ -0,0 +1,17 @@
+export { default as numeral } from "numeral";
+export { default as ChartCard } from "./ChartCard";
+export { default as Bar } from "./Bar";
+export { default as Pie } from "./Pie";
+export { default as Radar } from "./Radar";
+export { default as Gauge } from "./Gauge";
+export { default as MiniArea } from "./MiniArea";
+export { default as MiniBar } from "./MiniBar";
+export { default as MiniProgress } from "./MiniProgress";
+export { default as Field } from "./Field";
+export { default as WaterWave } from "./WaterWave";
+export { default as TagCloud } from "./TagCloud";
+export { default as TimelineChart } from "./TimelineChart";
+
+declare const yuan: (value: number | string) => string;
+
+export { yuan };

+ 31 - 0
src/components/Charts/index.js

@@ -0,0 +1,31 @@
+import numeral from 'numeral';
+import ChartCard from './ChartCard';
+import Bar from './Bar';
+import Pie from './Pie';
+import Radar from './Radar';
+import Gauge from './Gauge';
+import MiniArea from './MiniArea';
+import MiniBar from './MiniBar';
+import MiniProgress from './MiniProgress';
+import Field from './Field';
+import WaterWave from './WaterWave';
+import TagCloud from './TagCloud';
+import TimelineChart from './TimelineChart';
+
+const yuan = val => `&yen; ${numeral(val).format('0,0')}`;
+
+export {
+  yuan,
+  Bar,
+  Pie,
+  Gauge,
+  Radar,
+  MiniBar,
+  MiniArea,
+  MiniProgress,
+  ChartCard,
+  Field,
+  WaterWave,
+  TagCloud,
+  TimelineChart,
+};

+ 19 - 0
src/components/Charts/index.less

@@ -0,0 +1,19 @@
+.miniChart {
+  position: relative;
+  width: 100%;
+  .chartContent {
+    position: absolute;
+    bottom: -34px;
+    width: 100%;
+    & > div {
+      margin: 0 -5px;
+      overflow: hidden;
+    }
+  }
+  .chartLoading {
+    position: absolute;
+    top: 16px;
+    left: 50%;
+    margin-left: -7px;
+  }
+}

+ 132 - 0
src/components/Charts/index.md

@@ -0,0 +1,132 @@
+---
+title: 
+  en-US: Charts
+  zh-CN: Charts
+subtitle: 图表
+order: 2
+cols: 2
+---
+
+Ant Design Pro 提供的业务中常用的图表类型,都是基于 [G2](https://antv.alipay.com/g2/doc/index.html) 按照 Ant Design 图表规范封装,需要注意的是 Ant Design Pro 的图表组件以套件形式提供,可以任意组合实现复杂的业务需求。
+
+因为结合了 Ant Design 的标准设计,本着极简的设计思想以及开箱即用的理念,简化了大量 API 配置,所以如果需要灵活定制图表,可以参考 Ant Design Pro 图表实现,自行基于 [G2](https://antv.alipay.com/g2/doc/index.html) 封装图表组件使用。
+
+## API
+
+### ChartCard
+
+| 参数      | 说明                                      | 类型         | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| title | 卡片标题 | ReactNode\|string | - |
+| action | 卡片操作 | ReactNode | - |
+| total | 数据总量 | ReactNode \| number | - |
+| footer | 卡片底部 | ReactNode | - |
+| contentHeight | 内容区域高度 | number | - |
+| avatar | 右侧图标 | React.ReactNode | - |
+### MiniBar
+
+| 参数      | 说明                                      | 类型         | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| color | 图表颜色 | string | `#1890FF` |
+| height | 图表高度 | number | - |
+| data | 数据 | array<{x, y}> | - |
+
+### MiniArea
+
+| 参数      | 说明                                      | 类型         | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| color | 图表颜色 | string | `rgba(24, 144, 255, 0.2)` |
+| borderColor | 图表边颜色 | string | `#1890FF` |
+| height | 图表高度 | number | - |
+| line | 是否显示描边 | boolean | false |
+| animate | 是否显示动画 | boolean | true |
+| xAxis | [x 轴配置](http://antvis.github.io/g2/doc/tutorial/start/axis.html) | object | - |
+| yAxis | [y 轴配置](http://antvis.github.io/g2/doc/tutorial/start/axis.html) | object | - |
+| data | 数据 | array<{x, y}> | - |
+
+### MiniProgress
+
+| 参数      | 说明                                      | 类型         | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| target | 目标比例 | number | - |
+| color | 进度条颜色 | string | - |
+| strokeWidth | 进度条高度 | number | - |
+| percent | 进度比例 | number | - |
+
+### Bar
+
+| 参数      | 说明                                      | 类型         | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| title | 图表标题 | ReactNode\|string | - |
+| color | 图表颜色 | string | `rgba(24, 144, 255, 0.85)` |
+| margin | 图表内部间距 | array | \[32, 0, 32, 40\] |
+| height | 图表高度 | number | - |
+| data | 数据 | array<{x, y}> | - |
+| autoLabel | 在宽度不足时,自动隐藏 x 轴的 label | boolean | `true` |
+
+### Pie
+
+| 参数      | 说明                                      | 类型         | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| animate | 是否显示动画 | boolean | true |
+| color | 图表颜色 | string | `rgba(24, 144, 255, 0.85)` |
+| height | 图表高度 | number | - |
+| hasLegend | 是否显示 legend | boolean | `false` |
+| margin | 图表内部间距 | array | \[24, 0, 24, 0\] |
+| percent | 占比 | number | - |
+| tooltip | 是否显示 tooltip | boolean | true |
+| valueFormat | 显示值的格式化函数 | function | - |
+| title | 图表标题 | ReactNode|string | - |
+| subTitle | 图表子标题 | ReactNode|string | - |
+| total | 图标中央的总数 | string | - |
+
+### Radar
+
+| 参数      | 说明                                      | 类型         | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| title | 图表标题 | ReactNode\|string | - |
+| height | 图表高度 | number | - |
+| hasLegend | 是否显示 legend | boolean | `false` |
+| margin | 图表内部间距 | array | \[24, 30, 16, 30\] |
+| data | 图标数据 | array<{name,label,value}> | - |
+
+### Gauge
+
+| 参数      | 说明                                      | 类型         | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| title | 图表标题 | ReactNode\|string | - |
+| height | 图表高度 | number | - |
+| color | 图表颜色 | string | `#2F9CFF` |
+| bgColor | 图表背景颜色 | string | `#F0F2F5` |
+| percent | 进度比例 | number | - |
+
+### WaterWave
+
+| 参数      | 说明                                      | 类型         | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| title | 图表标题 | ReactNode\|string | - |
+| height | 图表高度 | number | - |
+| color | 图表颜色 | string | `#1890FF` |
+| percent | 进度比例 | number | - |
+
+### TagCloud
+
+| 参数      | 说明                                      | 类型         | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| data | 标题 | Array<name, value\> | - |
+| height | 高度值 | number | - |
+
+### TimelineChart
+
+| 参数      | 说明                                      | 类型         | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| data | 标题 | Array<x, y1, y2\> | - |
+| titleMap | 指标别名 | Object{y1: '客流量', y2: '支付笔数'} | - |
+| height | 高度值 | number | 400 |
+
+### Field
+
+| 参数      | 说明                                      | 类型         | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| label | 标题 | ReactNode\|string | - |
+| value | 值 | ReactNode\|string | - |

+ 127 - 0
src/components/DataSearch/index.js

@@ -0,0 +1,127 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { Input, Select, Button, Icon } from 'antd';
+import styles from './index.less';
+
+const Option = Select.Option;
+
+export default class DataSearch extends PureComponent {
+  static propTypes = {
+    size: PropTypes.string,
+    select: PropTypes.bool,
+    selectProps: PropTypes.object,
+    onSearch: PropTypes.func,
+    selectOptions: PropTypes.array,
+    style: PropTypes.object,
+    keyword: PropTypes.string,
+    filterSelectProps: PropTypes.object,
+  }
+  constructor(props) {
+    super(props);
+    const { select, selectOptions, selectProps, keyword, field } = this.props;
+    this.state = {
+      selectValue: select && selectProps ? selectProps.defaultValue : '',
+      inputValue: keyword ? keyword : '',
+      mode: field && selectOptions.filter(item => item.value === field)[0].mode || 'input',
+    }
+  }
+  handleSearch = () => {
+    const query = { keyword: this.state.inputValue };
+    if (this.props.select) {
+      query.field = this.state.selectValue;
+    }
+    this.props.onSearch && this.props.onSearch(query);
+  }
+  handleSelectChange = (value) => {
+    // 进行模式匹配
+    const { selectOptions } = this.props;
+    const match = selectOptions.filter(item => item.value === value)[0];
+    this.setState({
+      ...this.state,
+      selectValue: value,
+      inputValue: '',
+      mode: match.mode,
+    });
+  }
+  handleInputChange = (e) => {
+    this.setState({
+      ...this.state,
+      inputValue: e.target.value,
+    });
+  }
+  handleInputSelectChange = (value) => {
+    this.setState({
+      ...this.state,
+      inputValue: value,
+    })
+  }
+  handleInputSelectClear = (value) => {
+    if (!value) {
+      this.setState({
+        inputValue: ''
+      }, () => this.handleSearch());
+    }
+  }
+  handleClearInput = () => {
+    this.setState({
+      inputValue: '',
+    }, () => this.handleSearch());
+  }
+  renderKeyWordComponent = () => {
+    const { mode, inputValue } = this.state;
+    const { size, filterSelectProps = {} } = this.props;
+    const { data = [] } = filterSelectProps;
+    const suffix = inputValue ? <Icon type="close-circle" onClick={this.handleClearInput} /> : null;
+    const input = (
+      <Input
+        placeholder="请输入"
+        suffix={suffix}
+        onChange={this.handleInputChange}
+        onPressEnter={this.handleSearch}
+        size={size}
+        value={inputValue}
+      />
+    );
+    const select = (
+      <Select
+        allowClear
+        showSearch
+        size={size}
+        value={inputValue}
+        placeholder="请选择/请输入进行筛选"
+        optionFilterProp="children"
+        style={{ flexShrink: 1, flexGrow: 1 }}
+        onChange={this.handleInputSelectClear}
+        onSelect={this.handleInputSelectChange}
+        filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
+      >
+        {data.map(item => <Option key={item.value} value={item.value}>{item.name}</Option>)}
+      </Select>
+    );
+
+    switch (mode) {
+      case 'input':
+        return input;
+        break;
+      case 'select':
+        return select;
+        break;
+      default:
+        return input;
+        break;
+    }
+  }
+
+  render() {
+    const { size, select, selectOptions, selectProps, style } = this.props;
+    return (
+      <Input.Group compact size={size} className={styles.search} style={style}>
+        {select && <Select onChange={this.handleSelectChange} size={size} {...selectProps}>
+          {selectOptions && selectOptions.map((item, key) => <Option value={item.value} key={key}>{item.name || item.value}</Option>)}
+        </Select>}
+        {this.renderKeyWordComponent()}
+        <Button onClick={this.handleSearch} size={size} type="primary" icon="search">搜索</Button>
+      </Input.Group>
+    );
+  }
+}

+ 55 - 0
src/components/DataSearch/index.less

@@ -0,0 +1,55 @@
+.no-highlight() {
+  border-color: #e5e5e5;
+  box-shadow: none;
+}
+
+.search {
+  display: flex !important;
+  width: 100%;
+  position: relative;
+
+  :global {
+    .anticon-cross {
+      position: absolute;
+      width: 18px;
+      height: 18px;
+      right: 94px;
+      cursor: pointer;
+      color: #fff;
+      line-height: 18px;
+      border-radius: 50%!important;
+      background-color: rgba(0, 0, 0, .16);
+      top: 7px;
+      // opacity:
+    }
+
+    .ant-select {
+      width: 120px;
+      flex-shrink: 0;
+      flex-grow: 0;
+
+      &.ant-select-focused .ant-select-selection,
+      &.ant-select-open .ant-select-selection,
+      .ant-select-selection:active,
+      .ant-select-selection:focus,
+      .ant-select-selection:hover {
+        .no-highlight();
+      }
+    }
+
+    .ant-input {
+      &:focus,
+      &:hover {
+        .no-highlight();
+      }
+      flex-shrink: 1;
+      flex-grow: 1;
+    }
+
+    .ant-btn {
+      width: 80px;
+      flex-shrink: 0;
+      flex-grow: 0;
+    }
+  }
+}

+ 17 - 0
src/components/DescriptionList/Description.js

@@ -0,0 +1,17 @@
+import React from 'react';
+import classNames from 'classnames';
+import { Col } from 'antd';
+import styles from './index.less';
+import responsive from './responsive';
+
+const Description = ({ term, column, className, children, ...restProps }) => {
+  const clsString = classNames(styles.description, className);
+  return (
+    <Col className={clsString} {...responsive[column]} {...restProps}>
+      {term && <div className={styles.term}>{term}</div>}
+      {children && <div className={styles.detail}>{children}</div>}
+    </Col>
+  );
+};
+
+export default Description;

+ 21 - 0
src/components/DescriptionList/DescriptionList.js

@@ -0,0 +1,21 @@
+import React from 'react';
+import classNames from 'classnames';
+import { Row } from 'antd';
+import styles from './index.less';
+
+export default ({ className, title, col = 3, layout = 'horizontal', gutter = 32,
+  children, size, ...restProps }) => {
+  const clsString = classNames(styles.descriptionList, styles[layout], className, {
+    [styles.descriptionListSmall]: size === 'small',
+    [styles.descriptionListLarge]: size === 'large',
+  });
+  const column = col > 4 ? 4 : col;
+  return (
+    <div className={clsString} {...restProps}>
+      {title ? <div className={styles.title}>{title}</div> : null}
+      <Row gutter={gutter}>
+        {React.Children.map(children, child => React.cloneElement(child, { column }))}
+      </Row>
+    </div>
+  );
+};

+ 35 - 0
src/components/DescriptionList/demo/basic.md

@@ -0,0 +1,35 @@
+---
+order: 0
+title: Basic
+---
+
+基本描述列表。
+
+````jsx
+import DescriptionList from 'ant-design-pro/lib/DescriptionList';
+
+const { Description } = DescriptionList;
+
+ReactDOM.render(
+  <DescriptionList size="large" title="title">
+    <Description term="Firefox">
+      A free, open source, cross-platform,
+      graphical web browser developed by the
+      Mozilla Corporation and hundreds of
+      volunteers.
+    </Description>
+    <Description term="Firefox">
+      A free, open source, cross-platform,
+      graphical web browser developed by the
+      Mozilla Corporation and hundreds of
+      volunteers.
+    </Description>
+    <Description term="Firefox">
+      A free, open source, cross-platform,
+      graphical web browser developed by the
+      Mozilla Corporation and hundreds of
+      volunteers.
+    </Description>
+  </DescriptionList>
+, mountNode);
+````

+ 35 - 0
src/components/DescriptionList/demo/vertical.md

@@ -0,0 +1,35 @@
+---
+order: 1
+title: Vertical
+---
+
+垂直布局。
+
+````jsx
+import DescriptionList from 'ant-design-pro/lib/DescriptionList';
+
+const { Description } = DescriptionList;
+
+ReactDOM.render(
+  <DescriptionList size="large" title="title" layout="vertical">
+    <Description term="Firefox">
+      A free, open source, cross-platform,
+      graphical web browser developed by the
+      Mozilla Corporation and hundreds of
+      volunteers.
+    </Description>
+    <Description term="Firefox">
+      A free, open source, cross-platform,
+      graphical web browser developed by the
+      Mozilla Corporation and hundreds of
+      volunteers.
+    </Description>
+    <Description term="Firefox">
+      A free, open source, cross-platform,
+      graphical web browser developed by the
+      Mozilla Corporation and hundreds of
+      volunteers.
+    </Description>
+  </DescriptionList>
+, mountNode);
+````

+ 22 - 0
src/components/DescriptionList/index.d.ts

@@ -0,0 +1,22 @@
+import * as React from "react";
+export interface DescriptionListProps {
+  layout?: "horizontal" | "vertical";
+  col?: number;
+  title: React.ReactNode;
+  gutter?: number;
+  size?: "large" | "small";
+}
+
+declare class Description extends React.Component<
+  {
+    term: React.ReactNode;
+  },
+  any
+> {}
+
+export default class DescriptionList extends React.Component<
+  DescriptionListProps,
+  any
+> {
+  static Description: typeof Description;
+}

+ 5 - 0
src/components/DescriptionList/index.js

@@ -0,0 +1,5 @@
+import DescriptionList from './DescriptionList';
+import Description from './Description';
+
+DescriptionList.Description = Description;
+export default DescriptionList;

+ 75 - 0
src/components/DescriptionList/index.less

@@ -0,0 +1,75 @@
+@import "~antd/lib/style/themes/default.less";
+
+.descriptionList {
+  // offset the padding-bottom of last row
+  :global {
+    .ant-row {
+      margin-bottom: -16px;
+      overflow: hidden;
+    }
+  }
+
+  .title {
+    font-size: 14px;
+    color: @heading-color;
+    font-weight: 500;
+    margin-bottom: 16px;
+  }
+
+  .term {
+    line-height: 22px;
+    padding-bottom: 16px;
+    margin-right: 8px;
+    color: @heading-color;
+    white-space: nowrap;
+    display: table-cell;
+
+    &:after {
+      content: ":";
+      margin: 0 8px 0 2px;
+      position: relative;
+      top: -.5px;
+    }
+  }
+
+  .detail {
+    line-height: 22px;
+    width: 100%;
+    padding-bottom: 16px;
+    color: @text-color;
+    display: table-cell;
+  }
+
+  &.vertical {
+
+    .term {
+      padding-bottom: 8px;
+      display: block;
+    }
+
+    .detail {
+      display: block;
+    }
+  }
+}
+
+.descriptionListSmall {
+  // offset the padding-bottom of last row
+  :global {
+    .ant-row {
+      margin-bottom: -8px;
+    }
+  }
+  .title {
+    margin-bottom: 12px;
+    color: @text-color;
+  }
+  .term, .detail {
+    padding-bottom: 8px;
+  }
+}
+.descriptionListLarge {
+  .title {
+    font-size: 16px;
+  }
+}

+ 39 - 0
src/components/DescriptionList/index.md

@@ -0,0 +1,39 @@
+---
+title:
+  en-US: DescriptionList
+  zh-CN: DescriptionList
+subtitle: 描述列表
+cols: 1
+order: 4
+---
+
+成组展示多个只读字段,常见于详情页的信息展示。
+
+## API
+
+### DescriptionList
+
+| 参数      | 说明                                      | 类型         | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| layout    | 布局方式                                 | Enum{'horizontal', 'vertical'}  | 'horizontal' |
+| col       | 指定信息最多分几列展示,最终一行几列由 col 配置结合[响应式规则](/components/DescriptionList#响应式规则)决定          | number(0 < col <= 4)  | 3 |
+| title     | 列表标题                                 | ReactNode  | - |
+| gutter    | 列表项间距,单位为 `px`                    | number  | 32 |
+| size     | 列表型号,可以设置为 `large` `small`        | Enum{'large', 'small'}  | - |
+
+#### 响应式规则
+
+| 窗口宽度             | 展示列数                                      | 
+|---------------------|---------------------------------------------|
+| `≥768px`           |  `col`                                       |
+| `≥576px`           |  `col < 2 ? col : 2`                         |
+| `<576px`           |  `1`                                         |
+
+### DescriptionList.Description
+
+| 参数      | 说明                                      | 类型         | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| term     | 列表项标题                                 | ReactNode  | - |
+
+
+

+ 6 - 0
src/components/DescriptionList/responsive.js

@@ -0,0 +1,6 @@
+export default {
+  1: { xs: 24 },
+  2: { xs: 24, sm: 12 },
+  3: { xs: 24, sm: 12, md: 8 },
+  4: { xs: 24, sm: 12, md: 6 },
+};

+ 25 - 0
src/components/DropOption/index.js

@@ -0,0 +1,25 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { Dropdown, Button, Icon, Menu } from 'antd'
+
+const DropOption = ({ onMenuClick, menuOptions = [], buttonStyle, dropdownProps }) => {
+  const menu = menuOptions.map(item => <Menu.Item key={item.key}>{item.name}</Menu.Item>)
+  return (<Dropdown
+    overlay={<Menu onClick={onMenuClick}>{menu}</Menu>}
+    {...dropdownProps}
+  >
+    <Button style={{ border: 'none', ...buttonStyle }}>
+      <Icon style={{ marginRight: 2 }} type="bars" />
+      <Icon type="down" />
+    </Button>
+  </Dropdown>)
+}
+
+DropOption.propTypes = {
+  onMenuClick: PropTypes.func,
+  menuOptions: PropTypes.array.isRequired,
+  buttonStyle: PropTypes.object,
+  dropdownProps: PropTypes.object,
+}
+
+export default DropOption;

+ 21 - 0
src/components/Exception/demo/403.md

@@ -0,0 +1,21 @@
+---
+order: 2
+title: 403
+---
+
+403 页面,配合自定义操作。
+
+````jsx
+import Exception from 'ant-design-pro/lib/Exception';
+import { Button } from 'antd';
+
+const actions = (
+  <div>
+    <Button type="primary">回到首页</Button>
+    <Button>查看详情</Button>
+  </div>
+);
+ReactDOM.render(
+  <Exception type="403" actions={actions} />
+, mountNode);
+````

+ 14 - 0
src/components/Exception/demo/404.md

@@ -0,0 +1,14 @@
+---
+order: 0
+title: 404
+---
+
+404 页面。
+
+````jsx
+import Exception from 'ant-design-pro/lib/Exception';
+
+ReactDOM.render(
+  <Exception type="404" />
+, mountNode);
+````

+ 14 - 0
src/components/Exception/demo/500.md

@@ -0,0 +1,14 @@
+---
+order: 1
+title: 500
+---
+
+500 页面。
+
+````jsx
+import Exception from 'ant-design-pro/lib/Exception';
+
+ReactDOM.render(
+  <Exception type="500" />
+, mountNode);
+````

+ 33 - 0
src/components/Exception/index.js

@@ -0,0 +1,33 @@
+import React, { createElement } from 'react';
+import classNames from 'classnames';
+import { Button } from 'antd';
+import config from './typeConfig';
+import styles from './index.less';
+
+export default ({ className, linkElement = 'a', type, title, desc, img, actions, ...rest }) => {
+  const pageType = type in config ? type : '404';
+  const clsString = classNames(styles.exception, className);
+  return (
+    <div className={clsString} {...rest}>
+      <div className={styles.imgBlock}>
+        <div
+          className={styles.imgEle}
+          style={{ backgroundImage: `url(${img || config[pageType].img})` }}
+        />
+      </div>
+      <div className={styles.content}>
+        <h1>{title || config[pageType].title}</h1>
+        <div className={styles.desc}>{desc || config[pageType].desc}</div>
+        <div className={styles.actions}>
+          {
+            actions ||
+              createElement(linkElement, {
+                to: '/',
+                href: '/',
+              }, <Button type="primary">返回首页</Button>)
+          }
+        </div>
+      </div>
+    </div>
+  );
+};

+ 78 - 0
src/components/Exception/index.less

@@ -0,0 +1,78 @@
+@import "~antd/lib/style/themes/default.less";
+@import "~antd/lib/style/mixins/clearfix.less";
+
+.exception {
+  display: flex;
+  align-items: center;
+  height: 100%;
+
+  .imgBlock {
+    flex: 0 0 62.5%;
+    width: 62.5%;
+    padding-right: 152px;
+    .clearfix();
+  }
+
+  .imgEle {
+    height: 360px;
+    width: 100%;
+    max-width: 430px;
+    float: right;
+    background-repeat: no-repeat;
+    background-position: 50% 50%;
+    background-size: 100% 100%;
+  }
+
+  .content {
+    flex: auto;
+
+    h1 {
+      color: #434e59;
+      font-size: 72px;
+      font-weight: 600;
+      line-height: 72px;
+      margin-bottom: 24px;
+    }
+
+    .desc {
+      color: @text-color-secondary;
+      font-size: 20px;
+      line-height: 28px;
+      margin-bottom: 16px;
+    }
+
+    .actions {
+      button:not(:last-child) {
+        margin-right: 8px;
+      }
+    }
+  }
+}
+
+@media screen and (max-width: @screen-xl) {
+  .exception {
+    .imgBlock {
+      padding-right: 88px;
+    }
+  }
+}
+
+@media screen and (max-width: @screen-sm) {
+  .exception {
+    display: block;
+    text-align: center;
+    .imgBlock {
+      padding-right: 0;
+      margin: 0 auto 24px;
+    }
+  }
+}
+
+@media screen and (max-width: @screen-xs) {
+  .exception {
+    .imgBlock {
+      margin-bottom: -24px;
+      overflow: hidden;
+    }
+  }
+}

+ 19 - 0
src/components/Exception/index.md

@@ -0,0 +1,19 @@
+---
+title: Exception
+subtitle: 异常
+cols: 1
+order: 5
+---
+
+异常页用于对页面特定的异常状态进行反馈。通常,它包含对错误状态的阐述,并向用户提供建议或操作,避免用户感到迷失和困惑。
+
+## API
+
+| 参数         | 说明                                      | 类型         | 默认值 |
+|-------------|------------------------------------------|-------------|-------|
+| type        | 页面类型,若配置,则自带对应类型默认的 `title`,`desc`,`img`,此默认设置可以被 `title`,`desc`,`img` 覆盖 | Enum {'403', '404', '500'} | - |
+| title       | 标题     | ReactNode  | -    |
+| desc        | 补充描述    | ReactNode  | -    |
+| img         | 背景图片地址     | string  | -    |
+| actions     | 建议操作,配置此属性时默认的『返回首页』按钮不生效    | ReactNode  | -    |
+| linkElement | 定义链接的元素,默认为 `a` | string\|ReactElement | - |

+ 19 - 0
src/components/Exception/typeConfig.js

@@ -0,0 +1,19 @@
+const config = {
+  403: {
+    img: 'https://gw.alipayobjects.com/zos/rmsportal/wZcnGqRDyhPOEYFcZDnb.svg',
+    title: '403',
+    desc: '抱歉,你无权访问该页面',
+  },
+  404: {
+    img: 'https://gw.alipayobjects.com/zos/rmsportal/KpnpchXsobRgLElEozzI.svg',
+    title: '404',
+    desc: '抱歉,你访问的页面不存在',
+  },
+  500: {
+    img: 'https://gw.alipayobjects.com/zos/rmsportal/RVRUAYdCGeYNBWoKiIwB.svg',
+    title: '500',
+    desc: '抱歉,服务器出错了',
+  },
+};
+
+export default config;

+ 36 - 0
src/components/FooterToolbar/demo/basic.md

@@ -0,0 +1,36 @@
+---
+order: 0
+title: 演示
+iframe: 400
+---
+
+浮动固定页脚。
+
+````jsx
+import FooterToolbar from 'ant-design-pro/lib/FooterToolbar';
+import { Button } from 'antd';
+
+ReactDOM.render(
+  <div style={{ background: '#f7f7f7', padding: 24 }}>
+    <p>页面内容 页面内容 页面内容 页面内容</p>
+    <p>页面内容 页面内容 页面内容 页面内容</p>
+    <p>页面内容 页面内容 页面内容 页面内容</p>
+    <p>页面内容 页面内容 页面内容 页面内容</p>
+    <p>页面内容 页面内容 页面内容 页面内容</p>
+    <p>页面内容 页面内容 页面内容 页面内容</p>
+    <p>页面内容 页面内容 页面内容 页面内容</p>
+    <p>页面内容 页面内容 页面内容 页面内容</p>
+    <p>页面内容 页面内容 页面内容 页面内容</p>
+    <p>页面内容 页面内容 页面内容 页面内容</p>
+    <p>页面内容 页面内容 页面内容 页面内容</p>
+    <p>页面内容 页面内容 页面内容 页面内容</p>
+    <p>页面内容 页面内容 页面内容 页面内容</p>
+    <p>页面内容 页面内容 页面内容 页面内容</p>
+    <p>页面内容 页面内容 页面内容 页面内容</p>
+    <FooterToolbar extra="提示信息">
+      <Button>取消</Button>
+      <Button type="primary">提交</Button>
+    </FooterToolbar>
+  </div>
+, mountNode);
+````

+ 10 - 0
src/components/FooterToolbar/index.d.ts

@@ -0,0 +1,10 @@
+import * as React from 'react';
+export interface FooterToolbarProps {
+  extra: React.ReactNode;
+  style?: React.CSSProperties;
+}
+
+export default class FooterToolbar extends React.Component<
+  FooterToolbarProps,
+  any
+> {}

+ 18 - 0
src/components/FooterToolbar/index.js

@@ -0,0 +1,18 @@
+import React, { Component } from 'react';
+import classNames from 'classnames';
+import styles from './index.less';
+
+export default class FooterToolbar extends Component {
+  render() {
+    const { children, className, extra, ...restProps } = this.props;
+    return (
+      <div
+        className={classNames(className, styles.toolbar)}
+        {...restProps}
+      >
+        <div className={styles.left}>{extra}</div>
+        <div className={styles.right}>{children}</div>
+      </div>
+    );
+  }
+}

+ 33 - 0
src/components/FooterToolbar/index.less

@@ -0,0 +1,33 @@
+@import "~antd/lib/style/themes/default.less";
+
+.toolbar {
+  position: fixed;
+  width: 100%;
+  bottom: 0;
+  right: 0;
+  height: 56px;
+  line-height: 56px;
+  box-shadow: 0 -1px 2px rgba(0, 0, 0, .03);
+  background: #fff;
+  border-top: 1px solid @border-color-split;
+  padding: 0 24px;
+  z-index: 9;
+
+  &:after {
+    content: "";
+    display: block;
+    clear: both;
+  }
+
+  .left {
+    float: left;
+  }
+
+  .right {
+    float: right;
+  }
+
+  button + button {
+    margin-left: 8px;
+  }
+}

+ 21 - 0
src/components/FooterToolbar/index.md

@@ -0,0 +1,21 @@
+---
+title:
+  en-US: FooterToolbar
+  zh-CN: FooterToolbar
+subtitle: 底部工具栏
+cols: 1
+order: 6
+---
+
+固定在底部的工具栏。
+
+## 何时使用
+
+固定在内容区域的底部,不随滚动条移动,常用于长页面的数据搜集和提交工作。
+
+## API
+
+参数 | 说明 | 类型 | 默认值
+----|------|-----|------
+children | 工具栏内容,向右对齐 | ReactNode | -
+extra | 额外信息,向左对齐 | ReactNode | -

+ 33 - 0
src/components/GlobalFooter/demo/basic.md

@@ -0,0 +1,33 @@
+---
+order: 0
+title: 演示
+iframe: 400
+---
+
+基本页脚。
+
+````jsx
+import GlobalFooter from 'ant-design-pro/lib/GlobalFooter';
+import { Icon } from 'antd';
+
+const links = [{
+  title: '帮助',
+  href: '',
+}, {
+  title: '隐私',
+  href: '',
+}, {
+  title: '条款',
+  href: '',
+  blankTarget: true,
+}];
+
+const copyright = <div>Copyright <Icon type="copyright" /> 2017 蚂蚁金服体验技术部出品</div>;
+
+ReactDOM.render(
+  <div style={{ background: '#f5f5f5', overflow: 'hidden' }}>
+    <div style={{ height: 280 }} />
+    <GlobalFooter links={links} copyright={copyright} />
+  </div>
+, mountNode);
+````

+ 14 - 0
src/components/GlobalFooter/index.d.ts

@@ -0,0 +1,14 @@
+import * as React from "react";
+export interface GlobalFooterProps {
+  links: Array<{
+    title: React.ReactNode;
+    href: string;
+    blankTarget?: boolean;
+  }>;
+  copyright: React.ReactNode;
+}
+
+export default class GlobalFooter extends React.Component<
+  GlobalFooterProps,
+  any
+> {}

+ 29 - 0
src/components/GlobalFooter/index.js

@@ -0,0 +1,29 @@
+import React from 'react';
+import classNames from 'classnames';
+import styles from './index.less';
+
+export default ({ className, links, copyright, colorInverse }) => {
+  const clsString = classNames(styles.globalFooter, className);
+  const copyrightStyle = (colorInverse || false) ? styles.copyrightInverse : styles.copyright;
+  const linkStyle = (colorInverse || false) ? styles.linksInverse : styles.links;
+  return (
+    <div className={clsString}>
+      {
+        links && (
+          <div className={linkStyle}>
+            {links.map(link => (
+              <a
+                key={link.title}
+                target={link.blankTarget ? '_blank' : '_self'}
+                href={link.href}
+              >
+                {link.title}
+              </a>
+            ))}
+          </div>
+        )
+      }
+      {copyright && <div className={copyrightStyle}>{copyright}</div>}
+    </div>
+  );
+};

+ 51 - 0
src/components/GlobalFooter/index.less

@@ -0,0 +1,51 @@
+@import "~antd/lib/style/themes/default.less";
+
+.globalFooter {
+  padding: 0 16px;
+  margin: 24px 0 24px 0;
+  text-align: center;
+
+  .links {
+    margin-bottom: 8px;
+
+    a {
+      color: @text-color-secondary;
+      transition: all .3s;
+
+      &:not(:last-child) {
+        margin-right: 40px;
+      }
+
+      &:hover {
+        color: @text-color;
+      }
+    }
+  }
+
+  .linksInverse {
+    margin-bottom: 8px;
+
+    a {
+      color: #fff;
+      transition: all .3s;
+
+      &:not(:last-child) {
+        margin-right: 40px;
+      }
+
+      &:hover {
+        color: #fff;
+      }
+    }
+  }
+
+  .copyright {
+    color: @text-color-secondary;
+    font-size: @font-size-base;
+  }
+
+  .copyrightInverse {
+    color: #fff;
+    font-size: @font-size-base;
+  }
+}

+ 17 - 0
src/components/GlobalFooter/index.md

@@ -0,0 +1,17 @@
+---
+title:
+  en-US: GlobalFooter
+  zh-CN: GlobalFooter
+subtitle: 全局页脚
+cols: 1
+order: 7
+---
+
+页脚属于全局导航的一部分,作为对顶部导航的补充,通过传递数据控制展示内容。
+
+## API
+
+参数 | 说明 | 类型 | 默认值
+----|------|-----|------
+links | 链接数据 | array<{ title: ReactNode, href: string, blankTarget?: boolean }> | -
+copyright | 版权信息 | ReactNode | -

+ 170 - 0
src/components/GlobalHeader/index.js

@@ -0,0 +1,170 @@
+import React, { PureComponent } from 'react';
+import { Layout, Menu, Icon, Spin, Tag, Dropdown, Avatar, message, Divider } from 'antd';
+import moment from 'moment';
+import groupBy from 'lodash/groupBy';
+import Debounce from 'lodash-decorators/debounce';
+import { Link } from 'dva/router';
+import NoticeIcon from '../../components/NoticeIcon';
+import HeaderSearch from '../../components/HeaderSearch';
+import logo from '../../assets/logo.svg';
+import styles from './index.less';
+import { getLocalUser } from '../../utils/helper';
+
+const { Header } = Layout;
+
+export default class GlobalHeader extends PureComponent {
+  componentDidMount() {
+    /*
+    this.props.dispatch({
+      type: 'user/fetchCurrent',
+    });
+    */
+  }
+  componentWillUnmount() {
+    this.triggerResizeEvent.cancel();
+  }
+  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();
+      }
+      // transform id to item key
+      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');
+  }
+  handleNoticeClear = (type) => {
+    // this.props.dispatch({
+    //   type: 'global/clearNotices',
+    //   payload: type,
+    // });
+  }
+  handleNoticeVisibleChange = (visible) => {
+    // if (visible) {
+    //   this.props.dispatch({
+    //     type: 'global/fetchNotices',
+    //   });
+    // }
+  }
+  handleMenuClick = ({ key }) => {
+    if (key === 'logout') {
+      this.props.dispatch({
+        type: 'login/logout',
+      });
+    }
+  }
+  toggle = () => {
+    const { collapsed } = this.props;
+    this.props.dispatch({
+      type: 'global/changeLayoutCollapsed',
+      payload: !collapsed,
+    });
+    this.triggerResizeEvent();
+  }
+  @Debounce(600)
+  triggerResizeEvent() { // eslint-disable-line
+    const event = document.createEvent('HTMLEvents');
+    event.initEvent('resize', true, false);
+    window.dispatchEvent(event);
+  }
+  render() {
+    const {
+      collapsed, fetchingNotices, isMobile,
+    } = this.props;
+    const currentUser = getLocalUser() || {};
+    const menu = (
+      <Menu className={styles.menu} selectedKeys={[]} onClick={this.handleMenuClick}>
+        <Menu.Item disabled><Icon type="user" />个人中心</Menu.Item>
+        <Menu.Item disabled><Icon type="setting" />账号设置</Menu.Item>
+        <Menu.Divider />
+        <Menu.Item key="logout"><Icon type="logout" />退出登录</Menu.Item>
+      </Menu>
+    );
+    const noticeData = this.getNoticeData();
+    return (
+      <Header className={styles.header}>
+        {isMobile && (
+          [(
+            <Link to="/" className={styles.logo} key="logo">
+              <img src={logo} alt="logo" width="32" />
+            </Link>),
+            <Divider type="vertical" key="line" />,
+          ]
+        )}
+        <Icon
+          className={styles.trigger}
+          type={collapsed ? 'menu-unfold' : 'menu-fold'}
+          onClick={this.toggle}
+        />
+        <div className={styles.right}>
+          <HeaderSearch
+            className={`${styles.action} ${styles.search}`}
+            placeholder="敬请期待"
+            dataSource={[]}
+            onSearch={(value) => {
+              console.log('input', value); // eslint-disable-line
+            }}
+            onPressEnter={(value) => {
+              console.log('enter', value); // eslint-disable-line
+            }}
+          />
+          <NoticeIcon
+            className={styles.action}
+            count={currentUser.notifyCount}
+            onItemClick={(item, tabProps) => {
+              console.log(item, tabProps); // eslint-disable-line
+            }}
+            onClear={this.handleNoticeClear}
+            onPopupVisibleChange={this.handleNoticeVisibleChange}
+            loading={fetchingNotices}
+            popupAlign={{ offset: [20, -16] }}
+          >
+            <NoticeIcon.Tab
+              list={noticeData['通知']}
+              title="通知"
+              emptyText="你已查看所有通知"
+              emptyImage="https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg"
+            />
+            <NoticeIcon.Tab
+              list={noticeData['消息']}
+              title="消息"
+              emptyText="您已读完所有消息"
+              emptyImage="https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg"
+            />
+            <NoticeIcon.Tab
+              list={noticeData['待办']}
+              title="待办"
+              emptyText="你已完成所有待办"
+              emptyImage="https://gw.alipayobjects.com/zos/rmsportal/HsIsxMZiWKrNUavQUXqx.svg"
+            />
+          </NoticeIcon>
+          {currentUser.username ? (
+            <Dropdown overlay={menu}>
+              <span className={`${styles.action} ${styles.account}`}>
+                <Avatar size="small" style={{ backgroundColor: '#597ef7' }} className={styles.avatar} icon="user" src={currentUser.avatar} />
+                <span className={styles.name}>{currentUser.username}</span>
+              </span>
+            </Dropdown>
+          ) : <Spin size="small" style={{ marginLeft: 8 }} />}
+        </div>
+      </Header>
+    );
+  }
+}

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

@@ -0,0 +1,113 @@
+@import "~antd/lib/style/themes/default.less";
+
+.header {
+  padding: 0 12px 0 0;
+  background: #fff;
+  box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
+  position: relative;
+}
+
+:global {
+  .ant-layout {
+    overflow-x: hidden;
+  }
+}
+
+.logo {
+  height: 64px;
+  line-height: 58px;
+  vertical-align: top;
+  display: inline-block;
+  padding: 0 0 0 24px;
+  cursor: pointer;
+  font-size: 20px;
+  img {
+    display: inline-block;
+    vertical-align: middle;
+  }
+}
+
+.menu {
+  :global(.anticon) {
+    margin-right: 8px;
+  }
+  :global(.ant-dropdown-menu-item) {
+    text-align: center;
+    width: 100%;
+  }
+}
+
+i.trigger {
+  font-size: 20px;
+  line-height: 64px;
+  cursor: pointer;
+  transition: all .3s, padding 0s;
+  padding: 0 24px;
+  &:hover {
+    background: @primary-1;
+  }
+}
+
+.right {
+  float: right;
+  height: 100%;
+  .action {
+    cursor: pointer;
+    padding: 0 12px;
+    display: inline-block;
+    transition: all .3s;
+    height: 100%;
+    > i {
+      font-size: 16px;
+      vertical-align: middle;
+    }
+    &:global(.ant-popover-open),
+    &:hover {
+      background: @primary-1;
+    }
+  }
+  .search {
+    padding: 0;
+    margin: 0 12px;
+    &:hover {
+      background: transparent;
+    }
+  }
+  .account {
+    .avatar {
+      margin: 20px 8px 20px 0;
+      color: @primary-color;
+      background: rgba(255, 255, 255, .85);
+      vertical-align: middle;
+    }
+  }
+}
+
+@media only screen and (max-width: @screen-md) {
+  .header {
+    :global(.ant-divider-vertical) {
+      vertical-align: unset;
+    }
+    .name {
+      display: none;
+    }
+    i.trigger {
+      padding: 0 12px;
+    }
+    .logo {
+      padding-right: 12px;
+      position: relative;
+    }
+    .right {
+      position: absolute;
+      right: 12px;
+      top: 0;
+      background: #fff;
+      .account {
+        .avatar {
+          margin-right: 0;
+        }
+      }
+    }
+  }
+}

+ 34 - 0
src/components/HeaderSearch/demo/basic.md

@@ -0,0 +1,34 @@
+---
+order: 0
+title: 全局搜索
+---
+
+通常放置在导航工具条右侧。(点击搜索图标预览效果)
+
+````jsx
+import HeaderSearch from 'ant-design-pro/lib/HeaderSearch';
+
+ReactDOM.render(
+  <div
+    style={{
+      textAlign: 'right',
+      height: '64px',
+      lineHeight: '64px',
+      boxShadow: '0 1px 4px rgba(0,21,41,.12)',
+      padding: '0 32px',
+      width: '400px',
+    }}
+  >
+    <HeaderSearch
+      placeholder="站内搜索"
+      dataSource={['搜索提示一', '搜索提示二', '搜索提示三']}
+      onSearch={(value) => {
+        console.log('input', value); // eslint-disable-line
+      }}
+      onPressEnter={(value) => {
+        console.log('enter', value); // eslint-disable-line
+      }}
+    />
+  </div>
+, mountNode);
+````

+ 13 - 0
src/components/HeaderSearch/index.d.ts

@@ -0,0 +1,13 @@
+import * as React from "react";
+export interface HeaderSearchProps {
+  placeholder?: string;
+  dataSource?: Array<string>;
+  onSearch?: (value: string) => void;
+  onChange?: (value: string) => void;
+  onPressEnter?: (value: string) => void;
+}
+
+export default class HeaderSearch extends React.Component<
+  HeaderSearchProps,
+  any
+> {}

+ 85 - 0
src/components/HeaderSearch/index.js

@@ -0,0 +1,85 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { Input, Icon, AutoComplete } from 'antd';
+import classNames from 'classnames';
+import styles from './index.less';
+
+export default class HeaderSearch extends PureComponent {
+  static defaultProps = {
+    defaultActiveFirstOption: false,
+    onPressEnter: () => {},
+    onSearch: () => {},
+    className: '',
+    placeholder: '',
+    dataSource: [],
+  };
+  static propTypes = {
+    className: PropTypes.string,
+    placeholder: PropTypes.string,
+    onSearch: PropTypes.func,
+    onPressEnter: PropTypes.func,
+    defaultActiveFirstOption: PropTypes.bool,
+    dataSource: PropTypes.array,
+  };
+  state = {
+    searchMode: false,
+    value: '',
+  };
+  componentWillUnmount() {
+    clearTimeout(this.timeout);
+  }
+  onKeyDown = (e) => {
+    if (e.key === 'Enter') {
+      this.timeout = setTimeout(() => {
+        this.props.onPressEnter(this.state.value); // Fix duplicate onPressEnter
+      }, 0);
+    }
+  }
+  onChange = (value) => {
+    this.setState({ value });
+    if (this.props.onChange) {
+      this.props.onChange();
+    }
+  }
+  enterSearchMode = () => {
+    this.setState({ searchMode: true }, () => {
+      if (this.state.searchMode) {
+        this.input.focus();
+      }
+    });
+  }
+  leaveSearchMode = () => {
+    this.setState({
+      searchMode: false,
+      value: '',
+    });
+  }
+  render() {
+    const { className, placeholder, ...restProps } = this.props;
+    const inputClass = classNames(styles.input, {
+      [styles.show]: this.state.searchMode,
+    });
+    return (
+      <span
+        className={classNames(className, styles.headerSearch)}
+        onClick={this.enterSearchMode}
+      >
+        <Icon type="search" />
+        <AutoComplete
+          {...restProps}
+          className={inputClass}
+          value={this.state.value}
+          onChange={this.onChange}
+        >
+          <Input
+            placeholder={placeholder}
+            disabled={true}
+            ref={(node) => { this.input = node; }}
+            onKeyDown={this.onKeyDown}
+            onBlur={this.leaveSearchMode}
+          />
+        </AutoComplete>
+      </span>
+    );
+  }
+}

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

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

+ 20 - 0
src/components/HeaderSearch/index.md

@@ -0,0 +1,20 @@
+---
+title:
+  en-US: HeaderSearch
+  zh-CN: HeaderSearch
+subtitle: 顶部搜索框
+cols: 1
+order: 8
+---
+
+通常作为全局搜索的入口,放置在导航工具条右侧。
+
+## API
+
+参数 | 说明 | 类型 | 默认值
+----|------|-----|------
+placeholder | 占位文字 | string | -
+dataSource | 当前提示内容列表 | string[] | -
+onSearch | 选择某项或按下回车时的回调 | function(value) | -
+onChange | 输入搜索字符的回调 | function(value) | -
+onPressEnter | 按下回车时的回调 | function(value) | -

+ 0 - 0
src/components/NoticeIcon/NoticeList.js


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