commit 55498a2a9234131eb9dafc32031bf94d6f4ba01f Author: funics Date: Wed Oct 23 17:13:36 2019 +0800 init diff --git a/.babelrc.js b/.babelrc.js new file mode 100644 index 0000000..679a208 --- /dev/null +++ b/.babelrc.js @@ -0,0 +1,12 @@ +module.exports = { + plugins: [ + [ + 'babel-plugin-module-resolver', + { + alias: { + components: './src/components', + }, + }, + ], + ], +}; diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7e3649a --- /dev/null +++ b/.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 diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..c5dda4d --- /dev/null +++ b/.eslintrc @@ -0,0 +1,71 @@ +{ + "parser": "babel-eslint", + "extends": ["airbnb", "prettier"], + "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] + }, + "parserOptions": { + "ecmaFeatures": { + "experimentalObjectRestSpread": true + } + }, + "settings": { + "polyfills": ["fetch", "promises"] + } +} diff --git a/.ga b/.ga new file mode 100644 index 0000000..1932bb5 --- /dev/null +++ b/.ga @@ -0,0 +1,3 @@ +{ + "code":"UA-72788897-6" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fd9bf87 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +# roadhog-api-doc ignore +/src/utils/request-temp.js +_roadhog-api-doc + +# production +/dist +/vscode + +# misc +.DS_Store +npm-debug.log* +yarn-error.log + +/coverage +.idea +yarn.lock +package-lock.json +*bak +jsconfig.json diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..ac48aea --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +**/*.md +**/*.svg +package.json \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..0cc0de6 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "singleQuote": true, + "trailingComma": "es5", + "printWidth": 100, + "overrides": [ + { + "files": ".prettierrc", + "options": { "parser": "json" } + } + ] +} diff --git a/.roadhogrc.mock.js b/.roadhogrc.mock.js new file mode 100644 index 0000000..af60fec --- /dev/null +++ b/.roadhogrc.mock.js @@ -0,0 +1,140 @@ +import mockjs from 'mockjs'; +import { getRule, postRule } from './mock/rule'; +import { getActivities, getNotice, getFakeList } from './mock/api'; +import { getFakeChartData } from './mock/chart'; +import { getProfileBasicData } from './mock/profile'; +import { getProfileAdvancedData } from './mock/profile'; +import { getNotices } from './mock/notices'; +import { format, delay } from 'roadhog-api-doc'; + +// 是否禁用代理 +const noProxy = process.env.NO_PROXY === 'true'; + +// 代码中会兼容本地 service mock 以及部署站点的静态数据 +const proxy = { + // 支持值为 Object 和 Array + 'GET /api/currentUser': { + $desc: '获取当前用户接口', + $params: { + pageSize: { + desc: '分页', + exp: 2, + }, + }, + $body: { + name: 'Serati Ma', + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png', + userid: '00000001', + notifyCount: 12, + }, + }, + // GET POST 可省略 + 'GET /api/users': [ + { + key: '1', + name: 'John Brown', + age: 32, + address: 'New York No. 1 Lake Park', + }, + { + key: '2', + name: 'Jim Green', + age: 42, + address: 'London No. 1 Lake Park', + }, + { + key: '3', + name: 'Joe Black', + age: 32, + address: 'Sidney No. 1 Lake Park', + }, + ], + 'GET /api/project/notice': getNotice, + 'GET /api/activities': getActivities, + 'GET /api/rule': getRule, + 'POST /api/rule': { + $params: { + pageSize: { + desc: '分页', + exp: 2, + }, + }, + $body: postRule, + }, + 'POST /api/forms': (req, res) => { + res.send({ message: 'Ok' }); + }, + 'GET /api/tags': mockjs.mock({ + 'list|100': [{ name: '@city', 'value|1-100': 150, 'type|0-2': 1 }], + }), + 'GET /api/fake_list': getFakeList, + 'GET /api/fake_chart_data': getFakeChartData, + 'GET /api/profile/basic': getProfileBasicData, + 'GET /api/profile/advanced': getProfileAdvancedData, + 'POST /api/login/account': (req, res) => { + const { password, userName, type } = req.body; + if (password === '888888' && userName === 'admin') { + res.send({ + status: 'ok', + type, + currentAuthority: 'admin', + }); + return; + } + if (password === '123456' && userName === 'user') { + res.send({ + status: 'ok', + type, + currentAuthority: 'user', + }); + return; + } + res.send({ + status: 'error', + type, + currentAuthority: 'guest', + }); + }, + 'POST /api/register': (req, res) => { + res.send({ status: 'ok', currentAuthority: 'user' }); + }, + 'GET /api/notices': getNotices, + 'GET /api/500': (req, res) => { + res.status(500).send({ + timestamp: 1513932555104, + status: 500, + error: 'error', + message: 'error', + path: '/base/category/list', + }); + }, + 'GET /api/404': (req, res) => { + res.status(404).send({ + timestamp: 1513932643431, + status: 404, + error: 'Not Found', + message: 'No message available', + path: '/base/category/list/2121212', + }); + }, + 'GET /api/403': (req, res) => { + res.status(403).send({ + timestamp: 1513932555104, + status: 403, + error: 'Unauthorized', + message: 'Unauthorized', + path: '/base/category/list', + }); + }, + 'GET /api/401': (req, res) => { + res.status(401).send({ + timestamp: 1513932555104, + status: 401, + error: 'Unauthorized', + message: 'Unauthorized', + path: '/base/category/list', + }); + }, +}; + +export default (noProxy ? {} : delay(proxy, 1000)); diff --git a/.stylelintrc b/.stylelintrc new file mode 100644 index 0000000..a883e81 --- /dev/null +++ b/.stylelintrc @@ -0,0 +1,27 @@ +{ + "extends": ["stylelint-config-standard", "stylelint-config-prettier"], + "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-descendant-combinator-no-non-space": null, + "selector-list-comma-newline-after": null, + "selector-pseudo-element-colon-notation": null, + "unit-no-unknown": null, + "no-descending-specificity": null, + "value-list-max-empty-lines": null + } +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..7f55537 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,36 @@ +language: node_js + +node_js: + - "8" + +env: + matrix: + - TEST_TYPE=lint + - TEST_TYPE=build + - TEST_TYPE=test-all + - TEST_TYPE=test-dist + +addons: + apt: + packages: + - xvfb + +install: + - export DISPLAY=':99.0' + - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & + - npm install + +script: + - | + if [ "$TEST_TYPE" = lint ]; then + npm run lint + elif [ "$TEST_TYPE" = build ]; then + npm run build + elif [ "$TEST_TYPE" = test-all ]; then + npm run test:all + elif [ "$TEST_TYPE" = test-dist ]; then + npm run site + mv dist/* ./ + php -S localhost:8000 & + DEBUG=* npm test .e2e.js + fi diff --git a/.webpackrc.js b/.webpackrc.js new file mode 100644 index 0000000..9678603 --- /dev/null +++ b/.webpackrc.js @@ -0,0 +1,22 @@ +const path = require('path'); + +export default { + entry: 'src/index.js', + extraBabelPlugins: [['import', { libraryName: 'antd', libraryDirectory: 'es', style: true }]], + env: { + development: { + extraBabelPlugins: ['dva-hmr'], + }, + }, + alias: { + components: path.resolve(__dirname, 'src/components/'), + }, + ignoreMomentLocale: true, + theme: './src/theme.js', + html: { + template: './src/index.ejs', + }, + disableDynamicImport: true, + publicPath: '/', + hash: true, +}; diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..9e41f2e --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at afc163@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7ac7318 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Alipay.inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7224c3b --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# 教育平台 + +--- + +## 前言 + +  作为一个程序‘猿’,时曾在网上搜索各类的实战项目源码,大部分都是已经成型的企业的业务逻辑进行的二次分析,交互没有预期都是提前已经知晓的。但我们实际在工作中,经常会遇到随时更改的各类需求,所以对程序员逻辑严谨度要求高,处理特殊问题复杂,常常会让我们很头疼。既然这样,那不如自己来从源头分析一个产品。进行开发,希望站在研发的各个位置上,进行一次分析,对以后在项目中分析问题时,可以有一个更全面的眼光进行分析问题,我开源出来这次整体开发过程,希望对能看到的人会有帮助。 + +  为啥是教育平台,而不是仿照现成的东西?原因很简单,业余时间,经常逛各类在线学习平台,每个网站都有各自的特点,但是又都有一些不好的交互,自己希望开发一个好的教育平台,方便大家学习。 + +  这种功能性的项目很实用但是往往也很枯燥,没有音乐播放器那么看起来绚丽,思来想去发现饿了么是一个不错的素材,一来它足够复杂,开放的外卖平台比一般的公司独有商店更加复杂。二来 见到那么多美食,大家也不会感觉到厌烦 + +  此项目估计不会少于50个页面,涉及注册、登陆、组织管理、班级管理等等,是一个完整的流程。一般公司即便是官网的单页面项目都没这么复杂,如果这个项目能驾驭的了,相信大部分公司的其他单页面应用也就不在话下,即便更复杂,也不会比这个高到哪里去。 + +  因为利用业余时间来做,周期肯定有点长,项目从零布局开始到完成目前还不知道会多长时间,慢慢更新吧,会不断的进行一些页面的开发,增加详细的注释。 + +  另外,这个项目为了开发迅速,使用Antd-pro进行构建,未自己重新构建,因为自己比较喜欢这个样式风格,哈哈~ 。 + +### 注:此项目纯属个人瞎搞,如有雷同纯属意外; + +--- + +## 技术栈 +>React.js + dva.js + react-router + Redux + Ant.Design + Ant.Design.pro + WebPack + Less + Es6 + eS-lint + +## 项目运行 +注意:由于涉及 ES6/7 等新属性,nodejs 必须是 6.0 以上版本 ,node7是测试版,有可能会出问题,建议使用稳定版 + +>安装依赖 +>  npm install +>启动 +>  npm start + +## 说明 + +>本项目主要用于熟悉如何用 React + dva 架构一个大型项目 + +>如果对您有帮助,您可以点右上角 “Star” 支持一下 谢谢! + +>或者您可以 “follow” 一下,我会不断开源更多的有趣的项目 + +>开发环境 windows Chrome 55 + +>开发工具 Eclipse + vscode + +>如有问题请直接在 Issues 中提,或者您发现问题并有非常好的解决方案,欢迎 PR 👍 + + + + diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..22ae188 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,26 @@ +# Test against the latest version of this Node.js version +environment: + nodejs_version: "8" + +# this is how to allow failing jobs in the matrix +matrix: + fast_finish: true # set this flag to immediately finish build once one of the jobs fails. + +# Install scripts. (runs after repo cloning) +install: + # Get the latest stable version of Node.js or io.js + - ps: Install-Product node $env:nodejs_version + # install modules + - npm install + # Output useful info for debugging. + - node --version + - npm --version + +# Post-install test scripts. +test_script: + - npm run lint + - npm run test:all + - npm run build + +# Don't actually build. +build: off diff --git a/mock/.gitkeep b/mock/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/mock/api.js b/mock/api.js new file mode 100644 index 0000000..a274afb --- /dev/null +++ b/mock/api.js @@ -0,0 +1,296 @@ +import { parse } from 'url'; + +const titles = [ + 'Alipay', + 'Angular', + 'Ant Design', + 'Ant Design Pro', + 'Bootstrap', + 'React', + 'Vue', + 'Webpack', +]; +const avatars = [ + 'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png', // Alipay + 'https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png', // Angular + 'https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png', // Ant Design + 'https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png', // Ant Design Pro + 'https://gw.alipayobjects.com/zos/rmsportal/siCrBXXhmvTQGWPNLBow.png', // Bootstrap + 'https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png', // React + 'https://gw.alipayobjects.com/zos/rmsportal/ComBAopevLwENQdKWiIn.png', // Vue + 'https://gw.alipayobjects.com/zos/rmsportal/nxkuOJlFJuAUhzlMTCEe.png', // Webpack +]; + +const avatars2 = [ + 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png', + 'https://gw.alipayobjects.com/zos/rmsportal/cnrhVkzwxjPwAaCfPbdc.png', + 'https://gw.alipayobjects.com/zos/rmsportal/gaOngJwsRYRaVAuXXcmB.png', + 'https://gw.alipayobjects.com/zos/rmsportal/ubnKSIfAJTxIgXOKlciN.png', + 'https://gw.alipayobjects.com/zos/rmsportal/WhxKECPNujWoWEFNdnJE.png', + 'https://gw.alipayobjects.com/zos/rmsportal/jZUIxmJycoymBprLOUbT.png', + 'https://gw.alipayobjects.com/zos/rmsportal/psOgztMplJMGpVEqfcgF.png', + 'https://gw.alipayobjects.com/zos/rmsportal/ZpBqSxLxVEXfcUNoPKrz.png', + 'https://gw.alipayobjects.com/zos/rmsportal/laiEnJdGHVOhJrUShBaJ.png', + 'https://gw.alipayobjects.com/zos/rmsportal/UrQsqscbKEpNuJcvBZBu.png', +]; + +const covers = [ + 'https://gw.alipayobjects.com/zos/rmsportal/uMfMFlvUuceEyPpotzlq.png', + 'https://gw.alipayobjects.com/zos/rmsportal/iZBVOIhGJiAnhplqjvZW.png', + 'https://gw.alipayobjects.com/zos/rmsportal/uVZonEtjWwmUZPBQfycs.png', + 'https://gw.alipayobjects.com/zos/rmsportal/gLaIAoVWTtLbBWZNYEMg.png', +]; +const desc = [ + '那是一种内在的东西, 他们到达不了,也无法触及的', + '希望是一个好东西,也许是最好的,好东西是不会消亡的', + '生命就像一盒巧克力,结果往往出人意料', + '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆', + '那时候我只会想自己想要什么,从不想自己拥有什么', +]; + +const user = [ + '付小小', + '曲丽丽', + '林东东', + '周星星', + '吴加好', + '朱偏右', + '鱼酱', + '乐哥', + '谭小仪', + '仲尼', +]; + +export function fakeList(count) { + const list = []; + for (let i = 0; i < count; i += 1) { + list.push({ + id: `fake-list-${i}`, + owner: user[i % 10], + title: titles[i % 8], + avatar: avatars[i % 8], + cover: parseInt(i / 4, 10) % 2 === 0 ? covers[i % 4] : covers[3 - i % 4], + status: ['active', 'exception', 'normal'][i % 3], + percent: Math.ceil(Math.random() * 50) + 50, + logo: avatars[i % 8], + href: 'https://ant.design', + updatedAt: new Date(new Date().getTime() - 1000 * 60 * 60 * 2 * i), + createdAt: new Date(new Date().getTime() - 1000 * 60 * 60 * 2 * i), + subDescription: desc[i % 5], + description: + '在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。', + activeUser: Math.ceil(Math.random() * 100000) + 100000, + newUser: Math.ceil(Math.random() * 1000) + 1000, + star: Math.ceil(Math.random() * 100) + 100, + like: Math.ceil(Math.random() * 100) + 100, + message: Math.ceil(Math.random() * 10) + 10, + content: + '段落示意:蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。', + members: [ + { + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ZiESqWwCXBRQoaPONSJe.png', + name: '曲丽丽', + }, + { + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/tBOxZPlITHqwlGjsJWaF.png', + name: '王昭君', + }, + { + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/sBxjgqiuHMGRkIjqlQCd.png', + name: '董娜娜', + }, + ], + }); + } + + return list; +} + +export function getFakeList(req, res, u) { + let url = u; + if (!url || Object.prototype.toString.call(url) !== '[object String]') { + url = req.url; // eslint-disable-line + } + + const params = parse(url, true).query; + + const count = params.count * 1 || 20; + + const result = fakeList(count); + + if (res && res.json) { + res.json(result); + } else { + return result; + } +} + +export const getNotice = [ + { + id: 'xxx1', + title: titles[0], + logo: avatars[0], + description: '那是一种内在的东西,他们到达不了,也无法触及的', + updatedAt: new Date(), + member: '科学搬砖组', + href: '', + memberLink: '', + }, + { + id: 'xxx2', + title: titles[1], + logo: avatars[1], + description: '希望是一个好东西,也许是最好的,好东西是不会消亡的', + updatedAt: new Date('2017-07-24'), + member: '全组都是吴彦祖', + href: '', + memberLink: '', + }, + { + id: 'xxx3', + title: titles[2], + logo: avatars[2], + description: '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆', + updatedAt: new Date(), + member: '中二少女团', + href: '', + memberLink: '', + }, + { + id: 'xxx4', + title: titles[3], + logo: avatars[3], + description: '那时候我只会想自己想要什么,从不想自己拥有什么', + updatedAt: new Date('2017-07-23'), + member: '程序员日常', + href: '', + memberLink: '', + }, + { + id: 'xxx5', + title: titles[4], + logo: avatars[4], + description: '凛冬将至', + updatedAt: new Date('2017-07-23'), + member: '高逼格设计天团', + href: '', + memberLink: '', + }, + { + id: 'xxx6', + title: titles[5], + logo: avatars[5], + description: '生命就像一盒巧克力,结果往往出人意料', + updatedAt: new Date('2017-07-23'), + member: '骗你来学计算机', + href: '', + memberLink: '', + }, +]; + +export const getActivities = [ + { + id: 'trend-1', + updatedAt: new Date(), + user: { + name: '曲丽丽', + avatar: avatars2[0], + }, + group: { + name: '高逼格设计天团', + link: 'http://github.com/', + }, + project: { + name: '六月迭代', + link: 'http://github.com/', + }, + template: '在 @{group} 新建项目 @{project}', + }, + { + id: 'trend-2', + updatedAt: new Date(), + user: { + name: '付小小', + avatar: avatars2[1], + }, + group: { + name: '高逼格设计天团', + link: 'http://github.com/', + }, + project: { + name: '六月迭代', + link: 'http://github.com/', + }, + template: '在 @{group} 新建项目 @{project}', + }, + { + id: 'trend-3', + updatedAt: new Date(), + user: { + name: '林东东', + avatar: avatars2[2], + }, + group: { + name: '中二少女团', + link: 'http://github.com/', + }, + project: { + name: '六月迭代', + link: 'http://github.com/', + }, + template: '在 @{group} 新建项目 @{project}', + }, + { + id: 'trend-4', + updatedAt: new Date(), + user: { + name: '周星星', + avatar: avatars2[4], + }, + project: { + name: '5 月日常迭代', + link: 'http://github.com/', + }, + template: '将 @{project} 更新至已发布状态', + }, + { + id: 'trend-5', + updatedAt: new Date(), + user: { + name: '朱偏右', + avatar: avatars2[3], + }, + project: { + name: '工程效能', + link: 'http://github.com/', + }, + comment: { + name: '留言', + link: 'http://github.com/', + }, + template: '在 @{project} 发布了 @{comment}', + }, + { + id: 'trend-6', + updatedAt: new Date(), + user: { + name: '乐哥', + avatar: avatars2[5], + }, + group: { + name: '程序员日常', + link: 'http://github.com/', + }, + project: { + name: '品牌迭代', + link: 'http://github.com/', + }, + template: '在 @{group} 新建项目 @{project}', + }, +]; + +export default { + getNotice, + getActivities, + getFakeList, +}; diff --git a/mock/chart.js b/mock/chart.js new file mode 100644 index 0000000..55c8a41 --- /dev/null +++ b/mock/chart.js @@ -0,0 +1,197 @@ +import moment from 'moment'; + +// mock data +const visitData = []; +const beginDay = new Date().getTime(); + +const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5]; +for (let i = 0; i < fakeY.length; i += 1) { + visitData.push({ + x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'), + y: fakeY[i], + }); +} + +const visitData2 = []; +const fakeY2 = [1, 6, 4, 8, 3, 7, 2]; +for (let i = 0; i < fakeY2.length; i += 1) { + visitData2.push({ + x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'), + y: fakeY2[i], + }); +} + +const salesData = []; +for (let i = 0; i < 12; i += 1) { + salesData.push({ + x: `${i + 1}月`, + y: Math.floor(Math.random() * 1000) + 200, + }); +} +const searchData = []; +for (let i = 0; i < 50; i += 1) { + searchData.push({ + index: i + 1, + keyword: `搜索关键词-${i}`, + count: Math.floor(Math.random() * 1000), + range: Math.floor(Math.random() * 100), + status: Math.floor((Math.random() * 10) % 2), + }); +} +const salesTypeData = [ + { + x: '家用电器', + y: 4544, + }, + { + x: '食用酒水', + y: 3321, + }, + { + x: '个护健康', + y: 3113, + }, + { + x: '服饰箱包', + y: 2341, + }, + { + x: '母婴产品', + y: 1231, + }, + { + x: '其他', + y: 1231, + }, +]; + +const salesTypeDataOnline = [ + { + x: '家用电器', + y: 244, + }, + { + x: '食用酒水', + y: 321, + }, + { + x: '个护健康', + y: 311, + }, + { + x: '服饰箱包', + y: 41, + }, + { + x: '母婴产品', + y: 121, + }, + { + x: '其他', + y: 111, + }, +]; + +const salesTypeDataOffline = [ + { + x: '家用电器', + y: 99, + }, + { + x: '个护健康', + y: 188, + }, + { + x: '服饰箱包', + y: 344, + }, + { + x: '母婴产品', + y: 255, + }, + { + x: '其他', + y: 65, + }, +]; + +const offlineData = []; +for (let i = 0; i < 10; i += 1) { + offlineData.push({ + name: `门店${i}`, + cvr: Math.ceil(Math.random() * 9) / 10, + }); +} +const offlineChartData = []; +for (let i = 0; i < 20; i += 1) { + offlineChartData.push({ + x: new Date().getTime() + 1000 * 60 * 30 * i, + y1: Math.floor(Math.random() * 100) + 10, + y2: Math.floor(Math.random() * 100) + 10, + }); +} + +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], + }); + } + }); +}); + +export const getFakeChartData = { + visitData, + visitData2, + salesData, + searchData, + offlineData, + offlineChartData, + salesTypeData, + salesTypeDataOnline, + salesTypeDataOffline, + radarData, +}; + +export default { + getFakeChartData, +}; diff --git a/mock/notices.js b/mock/notices.js new file mode 100644 index 0000000..7e1b3ce --- /dev/null +++ b/mock/notices.js @@ -0,0 +1,99 @@ +export const getNotices = (req, res) => { + res.json([ + { + id: '000000001', + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png', + title: '你收到了 14 份新周报', + datetime: '2017-08-09', + type: '通知', + }, + { + id: '000000002', + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png', + title: '你推荐的 曲妮妮 已通过第三轮面试', + datetime: '2017-08-08', + type: '通知', + }, + { + id: '000000003', + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png', + title: '这种模板可以区分多种通知类型', + datetime: '2017-08-07', + read: true, + type: '通知', + }, + { + id: '000000004', + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png', + title: '左侧图标用于区分不同的类型', + datetime: '2017-08-07', + type: '通知', + }, + { + id: '000000005', + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png', + title: '内容不要超过两行字,超出时自动截断', + datetime: '2017-08-07', + type: '通知', + }, + { + id: '000000006', + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', + title: '曲丽丽 评论了你', + description: '描述信息描述信息描述信息', + datetime: '2017-08-07', + type: '消息', + }, + { + id: '000000007', + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', + title: '朱偏右 回复了你', + description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像', + datetime: '2017-08-07', + type: '消息', + }, + { + id: '000000008', + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', + title: '标题', + description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像', + datetime: '2017-08-07', + type: '消息', + }, + { + id: '000000009', + title: '任务名称', + description: '任务需要在 2017-01-12 20:00 前启动', + extra: '未开始', + status: 'todo', + type: '待办', + }, + { + id: '000000010', + title: '第三方紧急代码变更', + description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务', + extra: '马上到期', + status: 'urgent', + type: '待办', + }, + { + id: '000000011', + title: '信息安全考试', + description: '指派竹尔于 2017-01-09 前完成更新并发布', + extra: '已耗时 8 天', + status: 'doing', + type: '待办', + }, + { + id: '000000012', + title: 'ABCD 版本发布', + description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务', + extra: '进行中', + status: 'processing', + type: '待办', + }, + ]); +}; +export default { + getNotices, +}; diff --git a/mock/profile.js b/mock/profile.js new file mode 100644 index 0000000..7c24be1 --- /dev/null +++ b/mock/profile.js @@ -0,0 +1,158 @@ +const basicGoods = [ + { + id: '1234561', + name: '矿泉水 550ml', + barcode: '12421432143214321', + price: '2.00', + num: '1', + amount: '2.00', + }, + { + id: '1234562', + name: '凉茶 300ml', + barcode: '12421432143214322', + price: '3.00', + num: '2', + amount: '6.00', + }, + { + id: '1234563', + name: '好吃的薯片', + barcode: '12421432143214323', + price: '7.00', + num: '4', + amount: '28.00', + }, + { + id: '1234564', + name: '特别好吃的蛋卷', + barcode: '12421432143214324', + price: '8.50', + num: '3', + amount: '25.50', + }, +]; + +const basicProgress = [ + { + key: '1', + time: '2017-10-01 14:10', + rate: '联系客户', + status: 'processing', + operator: '取货员 ID1234', + cost: '5mins', + }, + { + key: '2', + time: '2017-10-01 14:05', + rate: '取货员出发', + status: 'success', + operator: '取货员 ID1234', + cost: '1h', + }, + { + key: '3', + time: '2017-10-01 13:05', + rate: '取货员接单', + status: 'success', + operator: '取货员 ID1234', + cost: '5mins', + }, + { + key: '4', + time: '2017-10-01 13:00', + rate: '申请审批通过', + status: 'success', + operator: '系统', + cost: '1h', + }, + { + key: '5', + time: '2017-10-01 12:00', + rate: '发起退货申请', + status: 'success', + operator: '用户', + cost: '5mins', + }, +]; + +const advancedOperation1 = [ + { + key: 'op1', + type: '订购关系生效', + name: '曲丽丽', + status: 'agree', + updatedAt: '2017-10-03 19:23:12', + memo: '-', + }, + { + key: 'op2', + type: '财务复审', + name: '付小小', + status: 'reject', + updatedAt: '2017-10-03 19:23:12', + memo: '不通过原因', + }, + { + key: 'op3', + type: '部门初审', + name: '周毛毛', + status: 'agree', + updatedAt: '2017-10-03 19:23:12', + memo: '-', + }, + { + key: 'op4', + type: '提交订单', + name: '林东东', + status: 'agree', + updatedAt: '2017-10-03 19:23:12', + memo: '很棒', + }, + { + key: 'op5', + type: '创建订单', + name: '汗牙牙', + status: 'agree', + updatedAt: '2017-10-03 19:23:12', + memo: '-', + }, +]; + +const advancedOperation2 = [ + { + key: 'op1', + type: '订购关系生效', + name: '曲丽丽', + status: 'agree', + updatedAt: '2017-10-03 19:23:12', + memo: '-', + }, +]; + +const advancedOperation3 = [ + { + key: 'op1', + type: '创建订单', + name: '汗牙牙', + status: 'agree', + updatedAt: '2017-10-03 19:23:12', + memo: '-', + }, +]; + +export const getProfileBasicData = { + basicGoods, + basicProgress, +}; + +export const getProfileAdvancedData = { + advancedOperation1, + advancedOperation2, + advancedOperation3, +}; + +export default { + getProfileBasicData, + getProfileAdvancedData, +}; diff --git a/mock/rule.js b/mock/rule.js new file mode 100644 index 0000000..e89b949 --- /dev/null +++ b/mock/rule.js @@ -0,0 +1,137 @@ +import { parse } from 'url'; + +// mock tableListDataSource +let tableListDataSource = []; +for (let i = 0; i < 46; i += 1) { + tableListDataSource.push({ + key: i, + disabled: i % 6 === 0, + href: 'https://ant.design', + avatar: [ + 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png', + 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png', + ][i % 2], + no: `TradeCode ${i}`, + title: `一个任务名称 ${i}`, + owner: '曲丽丽', + description: '这是一段描述', + callNo: Math.floor(Math.random() * 1000), + status: Math.floor(Math.random() * 10) % 4, + updatedAt: new Date(`2017-07-${Math.floor(i / 2) + 1}`), + createdAt: new Date(`2017-07-${Math.floor(i / 2) + 1}`), + progress: Math.ceil(Math.random() * 100), + }); +} + +export function getRule(req, res, u) { + let url = u; + if (!url || Object.prototype.toString.call(url) !== '[object String]') { + url = req.url; // eslint-disable-line + } + + const params = parse(url, true).query; + + let dataSource = [...tableListDataSource]; + + if (params.sorter) { + const s = params.sorter.split('_'); + dataSource = dataSource.sort((prev, next) => { + if (s[1] === 'descend') { + return next[s[0]] - prev[s[0]]; + } + return prev[s[0]] - next[s[0]]; + }); + } + + if (params.status) { + const status = params.status.split(','); + let filterDataSource = []; + status.forEach(s => { + filterDataSource = filterDataSource.concat( + [...dataSource].filter(data => parseInt(data.status, 10) === parseInt(s[0], 10)) + ); + }); + dataSource = filterDataSource; + } + + if (params.no) { + dataSource = dataSource.filter(data => data.no.indexOf(params.no) > -1); + } + + let pageSize = 10; + if (params.pageSize) { + pageSize = params.pageSize * 1; + } + + const result = { + list: dataSource, + pagination: { + total: dataSource.length, + pageSize, + current: parseInt(params.currentPage, 10) || 1, + }, + }; + + if (res && res.json) { + res.json(result); + } else { + return result; + } +} + +export function postRule(req, res, u, b) { + let url = u; + if (!url || Object.prototype.toString.call(url) !== '[object String]') { + url = req.url; // eslint-disable-line + } + + const body = (b && b.body) || req.body; + const { method, no, description } = body; + + switch (method) { + /* eslint no-case-declarations:0 */ + case 'delete': + tableListDataSource = tableListDataSource.filter(item => no.indexOf(item.no) === -1); + break; + case 'post': + const i = Math.ceil(Math.random() * 10000); + tableListDataSource.unshift({ + key: i, + href: 'https://ant.design', + avatar: [ + 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png', + 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png', + ][i % 2], + no: `TradeCode ${i}`, + title: `一个任务名称 ${i}`, + owner: '曲丽丽', + description, + callNo: Math.floor(Math.random() * 1000), + status: Math.floor(Math.random() * 10) % 2, + updatedAt: new Date(), + createdAt: new Date(), + progress: Math.ceil(Math.random() * 100), + }); + break; + default: + break; + } + + const result = { + list: tableListDataSource, + pagination: { + total: tableListDataSource.length, + }, + }; + + if (res && res.json) { + res.json(result); + } else { + return result; + } +} + +export default { + getRule, + postRule, +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..81012a5 --- /dev/null +++ b/package.json @@ -0,0 +1,101 @@ +{ + "name": "ant-design-pro", + "version": "1.3.0", + "description": "An out-of-box UI solution for enterprise applications", + "private": true, + "scripts": { + "precommit": "npm run lint-staged", + "start": "cross-env ESLINT=none roadhog dev", + "start:no-proxy": "cross-env NO_PROXY=true ESLINT=none roadhog dev", + "build": "cross-env ESLINT=none roadhog build", + "site": "roadhog-api-doc static && gh-pages -d dist", + "analyze": "cross-env ANALYZE=true roadhog build", + "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": "roadhog test", + "test:component": "roadhog test ./src/components", + "test:all": "node ./tests/run-tests.js", + "prettier": "prettier --write ./src/**/**/**/*" + }, + "dependencies": { + "@antv/data-set": "^0.8.0", + "@babel/polyfill": "^7.0.0-beta.36", + "antd": "^3.3.0", + "babel-runtime": "^6.9.2", + "bizcharts": "^3.1.3-beta.1", + "bizcharts-plugin-slider": "^2.0.1", + "classnames": "^2.2.5", + "dva": "^2.1.0", + "dva-loading": "^1.0.4", + "enquire-js": "^0.2.1", + "lodash": "^4.17.4", + "lodash-decorators": "^4.4.1", + "moment": "^2.19.1", + "numeral": "^2.0.6", + "omit.js": "^1.0.0", + "path-to-regexp": "^2.1.0", + "prop-types": "^15.5.10", + "qs": "^6.5.0", + "rc-drawer-menu": "^0.5.0", + "react": "^16.2.0", + "react-container-query": "^0.9.1", + "react-document-title": "^2.0.3", + "react-dom": "^16.2.0", + "react-fittext": "^1.0.0", + "rollbar": "^2.3.4", + "url-polyfill": "^1.0.10" + }, + "devDependencies": { + "babel-eslint": "^8.1.2", + "babel-plugin-dva-hmr": "^0.4.1", + "babel-plugin-import": "^1.6.7", + "babel-plugin-module-resolver": "^3.1.1", + "cross-env": "^5.1.1", + "cross-port-killer": "^1.0.1", + "enzyme": "^3.1.0", + "eslint": "^4.14.0", + "eslint-config-airbnb": "^16.0.0", + "eslint-plugin-babel": "^4.0.0", + "eslint-plugin-compat": "^2.1.0", + "eslint-plugin-import": "^2.8.0", + "eslint-plugin-jsx-a11y": "^6.0.3", + "eslint-plugin-markdown": "^1.0.0-beta.6", + "eslint-plugin-react": "^7.0.1", + "eslint-config-prettier": "^2.9.0", + "gh-pages": "^1.0.0", + "husky": "^0.14.3", + "lint-staged": "^6.0.0", + "mockjs": "^1.0.1-beta3", + "prettier": "1.11.1", + "pro-download": "^1.0.1", + "redbox-react": "^1.5.0", + "regenerator-runtime": "^0.11.1", + "roadhog": "^2.3.0", + "roadhog-api-doc": "^0.3.4", + "stylelint": "^8.4.0", + "stylelint-config-prettier": "^3.0.4", + "stylelint-config-standard": "^18.0.0" + }, + "optionalDependencies": { + "puppeteer": "^1.1.1" + }, + "lint-staged": { + "**/*.{js,jsx,less}": [ + "prettier --wirter", + "git add" + ], + "**/*.{js,jsx}": "lint-staged:js", + "**/*.less": "stylelint --syntax less" + }, + "engines": { + "node": ">=8.0.0" + }, + "browserslist": [ + "> 1%", + "last 2 versions", + "not ie <= 10" + ] +} \ No newline at end of file diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000..cc27768 Binary files /dev/null and b/public/favicon.png differ diff --git a/src/assets/logo.svg b/src/assets/logo.svg new file mode 100644 index 0000000..e9f8c2a --- /dev/null +++ b/src/assets/logo.svg @@ -0,0 +1,43 @@ + + + + Group 28 Copy 5 + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/common/menu.js b/src/common/menu.js new file mode 100644 index 0000000..ee3875e --- /dev/null +++ b/src/common/menu.js @@ -0,0 +1,177 @@ +import { isUrl } from '../utils/utils'; + +const menuData = [ + { + name: 'dashboard', + icon: 'dashboard', + path: 'dashboard', + children: [ + { + name: '分析页', + path: 'analysis', + }, + { + name: '监控页', + path: 'monitor', + }, + { + name: '工作台', + path: 'workplace', + // hideInBreadcrumb: true, + // hideInMenu: true, + }, + ], + }, + { + name: '表单页', + icon: 'form', + path: 'form', + children: [ + { + name: '基础表单', + path: 'basic-form', + }, + { + name: '分步表单', + path: 'step-form', + }, + { + name: '高级表单', + authority: 'admin', + path: 'advanced-form', + }, + ], + }, + { + name: '列表页', + icon: 'table', + path: 'list', + children: [ + { + name: '查询表格', + path: 'table-list', + }, + { + name: '标准列表', + path: 'basic-list', + }, + { + name: '卡片列表', + path: 'card-list', + }, + { + name: '搜索列表', + path: 'search', + children: [ + { + name: '搜索列表(文章)', + path: 'articles', + }, + { + name: '搜索列表(项目)', + path: 'projects', + }, + { + name: '搜索列表(应用)', + path: 'applications', + }, + ], + }, + ], + }, + { + name: '详情页', + icon: 'profile', + path: 'profile', + children: [ + { + name: '基础详情页', + path: 'basic', + }, + { + name: '高级详情页', + path: 'advanced', + authority: 'admin', + }, + ], + }, + { + name: '结果页', + icon: 'check-circle-o', + path: 'result', + children: [ + { + name: '成功', + path: 'success', + }, + { + name: '失败', + path: 'fail', + }, + ], + }, + { + name: '异常页', + icon: 'warning', + path: 'exception', + children: [ + { + name: '403', + path: '403', + }, + { + name: '404', + path: '404', + }, + { + name: '500', + path: '500', + }, + { + name: '触发异常', + path: 'trigger', + hideInMenu: true, + }, + ], + }, + { + name: '账户', + icon: 'user', + path: 'user', + authority: 'guest', + children: [ + { + name: '登录', + path: 'login', + }, + { + name: '注册', + path: 'register', + }, + { + name: '注册结果', + path: 'register-result', + }, + ], + }, +]; + +function formatter(data, parentPath = '/', parentAuthority) { + return data.map(item => { + let { path } = item; + if (!isUrl(path)) { + path = parentPath + item.path; + } + const result = { + ...item, + path, + authority: item.authority || parentAuthority, + }; + if (item.children) { + result.children = formatter(item.children, `${parentPath}${item.path}/`, item.authority); + } + return result; + }); +} + +export const getMenuData = () => formatter(menuData); diff --git a/src/common/router.js b/src/common/router.js new file mode 100644 index 0000000..e7312c4 --- /dev/null +++ b/src/common/router.js @@ -0,0 +1,205 @@ +import { createElement } from 'react'; +import dynamic from 'dva/dynamic'; +import pathToRegexp from 'path-to-regexp'; +import { getMenuData } from './menu'; + +let routerDataCache; + +const modelNotExisted = (app, model) => + // eslint-disable-next-line + !app._models.some(({ namespace }) => { + return namespace === model.substring(model.lastIndexOf('/') + 1); + }); + +// wrapper of dynamic +const dynamicWrapper = (app, models, component) => { + // () => require('module') + // transformed by babel-plugin-dynamic-import-node-sync + if (component.toString().indexOf('.then(') < 0) { + models.forEach(model => { + if (modelNotExisted(app, model)) { + // eslint-disable-next-line + app.model(require(`../models/${model}`).default); + } + }); + return props => { + if (!routerDataCache) { + routerDataCache = getRouterData(app); + } + return createElement(component().default, { + ...props, + routerData: routerDataCache, + }); + }; + } + // () => import('module') + return dynamic({ + app, + models: () => + models.filter(model => modelNotExisted(app, model)).map(m => import(`../models/${m}.js`)), + // add routerData prop + component: () => { + if (!routerDataCache) { + routerDataCache = getRouterData(app); + } + return component().then(raw => { + const Component = raw.default || raw; + return props => + createElement(Component, { + ...props, + routerData: routerDataCache, + }); + }); + }, + }); +}; + +function getFlatMenuData(menus) { + let keys = {}; + menus.forEach(item => { + if (item.children) { + keys[item.path] = { ...item }; + keys = { ...keys, ...getFlatMenuData(item.children) }; + } else { + keys[item.path] = { ...item }; + } + }); + return keys; +} + +export const getRouterData = app => { + const routerConfig = { + '/': { + component: dynamicWrapper(app, ['user', 'login'], () => import('../layouts/BasicLayout')), + }, + '/dashboard/analysis': { + component: dynamicWrapper(app, ['chart'], () => import('../routes/Dashboard/Analysis')), + }, + '/dashboard/monitor': { + component: dynamicWrapper(app, ['monitor'], () => import('../routes/Dashboard/Monitor')), + }, + '/dashboard/workplace': { + component: dynamicWrapper(app, ['project', 'activities', 'chart'], () => + import('../routes/Dashboard/Workplace') + ), + // hideInBreadcrumb: true, + // name: '工作台', + // authority: 'admin', + }, + '/form/basic-form': { + component: dynamicWrapper(app, ['form'], () => import('../routes/Forms/BasicForm')), + }, + '/form/step-form': { + component: dynamicWrapper(app, ['form'], () => import('../routes/Forms/StepForm')), + }, + '/form/step-form/info': { + name: '分步表单(填写转账信息)', + component: dynamicWrapper(app, ['form'], () => import('../routes/Forms/StepForm/Step1')), + }, + '/form/step-form/confirm': { + name: '分步表单(确认转账信息)', + component: dynamicWrapper(app, ['form'], () => import('../routes/Forms/StepForm/Step2')), + }, + '/form/step-form/result': { + name: '分步表单(完成)', + component: dynamicWrapper(app, ['form'], () => import('../routes/Forms/StepForm/Step3')), + }, + '/form/advanced-form': { + component: dynamicWrapper(app, ['form'], () => import('../routes/Forms/AdvancedForm')), + }, + '/list/table-list': { + component: dynamicWrapper(app, ['rule'], () => import('../routes/List/TableList')), + }, + '/list/basic-list': { + component: dynamicWrapper(app, ['list'], () => import('../routes/List/BasicList')), + }, + '/list/card-list': { + component: dynamicWrapper(app, ['list'], () => import('../routes/List/CardList')), + }, + '/list/search': { + component: dynamicWrapper(app, ['list'], () => import('../routes/List/List')), + }, + '/list/search/projects': { + component: dynamicWrapper(app, ['list'], () => import('../routes/List/Projects')), + }, + '/list/search/applications': { + component: dynamicWrapper(app, ['list'], () => import('../routes/List/Applications')), + }, + '/list/search/articles': { + component: dynamicWrapper(app, ['list'], () => import('../routes/List/Articles')), + }, + '/profile/basic': { + component: dynamicWrapper(app, ['profile'], () => import('../routes/Profile/BasicProfile')), + }, + '/profile/advanced': { + component: dynamicWrapper(app, ['profile'], () => + import('../routes/Profile/AdvancedProfile') + ), + }, + '/result/success': { + component: dynamicWrapper(app, [], () => import('../routes/Result/Success')), + }, + '/result/fail': { + component: dynamicWrapper(app, [], () => import('../routes/Result/Error')), + }, + '/exception/403': { + component: dynamicWrapper(app, [], () => import('../routes/Exception/403')), + }, + '/exception/404': { + component: dynamicWrapper(app, [], () => import('../routes/Exception/404')), + }, + '/exception/500': { + component: dynamicWrapper(app, [], () => import('../routes/Exception/500')), + }, + '/exception/trigger': { + component: dynamicWrapper(app, ['error'], () => + import('../routes/Exception/triggerException') + ), + }, + '/user': { + component: dynamicWrapper(app, [], () => import('../layouts/UserLayout')), + }, + '/user/login': { + component: dynamicWrapper(app, ['login'], () => import('../routes/User/Login')), + }, + '/user/register': { + component: dynamicWrapper(app, ['register'], () => import('../routes/User/Register')), + }, + '/user/register-result': { + component: dynamicWrapper(app, [], () => import('../routes/User/RegisterResult')), + }, + // '/user/:id': { + // component: dynamicWrapper(app, [], () => import('../routes/User/SomeComponent')), + // }, + }; + // Get name from ./menu.js or just set it in the router data. + const menuData = getFlatMenuData(getMenuData()); + + // Route configuration data + // eg. {name,authority ...routerConfig } + const routerData = {}; + // The route matches the menu + Object.keys(routerConfig).forEach(path => { + // Regular match item name + // eg. router /user/:id === /user/chen + const pathRegexp = pathToRegexp(path); + const menuKey = Object.keys(menuData).find(key => pathRegexp.test(`${key}`)); + let menuItem = {}; + // If menuKey is not empty + if (menuKey) { + menuItem = menuData[menuKey]; + } + let router = routerConfig[path]; + // If you need to configure complex parameter routing, + // https://github.com/ant-design/ant-design-pro-site/blob/master/docs/router-and-nav.md#%E5%B8%A6%E5%8F%82%E6%95%B0%E7%9A%84%E8%B7%AF%E7%94%B1%E8%8F%9C%E5%8D%95 + // eg . /list/:type/user/info/:id + router = { + ...router, + name: router.name || menuItem.name, + authority: router.authority || menuItem.authority, + hideInBreadcrumb: router.hideInBreadcrumb || menuItem.hideInBreadcrumb, + }; + routerData[path] = router; + }); + return routerData; +}; diff --git a/src/components/ActiveChart/index.js b/src/components/ActiveChart/index.js new file mode 100644 index 0000000..cd6119b --- /dev/null +++ b/src/components/ActiveChart/index.js @@ -0,0 +1,82 @@ +import React, { Component } from 'react'; + +import { MiniArea } from '../Charts'; +import NumberInfo from '../NumberInfo'; + +import styles from './index.less'; + +function fixedZero(val) { + return val * 1 < 10 ? `0${val}` : val; +} + +function getActiveData() { + const activeData = []; + for (let i = 0; i < 24; i += 1) { + activeData.push({ + x: `${fixedZero(i)}:00`, + y: Math.floor(Math.random() * 200) + i * 50, + }); + } + return activeData; +} + +export default class ActiveChart extends Component { + state = { + activeData: getActiveData(), + }; + + componentDidMount() { + this.timer = setInterval(() => { + this.setState({ + activeData: getActiveData(), + }); + }, 1000); + } + + componentWillUnmount() { + clearInterval(this.timer); + } + + render() { + const { activeData = [] } = this.state; + + return ( +
+ +
+ +
+ {activeData && ( +
+

{[...activeData].sort()[activeData.length - 1].y + 200} 亿元

+

{[...activeData].sort()[Math.floor(activeData.length / 2)].y} 亿元

+
+ )} + {activeData && ( +
+ 00:00 + {activeData[Math.floor(activeData.length / 2)].x} + {activeData[activeData.length - 1].x} +
+ )} +
+ ); + } +} diff --git a/src/components/ActiveChart/index.less b/src/components/ActiveChart/index.less new file mode 100644 index 0000000..8ecc323 --- /dev/null +++ b/src/components/ActiveChart/index.less @@ -0,0 +1,31 @@ +.activeChart { + position: relative; +} +.activeChartGrid { + p { + position: absolute; + top: 80px; + } + p:last-child { + top: 115px; + } +} +.activeChartLegend { + position: relative; + font-size: 0; + margin-top: 8px; + height: 20px; + line-height: 20px; + span { + display: inline-block; + font-size: 12px; + text-align: center; + width: 33.33%; + } + span:first-child { + text-align: left; + } + span:last-child { + text-align: right; + } +} diff --git a/src/components/Authorized/Authorized.js b/src/components/Authorized/Authorized.js new file mode 100644 index 0000000..8e7aacb --- /dev/null +++ b/src/components/Authorized/Authorized.js @@ -0,0 +1,12 @@ +import React from 'react'; +import CheckPermissions from './CheckPermissions'; + +class Authorized extends React.Component { + render() { + const { children, authority, noMatch = null } = this.props; + const childrenRender = typeof children === 'undefined' ? null : children; + return CheckPermissions(authority, childrenRender, noMatch); + } +} + +export default Authorized; diff --git a/src/components/Authorized/AuthorizedRoute.js b/src/components/Authorized/AuthorizedRoute.js new file mode 100644 index 0000000..60b45dc --- /dev/null +++ b/src/components/Authorized/AuthorizedRoute.js @@ -0,0 +1,19 @@ +import React from 'react'; +import { Route, Redirect } from 'react-router-dom'; +import Authorized from './Authorized'; + +class AuthorizedRoute extends React.Component { + render() { + const { component: Component, render, authority, redirectPath, ...rest } = this.props; + return ( + } />} + > + (Component ? : render(props))} /> + + ); + } +} + +export default AuthorizedRoute; diff --git a/src/components/Authorized/CheckPermissions.js b/src/components/Authorized/CheckPermissions.js new file mode 100644 index 0000000..d51e983 --- /dev/null +++ b/src/components/Authorized/CheckPermissions.js @@ -0,0 +1,69 @@ +import React from 'react'; +import PromiseRender from './PromiseRender'; +import { CURRENT } from './index'; + +function isPromise(obj) { + return ( + !!obj && + (typeof obj === 'object' || typeof obj === 'function') && + typeof obj.then === 'function' + ); +} + +/** + * 通用权限检查方法 + * Common check permissions method + * @param { 权限判定 Permission judgment type string |array | Promise | Function } authority + * @param { 你的权限 Your permission description type:string} currentAuthority + * @param { 通过的组件 Passing components } target + * @param { 未通过的组件 no pass components } Exception + */ +const checkPermissions = (authority, currentAuthority, target, Exception) => { + // 没有判定权限.默认查看所有 + // Retirement authority, return target; + if (!authority) { + return target; + } + // 数组处理 + if (Array.isArray(authority)) { + if (authority.indexOf(currentAuthority) >= 0) { + return target; + } + return Exception; + } + + // string 处理 + if (typeof authority === 'string') { + if (authority === currentAuthority) { + return target; + } + return Exception; + } + + // Promise 处理 + if (isPromise(authority)) { + return ; + } + + // Function 处理 + if (typeof authority === 'function') { + try { + const bool = authority(currentAuthority); + if (bool) { + return target; + } + return Exception; + } catch (error) { + throw error; + } + } + throw new Error('unsupported parameters'); +}; + +export { checkPermissions }; + +const check = (authority, target, Exception) => { + return checkPermissions(authority, CURRENT, target, Exception); +}; + +export default check; diff --git a/src/components/Authorized/CheckPermissions.test.js b/src/components/Authorized/CheckPermissions.test.js new file mode 100644 index 0000000..b4b5e2c --- /dev/null +++ b/src/components/Authorized/CheckPermissions.test.js @@ -0,0 +1,37 @@ +import { checkPermissions } from './CheckPermissions.js'; + +const target = 'ok'; +const error = 'error'; + +describe('test CheckPermissions', () => { + it('Correct string permission authentication', () => { + expect(checkPermissions('user', 'user', target, error)).toEqual('ok'); + }); + it('Correct string permission authentication', () => { + expect(checkPermissions('user', 'NULL', target, error)).toEqual('error'); + }); + it('authority is undefined , return ok', () => { + expect(checkPermissions(null, 'NULL', target, error)).toEqual('ok'); + }); + it('currentAuthority is undefined , return error', () => { + expect(checkPermissions('admin', null, target, error)).toEqual('error'); + }); + it('Wrong string permission authentication', () => { + expect(checkPermissions('admin', 'user', target, error)).toEqual('error'); + }); + it('Correct Array permission authentication', () => { + expect(checkPermissions(['user', 'admin'], 'user', target, error)).toEqual('ok'); + }); + it('Wrong Array permission authentication,currentAuthority error', () => { + expect(checkPermissions(['user', 'admin'], 'user,admin', target, error)).toEqual('error'); + }); + it('Wrong Array permission authentication', () => { + expect(checkPermissions(['user', 'admin'], 'guest', target, error)).toEqual('error'); + }); + it('Wrong Function permission authentication', () => { + expect(checkPermissions(() => false, 'guest', target, error)).toEqual('error'); + }); + it('Correct Function permission authentication', () => { + expect(checkPermissions(() => true, 'guest', target, error)).toEqual('ok'); + }); +}); diff --git a/src/components/Authorized/PromiseRender.js b/src/components/Authorized/PromiseRender.js new file mode 100644 index 0000000..02518ad --- /dev/null +++ b/src/components/Authorized/PromiseRender.js @@ -0,0 +1,59 @@ +import React from 'react'; +import { Spin } from 'antd'; + +export default class PromiseRender extends React.PureComponent { + state = { + component: null, + }; + componentDidMount() { + this.setRenderComponent(this.props); + } + componentWillReceiveProps(nextProps) { + // new Props enter + this.setRenderComponent(nextProps); + } + // set render Component : ok or error + setRenderComponent(props) { + const ok = this.checkIsInstantiation(props.ok); + const error = this.checkIsInstantiation(props.error); + props.promise + .then(() => { + this.setState({ + component: ok, + }); + }) + .catch(() => { + this.setState({ + component: error, + }); + }); + } + // Determine whether the incoming component has been instantiated + // AuthorizedRoute is already instantiated + // Authorized render is already instantiated, children is no instantiated + // Secured is not instantiated + checkIsInstantiation = target => { + if (!React.isValidElement(target)) { + return target; + } + return () => target; + }; + render() { + const Component = this.state.component; + return Component ? ( + + ) : ( +
+ +
+ ); + } +} diff --git a/src/components/Authorized/Secured.js b/src/components/Authorized/Secured.js new file mode 100644 index 0000000..1012da4 --- /dev/null +++ b/src/components/Authorized/Secured.js @@ -0,0 +1,55 @@ +import React from 'react'; +import Exception from '../Exception/index'; +import CheckPermissions from './CheckPermissions'; +/** + * 默认不能访问任何页面 + * default is "NULL" + */ +const Exception403 = () => ; + +// Determine whether the incoming component has been instantiated +// AuthorizedRoute is already instantiated +// Authorized render is already instantiated, children is no instantiated +// Secured is not instantiated +const checkIsInstantiation = target => { + if (!React.isValidElement(target)) { + return target; + } + return () => target; +}; + +/** + * 用于判断是否拥有权限访问此view权限 + * authority 支持传入 string ,funtion:()=>boolean|Promise + * e.g. 'user' 只有user用户能访问 + * e.g. 'user,admin' user和 admin 都能访问 + * e.g. ()=>boolean 返回true能访问,返回false不能访问 + * e.g. Promise then 能访问 catch不能访问 + * e.g. authority support incoming string, funtion: () => boolean | Promise + * e.g. 'user' only user user can access + * e.g. 'user, admin' user and admin can access + * e.g. () => boolean true to be able to visit, return false can not be accessed + * e.g. Promise then can not access the visit to catch + * @param {string | function | Promise} authority + * @param {ReactNode} error 非必需参数 + */ +const authorize = (authority, error) => { + /** + * conversion into a class + * 防止传入字符串时找不到staticContext造成报错 + * String parameters can cause staticContext not found error + */ + let classError = false; + if (error) { + classError = () => error; + } + if (!authority) { + throw new Error('authority is required'); + } + return function decideAuthority(targer) { + const component = CheckPermissions(authority, targer, classError || Exception403); + return checkIsInstantiation(component); + }; +}; + +export default authorize; diff --git a/src/components/Authorized/demo/AuthorizedArray.md b/src/components/Authorized/demo/AuthorizedArray.md new file mode 100644 index 0000000..46eaf76 --- /dev/null +++ b/src/components/Authorized/demo/AuthorizedArray.md @@ -0,0 +1,23 @@ +--- +order: 1 +title: + zh-CN: 使用数组作为参数 + en-US: Use Array as a parameter +--- + +Use Array as a parameter + +```jsx +import RenderAuthorized from 'ant-design-pro/lib/Authorized'; +import { Alert } from 'antd'; + +const Authorized = RenderAuthorized('user'); +const noMatch = ; + +ReactDOM.render( + + + , + mountNode, +); +``` diff --git a/src/components/Authorized/demo/AuthorizedFunction.md b/src/components/Authorized/demo/AuthorizedFunction.md new file mode 100644 index 0000000..8ad8b91 --- /dev/null +++ b/src/components/Authorized/demo/AuthorizedFunction.md @@ -0,0 +1,31 @@ +--- +order: 2 +title: + zh-CN: 使用方法作为参数 + en-US: Use function as a parameter +--- + +Use Function as a parameter + +```jsx +import RenderAuthorized from 'ant-design-pro/lib/Authorized'; +import { Alert } from 'antd'; + +const Authorized = RenderAuthorized('user'); +const noMatch = ; + +const havePermission = () => { + return false; +}; + +ReactDOM.render( + + + , + mountNode, +); +``` diff --git a/src/components/Authorized/demo/basic.md b/src/components/Authorized/demo/basic.md new file mode 100644 index 0000000..a5f12f2 --- /dev/null +++ b/src/components/Authorized/demo/basic.md @@ -0,0 +1,25 @@ +--- +order: 0 +title: + zh-CN: 基本使用 + en-US: Basic use +--- + +Basic use + +```jsx +import RenderAuthorized from 'ant-design-pro/lib/Authorized'; +import { Alert } from 'antd'; + +const Authorized = RenderAuthorized('user'); +const noMatch = ; + +ReactDOM.render( +
+ + + +
, + mountNode, +); +``` diff --git a/src/components/Authorized/demo/secured.md b/src/components/Authorized/demo/secured.md new file mode 100644 index 0000000..1e9537a --- /dev/null +++ b/src/components/Authorized/demo/secured.md @@ -0,0 +1,28 @@ +--- +order: 3 +title: + zh-CN: 注解基本使用 + en-US: Basic use secured +--- + +secured demo used + +```jsx +import RenderAuthorized from 'ant-design-pro/lib/Authorized'; +import { Alert } from 'antd'; + +const { Secured } = RenderAuthorized('user'); + +@Secured('admin') +class TestSecuredString extends React.Component { + render() { + ; + } +} +ReactDOM.render( +
+ +
, + mountNode, +); +``` diff --git a/src/components/Authorized/index.d.ts b/src/components/Authorized/index.d.ts new file mode 100644 index 0000000..ba9bc5b --- /dev/null +++ b/src/components/Authorized/index.d.ts @@ -0,0 +1,43 @@ +import * as React from 'react'; +import { RouteProps } from 'react-router'; + +type authorityFN = (currentAuthority?: string) => boolean; + +type authority = string | Array | authorityFN | Promise; + +export type IReactComponent

= + | React.StatelessComponent

+ | React.ComponentClass

+ | React.ClassicComponentClass

; + +interface Secured { + (authority: authority, error?: React.ReactNode): (target: T) => T; +} + +export interface AuthorizedRouteProps extends RouteProps { + authority: authority; +} +export class AuthorizedRoute extends React.Component {} + +interface check { + ( + authority: authority, + target: T, + Exception: S + ): T | S; +} + +interface AuthorizedProps { + authority: authority; + noMatch?: React.ReactNode; +} + +export class Authorized extends React.Component { + static Secured: Secured; + static AuthorizedRoute: typeof AuthorizedRoute; + static check: check; +} + +declare function renderAuthorize(currentAuthority: string): typeof Authorized; + +export default renderAuthorize; diff --git a/src/components/Authorized/index.js b/src/components/Authorized/index.js new file mode 100644 index 0000000..48d1b2b --- /dev/null +++ b/src/components/Authorized/index.js @@ -0,0 +1,32 @@ +import Authorized from './Authorized'; +import AuthorizedRoute from './AuthorizedRoute'; +import Secured from './Secured'; +import check from './CheckPermissions.js'; + +/* eslint-disable import/no-mutable-exports */ +let CURRENT = 'NULL'; + +Authorized.Secured = Secured; +Authorized.AuthorizedRoute = AuthorizedRoute; +Authorized.check = check; + +/** + * use authority or getAuthority + * @param {string|()=>String} currentAuthority + */ +const renderAuthorize = currentAuthority => { + if (currentAuthority) { + if (currentAuthority.constructor.name === 'Function') { + CURRENT = currentAuthority(); + } + if (currentAuthority.constructor.name === 'String') { + CURRENT = currentAuthority; + } + } else { + CURRENT = 'NULL'; + } + return Authorized; +}; + +export { CURRENT }; +export default renderAuthorize; diff --git a/src/components/Authorized/index.md b/src/components/Authorized/index.md new file mode 100644 index 0000000..51588bb --- /dev/null +++ b/src/components/Authorized/index.md @@ -0,0 +1,58 @@ +--- +title: + en-US: Authorized + zh-CN: Authorized +subtitle: 权限 +cols: 1 +order: 15 +--- + +权限组件,通过比对现有权限与准入权限,决定相关元素的展示。 + +## API + +### RenderAuthorized + +`RenderAuthorized: (currentAuthority: string | () => string) => Authorized` + +权限组件默认 export RenderAuthorized 函数,它接收当前权限作为参数,返回一个权限对象,该对象提供以下几种使用方式。 + + +### Authorized + +最基础的权限控制。 + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| children | 正常渲染的元素,权限判断通过时展示 | ReactNode | - | +| authority | 准入权限/权限判断 | `string | array | Promise | (currentAuthority) => boolean` | - | +| noMatch | 权限异常渲染元素,权限判断不通过时展示 | ReactNode | - | + +### Authorized.AuthorizedRoute + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| authority | 准入权限/权限判断 | `string | array | Promise | (currentAuthority) => boolean` | - | +| redirectPath | 权限异常时重定向的页面路由 | string | - | + +其余参数与 `Route` 相同。 + +### Authorized.Secured + +注解方式,`@Authorized.Secured(authority, error)` + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| authority | 准入权限/权限判断 | `string | Promise | (currentAuthority) => boolean` | - | +| error | 权限异常时渲染元素 | ReactNode | | + +### Authorized.check + +函数形式的 Authorized,用于某些不能被 HOC 包裹的组件。 `Authorized.check(authority, target, Exception)` +注意:传入一个 Promise 时,无论正确还是错误返回的都是一个 ReactClass。 + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| authority | 准入权限/权限判断 | `string | Promise | (currentAuthority) => boolean` | - | +| target | 权限判断通过时渲染的元素 | ReactNode | - | +| Exception | 权限异常时渲染元素 | ReactNode | - | diff --git a/src/components/AvatarList/AvatarItem.d.ts b/src/components/AvatarList/AvatarItem.d.ts new file mode 100644 index 0000000..5681de7 --- /dev/null +++ b/src/components/AvatarList/AvatarItem.d.ts @@ -0,0 +1,10 @@ +import * as React from 'react'; +export interface IAvatarItemProps { + tips: React.ReactNode; + src: string; + style?: React.CSSProperties; +} + +export default class AvatarItem extends React.Component { + constructor(props: IAvatarItemProps); +} diff --git a/src/components/AvatarList/demo/simple.md b/src/components/AvatarList/demo/simple.md new file mode 100644 index 0000000..e941aea --- /dev/null +++ b/src/components/AvatarList/demo/simple.md @@ -0,0 +1,20 @@ +--- +order: 0 +title: + zh-CN: 基础样例 + en-US: Basic Usage +--- + +Simplest of usage. + +````jsx +import AvatarList from 'ant-design-pro/lib/AvatarList'; + +ReactDOM.render( + + + + + +, mountNode); +```` diff --git a/src/components/AvatarList/index.d.ts b/src/components/AvatarList/index.d.ts new file mode 100644 index 0000000..5b9352e --- /dev/null +++ b/src/components/AvatarList/index.d.ts @@ -0,0 +1,12 @@ +import * as React from 'react'; +import AvatarItem from './AvatarItem'; + +export interface IAvatarListProps { + size?: 'large' | 'small' | 'mini' | 'default'; + style?: React.CSSProperties; + children: React.ReactElement | Array>; +} + +export default class AvatarList extends React.Component { + public static Item: typeof AvatarItem; +} diff --git a/src/components/AvatarList/index.en-US.md b/src/components/AvatarList/index.en-US.md new file mode 100644 index 0000000..58daaf9 --- /dev/null +++ b/src/components/AvatarList/index.en-US.md @@ -0,0 +1,22 @@ +--- +title: AvatarList +order: 1 +cols: 1 +--- + +A list of user's avatar for project or group member list frequently. If a large or small AvatarList is desired, set the `size` property to either `large` or `small` and `mini` respectively. Omit the `size` property for a AvatarList with the default size. + +## API + +### AvatarList + +| Property | Description | Type | Default | +|----------|------------------------------------------|-------------|-------| +| size | size of list | `large`、`small` 、`mini`, `default` | `default` | + +### AvatarList.Item + +| Property | Description | Type | Default | +|----------|------------------------------------------|-------------|-------| +| tips | title tips for avatar item | ReactNode\/string | - | +| src | the address of the image for an image avatar | string | - | diff --git a/src/components/AvatarList/index.js b/src/components/AvatarList/index.js new file mode 100644 index 0000000..6fc5927 --- /dev/null +++ b/src/components/AvatarList/index.js @@ -0,0 +1,43 @@ +import React from 'react'; +import { Tooltip, Avatar } from 'antd'; +import classNames from 'classnames'; + +import styles from './index.less'; + +const AvatarList = ({ children, size, ...other }) => { + const childrenWithProps = React.Children.map(children, child => + React.cloneElement(child, { + size, + }) + ); + + return ( +

+
    {childrenWithProps}
+
+ ); +}; + +const Item = ({ src, size, tips, onClick = () => {} }) => { + const cls = classNames(styles.avatarItem, { + [styles.avatarItemLarge]: size === 'large', + [styles.avatarItemSmall]: size === 'small', + [styles.avatarItemMini]: size === 'mini', + }); + + return ( +
  • + {tips ? ( + + + + ) : ( + + )} +
  • + ); +}; + +AvatarList.Item = Item; + +export default AvatarList; diff --git a/src/components/AvatarList/index.less b/src/components/AvatarList/index.less new file mode 100644 index 0000000..8660ba4 --- /dev/null +++ b/src/components/AvatarList/index.less @@ -0,0 +1,45 @@ +@import '~antd/lib/style/themes/default.less'; + +.avatarList { + display: inline-block; + ul { + display: inline-block; + margin-left: 8px; + font-size: 0; + } +} + +.avatarItem { + display: inline-block; + font-size: @font-size-base; + margin-left: -8px; + width: @avatar-size-base; + height: @avatar-size-base; + :global { + .ant-avatar { + border: 1px solid #fff; + } + } +} + +.avatarItemLarge { + width: @avatar-size-lg; + height: @avatar-size-lg; +} + +.avatarItemSmall { + width: @avatar-size-sm; + height: @avatar-size-sm; +} + +.avatarItemMini { + width: 20px; + height: 20px; + :global { + .ant-avatar { + width: 20px; + height: 20px; + line-height: 20px; + } + } +} diff --git a/src/components/AvatarList/index.zh-CN.md b/src/components/AvatarList/index.zh-CN.md new file mode 100644 index 0000000..c229a5d --- /dev/null +++ b/src/components/AvatarList/index.zh-CN.md @@ -0,0 +1,23 @@ +--- +title: AvatarList +subtitle: 用户头像列表 +order: 1 +cols: 1 +--- + +一组用户头像,常用在项目/团队成员列表。可通过设置 `size` 属性来指定头像大小。 + +## API + +### AvatarList + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| size | 头像大小 | `large`、`small` 、`mini`, `default` | `default` | + +### AvatarList.Item + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| tips | 头像展示文案 | ReactNode\/string | - | +| src | 头像图片连接 | string | - | diff --git a/src/components/Charts/Bar/index.d.ts b/src/components/Charts/Bar/index.d.ts new file mode 100644 index 0000000..4899082 --- /dev/null +++ b/src/components/Charts/Bar/index.d.ts @@ -0,0 +1,15 @@ +import * as React from 'react'; +export interface IBarProps { + title: React.ReactNode; + color?: string; + padding?: [number, number, number, number]; + height: number; + data: Array<{ + x: string; + y: number; + }>; + autoLabel?: boolean; + style?: React.CSSProperties; +} + +export default class Bar extends React.Component {} diff --git a/src/components/Charts/Bar/index.js b/src/components/Charts/Bar/index.js new file mode 100644 index 0000000..a3c3608 --- /dev/null +++ b/src/components/Charts/Bar/index.js @@ -0,0 +1,113 @@ +import React, { Component } from 'react'; +import { Chart, Axis, Tooltip, Geom } from 'bizcharts'; +import Debounce from 'lodash-decorators/debounce'; +import Bind from 'lodash-decorators/bind'; +import autoHeight from '../autoHeight'; +import styles from '../index.less'; + +@autoHeight() +class Bar extends Component { + state = { + autoHideXLabels: false, + }; + + componentDidMount() { + window.addEventListener('resize', this.resize); + } + + componentWillUnmount() { + window.removeEventListener('resize', this.resize); + } + + @Bind() + @Debounce(400) + 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, + }); + } + } else if (autoHideXLabels) { + this.setState({ + autoHideXLabels: false, + }); + } + } + + handleRoot = n => { + this.root = n; + }; + + handleRef = n => { + this.node = n; + }; + + render() { + const { + height, + title, + forceFit = true, + data, + color = 'rgba(24, 144, 255, 0.85)', + padding, + } = this.props; + + const { autoHideXLabels } = this.state; + + const scale = { + x: { + type: 'cat', + }, + y: { + min: 0, + }, + }; + + const tooltip = [ + 'x*y', + (x, y) => ({ + name: x, + value: y, + }), + ]; + + return ( +
    +
    + {title &&

    {title}

    } + + + + + + +
    +
    + ); + } +} + +export default Bar; diff --git a/src/components/Charts/ChartCard/index.d.ts b/src/components/Charts/ChartCard/index.d.ts new file mode 100644 index 0000000..80fd376 --- /dev/null +++ b/src/components/Charts/ChartCard/index.d.ts @@ -0,0 +1,12 @@ +import * as React from 'react'; +export interface IChartCardProps { + title: React.ReactNode; + action?: React.ReactNode; + total?: React.ReactNode | function | number; + footer?: React.ReactNode; + contentHeight?: number; + avatar?: React.ReactNode; + style?: React.CSSProperties; +} + +export default class ChartCard extends React.Component {} diff --git a/src/components/Charts/ChartCard/index.js b/src/components/Charts/ChartCard/index.js new file mode 100644 index 0000000..c3ee186 --- /dev/null +++ b/src/components/Charts/ChartCard/index.js @@ -0,0 +1,77 @@ +import React from 'react'; +import { Card, Spin } from 'antd'; +import classNames from 'classnames'; + +import styles from './index.less'; + +const renderTotal = total => { + let totalDom; + switch (typeof total) { + case 'undefined': + totalDom = null; + break; + case 'function': + totalDom =
    {total()}
    ; + break; + default: + totalDom =
    {total}
    ; + } + return totalDom; +}; + +const ChartCard = ({ + loading = false, + contentHeight, + title, + avatar, + action, + total, + footer, + children, + ...rest +}) => { + const content = ( +
    +
    +
    {avatar}
    +
    +
    + {title} + {action} +
    + {renderTotal(total)} +
    +
    + {children && ( +
    +
    {children}
    +
    + )} + {footer && ( +
    + {footer} +
    + )} +
    + ); + + return ( + + { + + {content} + + } + + ); +}; + +export default ChartCard; diff --git a/src/components/Charts/ChartCard/index.less b/src/components/Charts/ChartCard/index.less new file mode 100644 index 0000000..fa2eb16 --- /dev/null +++ b/src/components/Charts/ChartCard/index.less @@ -0,0 +1,78 @@ +@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; + } +} + +.spin :global(.ant-spin-container) { + overflow: visible; +} diff --git a/src/components/Charts/Field/index.d.ts b/src/components/Charts/Field/index.d.ts new file mode 100644 index 0000000..975fb66 --- /dev/null +++ b/src/components/Charts/Field/index.d.ts @@ -0,0 +1,8 @@ +import * as React from 'react'; +export interface IFieldProps { + label: React.ReactNode; + value: React.ReactNode; + style?: React.CSSProperties; +} + +export default class Field extends React.Component {} diff --git a/src/components/Charts/Field/index.js b/src/components/Charts/Field/index.js new file mode 100644 index 0000000..0f9ace2 --- /dev/null +++ b/src/components/Charts/Field/index.js @@ -0,0 +1,12 @@ +import React from 'react'; + +import styles from './index.less'; + +const Field = ({ label, value, ...rest }) => ( +
    + {label} + {value} +
    +); + +export default Field; diff --git a/src/components/Charts/Field/index.less b/src/components/Charts/Field/index.less new file mode 100644 index 0000000..aeafbcb --- /dev/null +++ b/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; + } +} diff --git a/src/components/Charts/Gauge/index.d.ts b/src/components/Charts/Gauge/index.d.ts new file mode 100644 index 0000000..66e3c00 --- /dev/null +++ b/src/components/Charts/Gauge/index.d.ts @@ -0,0 +1,11 @@ +import * as React from 'react'; +export interface IGaugeProps { + title: React.ReactNode; + color?: string; + height: number; + bgColor?: number; + percent: number; + style?: React.CSSProperties; +} + +export default class Gauge extends React.Component {} diff --git a/src/components/Charts/Gauge/index.js b/src/components/Charts/Gauge/index.js new file mode 100644 index 0000000..d9289ea --- /dev/null +++ b/src/components/Charts/Gauge/index.js @@ -0,0 +1,167 @@ +import React from 'react'; +import { Chart, Geom, Axis, Coord, Guide, Shape } from 'bizcharts'; +import autoHeight from '../autoHeight'; + +const { Arc, Html, Line } = Guide; + +const defaultFormatter = val => { + switch (val) { + case '2': + return '差'; + case '4': + return '中'; + case '6': + return '良'; + case '8': + return '优'; + default: + return ''; + } +}; + +Shape.registerShape('point', 'pointer', { + drawShape(cfg, group) { + let point = cfg.points[0]; + point = this.parsePoint(point); + const center = this.parsePoint({ + x: 0, + y: 0, + }); + group.addShape('line', { + attrs: { + x1: center.x, + y1: center.y, + x2: point.x, + y2: point.y, + stroke: cfg.color, + lineWidth: 2, + lineCap: 'round', + }, + }); + return group.addShape('circle', { + attrs: { + x: center.x, + y: center.y, + r: 6, + stroke: cfg.color, + lineWidth: 3, + fill: '#fff', + }, + }); + }, +}); + +@autoHeight() +export default class Gauge extends React.Component { + render() { + const { + title, + height, + percent, + forceFit = true, + formatter = defaultFormatter, + color = '#2F9CFF', + bgColor = '#F0F2F5', + } = this.props; + const cols = { + value: { + type: 'linear', + min: 0, + max: 10, + tickCount: 6, + nice: true, + }, + }; + const data = [{ value: percent / 10 }]; + return ( + + + + + + + + + + + { + return ` +
    +

    ${title}

    +

    + ${data[0].value * 10}% +

    +
    `; + }} + /> +
    + +
    + ); + } +} diff --git a/src/components/Charts/MiniArea/index.d.ts b/src/components/Charts/MiniArea/index.d.ts new file mode 100644 index 0000000..2bcac70 --- /dev/null +++ b/src/components/Charts/MiniArea/index.d.ts @@ -0,0 +1,29 @@ +import * as React from 'react'; + +// g2已经更新到3.0 +// 不带的写了 + +export interface IAxis { + title: any; + line: any; + gridAlign: any; + labels: any; + tickLine: any; + grid: any; +} + +export interface IMiniAreaProps { + color?: string; + height: number; + borderColor?: string; + line?: boolean; + animate?: boolean; + xAxis?: IAxis; + yAxis?: IAxis; + data: Array<{ + x: number; + y: number; + }>; +} + +export default class MiniArea extends React.Component {} diff --git a/src/components/Charts/MiniArea/index.js b/src/components/Charts/MiniArea/index.js new file mode 100644 index 0000000..a5526a9 --- /dev/null +++ b/src/components/Charts/MiniArea/index.js @@ -0,0 +1,106 @@ +import React from 'react'; +import { Chart, Axis, Tooltip, Geom } from 'bizcharts'; +import autoHeight from '../autoHeight'; +import styles from '../index.less'; + +@autoHeight() +export default class MiniArea extends React.Component { + render() { + const { + height, + data = [], + forceFit = true, + color = 'rgba(24, 144, 255, 0.2)', + borderColor = '#1089ff', + scale = {}, + borderWidth = 2, + line, + xAxis, + yAxis, + animate = true, + } = this.props; + + const padding = [36, 5, 30, 5]; + + const scaleProps = { + x: { + type: 'cat', + range: [0, 1], + ...scale.x, + }, + y: { + min: 0, + ...scale.y, + }, + }; + + const tooltip = [ + 'x*y', + (x, y) => ({ + name: x, + value: y, + }), + ]; + + const chartHeight = height + 54; + + return ( +
    +
    + {height > 0 && ( + + + + + + {line ? ( + + ) : ( + + )} + + )} +
    +
    + ); + } +} diff --git a/src/components/Charts/MiniBar/index.d.ts b/src/components/Charts/MiniBar/index.d.ts new file mode 100644 index 0000000..0c4bd6c --- /dev/null +++ b/src/components/Charts/MiniBar/index.d.ts @@ -0,0 +1,12 @@ +import * as React from 'react'; +export interface IMiniBarProps { + color?: string; + height: number; + data: Array<{ + x: number | string; + y: number; + }>; + style?: React.CSSProperties; +} + +export default class MiniBar extends React.Component {} diff --git a/src/components/Charts/MiniBar/index.js b/src/components/Charts/MiniBar/index.js new file mode 100644 index 0000000..92ee6b5 --- /dev/null +++ b/src/components/Charts/MiniBar/index.js @@ -0,0 +1,50 @@ +import React from 'react'; +import { Chart, Tooltip, Geom } from 'bizcharts'; +import autoHeight from '../autoHeight'; +import styles from '../index.less'; + +@autoHeight() +export default class MiniBar extends React.Component { + render() { + const { height, forceFit = true, color = '#1890FF', data = [] } = this.props; + + const scale = { + x: { + type: 'cat', + }, + y: { + min: 0, + }, + }; + + const padding = [36, 5, 30, 5]; + + const tooltip = [ + 'x*y', + (x, y) => ({ + name: x, + value: y, + }), + ]; + + // for tooltip not to be hide + const chartHeight = height + 54; + + return ( +
    +
    + + + + +
    +
    + ); + } +} diff --git a/src/components/Charts/MiniProgress/index.d.ts b/src/components/Charts/MiniProgress/index.d.ts new file mode 100644 index 0000000..aaeb726 --- /dev/null +++ b/src/components/Charts/MiniProgress/index.d.ts @@ -0,0 +1,10 @@ +import * as React from 'react'; +export interface IMiniProgressProps { + target: number; + color?: string; + strokeWidth?: number; + percent?: number; + style?: React.CSSProperties; +} + +export default class MiniProgress extends React.Component {} diff --git a/src/components/Charts/MiniProgress/index.js b/src/components/Charts/MiniProgress/index.js new file mode 100644 index 0000000..795c79b --- /dev/null +++ b/src/components/Charts/MiniProgress/index.js @@ -0,0 +1,27 @@ +import React from 'react'; +import { Tooltip } from 'antd'; + +import styles from './index.less'; + +const MiniProgress = ({ target, color = 'rgb(19, 194, 194)', strokeWidth, percent }) => ( +
    + +
    + + +
    +
    +
    +
    +
    +
    +); + +export default MiniProgress; diff --git a/src/components/Charts/MiniProgress/index.less b/src/components/Charts/MiniProgress/index.less new file mode 100644 index 0000000..e5f148c --- /dev/null +++ b/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 0.4s cubic-bezier(0.08, 0.82, 0.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; + } + } +} diff --git a/src/components/Charts/Pie/index.d.ts b/src/components/Charts/Pie/index.d.ts new file mode 100644 index 0000000..5b19208 --- /dev/null +++ b/src/components/Charts/Pie/index.d.ts @@ -0,0 +1,20 @@ +import * as React from 'react'; +export interface IPieProps { + animate?: boolean; + color?: string; + height: number; + hasLegend?: boolean; + padding?: [number, number, number, number]; + percent?: number; + data?: Array<{ + x: string | string; + y: number; + }>; + total?: string | function; + title?: React.ReactNode; + tooltip?: boolean; + valueFormat?: (value: string) => string | React.ReactNode; + subTitle?: React.ReactNode; +} + +export default class Pie extends React.Component {} diff --git a/src/components/Charts/Pie/index.js b/src/components/Charts/Pie/index.js new file mode 100644 index 0000000..0d0dc3c --- /dev/null +++ b/src/components/Charts/Pie/index.js @@ -0,0 +1,255 @@ +import React, { Component } from 'react'; +import { Chart, Tooltip, Geom, Coord } from 'bizcharts'; +import { DataView } from '@antv/data-set'; +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 autoHeight from '../autoHeight'; + +import styles from './index.less'; + +/* eslint react/no-danger:0 */ +@autoHeight() +export default class Pie extends Component { + state = { + legendData: [], + legendBlock: false, + }; + + componentDidMount() { + this.getLegendData(); + this.resize(); + window.addEventListener('resize', this.resize); + } + + componentWillReceiveProps(nextProps) { + if (this.props.data !== nextProps.data) { + // because of charts data create when rendered + // so there is a trick for get rendered time + this.setState( + { + legendData: [...this.state.legendData], + }, + () => { + this.getLegendData(); + } + ); + } + } + + componentWillUnmount() { + window.removeEventListener('resize', this.resize); + this.resize.cancel(); + } + + getG2Instance = chart => { + this.chart = chart; + }; + + // for custom lengend view + getLegendData = () => { + if (!this.chart) return; + const geom = this.chart.getAllGeoms()[0]; // 获取所有的图形 + const items = geom.get('dataArray') || []; // 获取图形对应的 + + const legendData = items.map(item => { + /* eslint no-underscore-dangle:0 */ + const origin = item[0]._origin; + origin.color = item[0].color; + origin.checked = true; + return origin; + }); + + this.setState({ + legendData, + }); + }; + + // for window resize auto responsive legend + @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, + }); + } + } else if (this.state.legendBlock) { + this.setState({ + legendBlock: false, + }); + } + } + + handleRoot = n => { + this.root = n; + }; + + handleLegendClick = (item, i) => { + const newItem = item; + newItem.checked = !newItem.checked; + + const { legendData } = this.state; + legendData[i] = newItem; + + const filteredLegendData = legendData.filter(l => l.checked).map(l => l.x); + + if (this.chart) { + this.chart.filter('x', val => filteredLegendData.indexOf(val) > -1); + } + + this.setState({ + legendData, + }); + }; + + render() { + const { + valueFormat, + subTitle, + total, + hasLegend = false, + className, + style, + height, + forceFit = true, + percent = 0, + color, + inner = 0.75, + animate = true, + colors, + lineWidth = 1, + } = this.props; + + const { legendData, legendBlock } = this.state; + const pieClassName = classNames(styles.pie, className, { + [styles.hasLegend]: !!hasLegend, + [styles.legendBlock]: legendBlock, + }); + + const defaultColors = colors; + let data = this.props.data || []; + let selected = this.props.selected || true; + let tooltip = this.props.tooltip || true; + let formatColor; + + const scale = { + x: { + type: 'cat', + range: [0, 1], + }, + y: { + min: 0, + }, + }; + + if (percent) { + selected = false; + tooltip = false; + formatColor = value => { + if (value === '占比') { + return color || 'rgba(24, 144, 255, 0.85)'; + } else { + return '#F0F2F5'; + } + }; + + data = [ + { + x: '占比', + y: parseFloat(percent), + }, + { + x: '反比', + y: 100 - parseFloat(percent), + }, + ]; + } + + const tooltipFormat = [ + 'x*percent', + (x, p) => ({ + name: x, + value: `${(p * 100).toFixed(2)}%`, + }), + ]; + + const padding = [12, 0, 12, 0]; + + const dv = new DataView(); + dv.source(data).transform({ + type: 'percent', + field: 'y', + dimension: 'x', + as: 'percent', + }); + + return ( +
    + +
    + + {!!tooltip && } + + + + + {(subTitle || total) && ( +
    + {subTitle &&

    {subTitle}

    } + {/* eslint-disable-next-line */} + {total && ( +
    {typeof total === 'function' ? total() : total}
    + )} +
    + )} +
    +
    + + {hasLegend && ( +
      + {legendData.map((item, i) => ( +
    • this.handleLegendClick(item, i)}> + + {item.x} + + + {`${(isNaN(item.percent) ? 0 : item.percent * 100).toFixed(2)}%`} + + {valueFormat ? valueFormat(item.y) : item.y} +
    • + ))} +
    + )} +
    + ); + } +} diff --git a/src/components/Charts/Pie/index.less b/src/components/Charts/Pie/index.less new file mode 100644 index 0000000..277274c --- /dev/null +++ b/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; + } +} diff --git a/src/components/Charts/Radar/index.d.ts b/src/components/Charts/Radar/index.d.ts new file mode 100644 index 0000000..963ac8c --- /dev/null +++ b/src/components/Charts/Radar/index.d.ts @@ -0,0 +1,15 @@ +import * as React from 'react'; +export interface IRadarProps { + title?: React.ReactNode; + height: number; + padding?: [number, number, number, number]; + hasLegend?: boolean; + data: Array<{ + name: string; + label: string; + value: string; + }>; + style?: React.CSSProperties; +} + +export default class Radar extends React.Component {} diff --git a/src/components/Charts/Radar/index.js b/src/components/Charts/Radar/index.js new file mode 100644 index 0000000..fc3ab44 --- /dev/null +++ b/src/components/Charts/Radar/index.js @@ -0,0 +1,180 @@ +import React, { Component } from 'react'; +import { Chart, Tooltip, Geom, Coord, Axis } from 'bizcharts'; +import { Row, Col } from 'antd'; +import autoHeight from '../autoHeight'; +import styles from './index.less'; + +/* eslint react/no-danger:0 */ +@autoHeight() +export default class Radar extends Component { + state = { + legendData: [], + }; + + componentDidMount() { + this.getLengendData(); + } + + componentWillReceiveProps(nextProps) { + if (this.props.data !== nextProps.data) { + this.getLengendData(); + } + } + + getG2Instance = chart => { + this.chart = chart; + }; + + // for custom lengend view + getLengendData = () => { + if (!this.chart) return; + const geom = this.chart.getAllGeoms()[0]; // 获取所有的图形 + const items = geom.get('dataArray') || []; // 获取图形对应的 + + const legendData = items.map(item => { + // eslint-disable-next-line + const origins = item.map(t => t._origin); + const result = { + name: origins[0].name, + color: item[0].color, + checked: true, + value: origins.reduce((p, n) => p + n.value, 0), + }; + + return result; + }); + + this.setState({ + legendData, + }); + }; + + handleRef = n => { + this.node = n; + }; + + handleLegendClick = (item, i) => { + const newItem = item; + newItem.checked = !newItem.checked; + + const { legendData } = this.state; + legendData[i] = newItem; + + const filteredLegendData = legendData.filter(l => l.checked).map(l => l.name); + + if (this.chart) { + this.chart.filter('name', val => filteredLegendData.indexOf(val) > -1); + this.chart.repaint(); + } + + this.setState({ + legendData, + }); + }; + + render() { + const defaultColors = [ + '#1890FF', + '#FACC14', + '#2FC25B', + '#8543E0', + '#F04864', + '#13C2C2', + '#fa8c16', + '#a0d911', + ]; + + const { + data = [], + height = 0, + title, + hasLegend = false, + forceFit = true, + tickCount = 4, + padding = [35, 30, 16, 30], + animate = true, + colors = defaultColors, + } = this.props; + + const { legendData } = this.state; + + const scale = { + value: { + min: 0, + tickCount, + }, + }; + + const chartHeight = height - (hasLegend ? 80 : 22); + + return ( +
    + {title &&

    {title}

    } + + + + + + + + + {hasLegend && ( + + {legendData.map((item, i) => ( + this.handleLegendClick(item, i)} + > +
    +

    + + {item.name} +

    +
    {item.value}
    +
    + + ))} +
    + )} +
    + ); + } +} diff --git a/src/components/Charts/Radar/index.less b/src/components/Charts/Radar/index.less new file mode 100644 index 0000000..15b8725 --- /dev/null +++ b/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; + } + } +} diff --git a/src/components/Charts/TagCloud/index.d.ts b/src/components/Charts/TagCloud/index.d.ts new file mode 100644 index 0000000..462650c --- /dev/null +++ b/src/components/Charts/TagCloud/index.d.ts @@ -0,0 +1,11 @@ +import * as React from 'react'; +export interface ITagCloudProps { + data: Array<{ + name: string; + value: number; + }>; + height: number; + style?: React.CSSProperties; +} + +export default class TagCloud extends React.Component {} diff --git a/src/components/Charts/TagCloud/index.js b/src/components/Charts/TagCloud/index.js new file mode 100644 index 0000000..08418ea --- /dev/null +++ b/src/components/Charts/TagCloud/index.js @@ -0,0 +1,164 @@ +import React, { Component } from 'react'; +import { Chart, Geom, Coord, Shape } from 'bizcharts'; +import DataSet from '@antv/data-set'; +import Debounce from 'lodash-decorators/debounce'; +import Bind from 'lodash-decorators/bind'; +import classNames from 'classnames'; +import autoHeight from '../autoHeight'; +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'; + +@autoHeight() +class TagCloud extends Component { + state = { + dv: null, + }; + + componentDidMount() { + this.initTagCloud(); + this.renderChart(); + window.addEventListener('resize', this.resize); + } + + componentWillReceiveProps(nextProps) { + if (JSON.stringify(nextProps.data) !== JSON.stringify(this.props.data)) { + this.renderChart(nextProps); + } + } + + componentWillUnmount() { + this.isUnmount = true; + window.removeEventListener('resize', this.resize); + } + + resize = () => { + this.renderChart(); + }; + + saveRootRef = node => { + this.root = node; + }; + + initTagCloud = () => { + function getTextAttrs(cfg) { + return Object.assign( + {}, + { + fillOpacity: cfg.opacity, + fontSize: cfg.origin._origin.size, + rotate: cfg.origin._origin.rotate, + text: cfg.origin._origin.text, + textAlign: 'center', + fontFamily: cfg.origin._origin.font, + fill: cfg.color, + textBaseline: 'Alphabetic', + }, + cfg.style + ); + } + + // 给point注册一个词云的shape + Shape.registerShape('point', 'cloud', { + drawShape(cfg, container) { + const attrs = getTextAttrs(cfg); + return container.addShape('text', { + attrs: Object.assign(attrs, { + x: cfg.x, + y: cfg.y, + }), + }); + }, + }); + }; + + @Bind() + @Debounce(500) + renderChart(nextProps) { + // const colors = ['#1890FF', '#41D9C7', '#2FC25B', '#FACC14', '#9AE65C']; + const { data, height } = nextProps || this.props; + + if (data.length < 1 || !this.root) { + return; + } + + const h = height * 4; + const w = this.root.offsetWidth * 4; + + const onload = () => { + const dv = new DataSet.View().source(data); + const range = dv.range('value'); + const [min, max] = range; + dv.transform({ + type: 'tag-cloud', + fields: ['name', 'value'], + imageMask: this.imageMask, + font: 'Verdana', + size: [w, h], // 宽高设置最好根据 imageMask 做调整 + padding: 5, + timeInterval: 5000, // max execute time + rotate() { + return 0; + }, + fontSize(d) { + // eslint-disable-next-line + return Math.pow((d.value - min) / (max - min), 2) * (70 - 20) + 20; + }, + }); + + if (this.isUnmount) { + return; + } + + this.setState({ + dv, + w, + h, + }); + }; + + if (!this.imageMask) { + this.imageMask = new Image(); + this.imageMask.crossOrigin = ''; + this.imageMask.src = imgUrl; + + this.imageMask.onload = onload; + } else { + onload(); + } + } + + render() { + const { className, height } = this.props; + const { dv, w, h } = this.state; + + return ( +
    + {dv && ( + + + + + )} +
    + ); + } +} + +export default TagCloud; diff --git a/src/components/Charts/TagCloud/index.less b/src/components/Charts/TagCloud/index.less new file mode 100644 index 0000000..f5c12ad --- /dev/null +++ b/src/components/Charts/TagCloud/index.less @@ -0,0 +1,7 @@ +.tagCloud { + overflow: hidden; + canvas { + transform: scale(0.25); + transform-origin: 0 0; + } +} diff --git a/src/components/Charts/TimelineChart/index.d.ts b/src/components/Charts/TimelineChart/index.d.ts new file mode 100644 index 0000000..d9312fe --- /dev/null +++ b/src/components/Charts/TimelineChart/index.d.ts @@ -0,0 +1,14 @@ +import * as React from 'react'; +export interface ITimelineChartProps { + data: Array<{ + x: string; + y1: string; + y2: string; + }>; + titleMap: { y1: string; y2: string }; + padding?: [number, number, number, number]; + height?: number; + style?: React.CSSProperties; +} + +export default class TimelineChart extends React.Component {} diff --git a/src/components/Charts/TimelineChart/index.js b/src/components/Charts/TimelineChart/index.js new file mode 100644 index 0000000..e7ab6b1 --- /dev/null +++ b/src/components/Charts/TimelineChart/index.js @@ -0,0 +1,123 @@ +import React from 'react'; +import { Chart, Tooltip, Geom, Legend, Axis } from 'bizcharts'; +import DataSet from '@antv/data-set'; +import Slider from 'bizcharts-plugin-slider'; +import autoHeight from '../autoHeight'; +import styles from './index.less'; + +@autoHeight() +export default class TimelineChart extends React.Component { + render() { + const { + title, + height = 400, + padding = [60, 20, 40, 40], + titleMap = { + y1: 'y1', + y2: 'y2', + }, + borderWidth = 2, + data = [ + { + x: 0, + y1: 0, + y2: 0, + }, + ], + } = this.props; + + data.sort((a, b) => a.x - b.x); + + 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 + ); + } + + const ds = new DataSet({ + state: { + start: data[0].x, + end: data[data.length - 1].x, + }, + }); + + const dv = ds.createView(); + dv + .source(data) + .transform({ + type: 'filter', + callback: obj => { + const date = obj.x; + return date <= ds.state.end && date >= ds.state.start; + }, + }) + .transform({ + type: 'map', + callback(row) { + const newRow = { ...row }; + newRow[titleMap.y1] = row.y1; + newRow[titleMap.y2] = row.y2; + return newRow; + }, + }) + .transform({ + type: 'fold', + fields: [titleMap.y1, titleMap.y2], // 展开字段集 + key: 'key', // key字段 + value: 'value', // value字段 + }); + + const timeScale = { + type: 'time', + tickInterval: 60 * 60 * 1000, + mask: 'HH:mm', + range: [0, 1], + }; + + const cols = { + x: timeScale, + value: { + max, + min: 0, + }, + }; + + const SliderGen = () => ( + { + ds.setState('start', startValue); + ds.setState('end', endValue); + }} + /> + ); + + return ( +
    +
    + {title &&

    {title}

    } + + + + + + +
    + +
    +
    +
    + ); + } +} diff --git a/src/components/Charts/TimelineChart/index.less b/src/components/Charts/TimelineChart/index.less new file mode 100644 index 0000000..1751975 --- /dev/null +++ b/src/components/Charts/TimelineChart/index.less @@ -0,0 +1,3 @@ +.timelineChart { + background: #fff; +} diff --git a/src/components/Charts/WaterWave/index.d.ts b/src/components/Charts/WaterWave/index.d.ts new file mode 100644 index 0000000..8f5588d --- /dev/null +++ b/src/components/Charts/WaterWave/index.d.ts @@ -0,0 +1,10 @@ +import * as React from 'react'; +export interface IWaterWaveProps { + title: React.ReactNode; + color?: string; + height: number; + percent: number; + style?: React.CSSProperties; +} + +export default class WaterWave extends React.Component {} diff --git a/src/components/Charts/WaterWave/index.js b/src/components/Charts/WaterWave/index.js new file mode 100644 index 0000000..5b463ad --- /dev/null +++ b/src/components/Charts/WaterWave/index.js @@ -0,0 +1,197 @@ +import React, { PureComponent } from 'react'; +import autoHeight from '../autoHeight'; +import styles from './index.less'; + +/* eslint no-return-assign: 0 */ +/* eslint no-mixed-operators: 0 */ +// riddle: https://riddle.alibaba-inc.com/riddles/2d9a4b90 + +@autoHeight() +export default class WaterWave extends PureComponent { + 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 ( +
    (this.root = n)} + style={{ transform: `scale(${radio})` }} + > +
    + (this.node = n)} + width={height * 2} + height={height * 2} + /> +
    +
    + {title && {title}} +

    {percent}%

    +
    +
    + ); + } +} diff --git a/src/components/Charts/WaterWave/index.less b/src/components/Charts/WaterWave/index.less new file mode 100644 index 0000000..43ba05c --- /dev/null +++ b/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(0.5); + transform-origin: 0 0; + } +} diff --git a/src/components/Charts/autoHeight.js b/src/components/Charts/autoHeight.js new file mode 100644 index 0000000..01ae92d --- /dev/null +++ b/src/components/Charts/autoHeight.js @@ -0,0 +1,63 @@ +/* eslint eqeqeq: 0 */ +import React from 'react'; + +function computeHeight(node) { + const totalHeight = parseInt(getComputedStyle(node).height, 10); + const padding = + parseInt(getComputedStyle(node).paddingTop, 10) + + parseInt(getComputedStyle(node).paddingBottom, 10); + return totalHeight - padding; +} + +function getAutoHeight(n) { + if (!n) { + return 0; + } + + let node = n; + + let height = computeHeight(node); + + while (!height) { + node = node.parentNode; + if (node) { + height = computeHeight(node); + } else { + break; + } + } + + return height; +} + +const autoHeight = () => WrappedComponent => { + return class extends React.Component { + state = { + computedHeight: 0, + }; + + componentDidMount() { + const { height } = this.props; + if (!height) { + const h = getAutoHeight(this.root); + // eslint-disable-next-line + this.setState({ computedHeight: h }); + } + } + + handleRoot = node => { + this.root = node; + }; + + render() { + const { height } = this.props; + const { computedHeight } = this.state; + const h = height || computedHeight; + return ( +
    {h > 0 && }
    + ); + } + }; +}; + +export default autoHeight; diff --git a/src/components/Charts/demo/bar.md b/src/components/Charts/demo/bar.md new file mode 100644 index 0000000..955f44e --- /dev/null +++ b/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( + +, mountNode); +```` diff --git a/src/components/Charts/demo/chart-card.md b/src/components/Charts/demo/chart-card.md new file mode 100644 index 0000000..4da852b --- /dev/null +++ b/src/components/Charts/demo/chart-card.md @@ -0,0 +1,95 @@ +--- +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( + + + + + + } + total={() => ( + + )} + footer={ + + } + contentHeight={46} + > + + 周同比 + + 12% + + + + 日环比 + + 11% + + + + + + + } + action={ + + + + } + total={() => ( + + )} + footer={ + + } + /> + + + + } + action={ + + + + } + total={() => ( + + )} + /> + + , + mountNode, +); +``` diff --git a/src/components/Charts/demo/gauge.md b/src/components/Charts/demo/gauge.md new file mode 100644 index 0000000..f53465d --- /dev/null +++ b/src/components/Charts/demo/gauge.md @@ -0,0 +1,18 @@ +--- +order: 7 +title: 仪表盘 +--- + +仪表盘是一种进度展示方式,可以更直观的展示当前的进展情况,通常也可表示占比。 + +````jsx +import { Gauge } from 'ant-design-pro/lib/Charts'; + +ReactDOM.render( + +, mountNode); +```` diff --git a/src/components/Charts/demo/mini-area.md b/src/components/Charts/demo/mini-area.md new file mode 100644 index 0000000..2b9bfb4 --- /dev/null +++ b/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( + +, mountNode); +```` diff --git a/src/components/Charts/demo/mini-bar.md b/src/components/Charts/demo/mini-bar.md new file mode 100644 index 0000000..fef301b --- /dev/null +++ b/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( + +, mountNode); +```` diff --git a/src/components/Charts/demo/mini-pie.md b/src/components/Charts/demo/mini-pie.md new file mode 100644 index 0000000..9b1abf0 --- /dev/null +++ b/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( + , + mountNode +); +``` diff --git a/src/components/Charts/demo/mini-progress.md b/src/components/Charts/demo/mini-progress.md new file mode 100644 index 0000000..6308a8f --- /dev/null +++ b/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( + +, mountNode); +```` diff --git a/src/components/Charts/demo/mix.md b/src/components/Charts/demo/mix.md new file mode 100644 index 0000000..fc64110 --- /dev/null +++ b/src/components/Charts/demo/mix.md @@ -0,0 +1,84 @@ +--- +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( + + + + 本周访问} + total={numeral(12321).format('0,0')} + status="up" + subTotal={17.1} + /> + + + + + } + total={numeral(8846).format('0,0')} + footer={} + contentHeight={46} + > + + + + + } + total="78%" + footer={ +
    + + 周同比 + 12% + + + 日环比 + 11% + +
    + } + contentHeight={46} + > + +
    + +
    +, mountNode); +```` diff --git a/src/components/Charts/demo/pie.md b/src/components/Charts/demo/pie.md new file mode 100644 index 0000000..9c87161 --- /dev/null +++ b/src/components/Charts/demo/pie.md @@ -0,0 +1,54 @@ +--- +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( + ( + now.y + pre, 0)) + }} + /> + )} + data={salesPieData} + valueFormat={val => } + height={294} + />, + mountNode, +); +``` diff --git a/src/components/Charts/demo/radar.md b/src/components/Charts/demo/radar.md new file mode 100644 index 0000000..584344a --- /dev/null +++ b/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( + + + +, mountNode); +```` diff --git a/src/components/Charts/demo/tag-cloud.md b/src/components/Charts/demo/tag-cloud.md new file mode 100644 index 0000000..c66f6fe --- /dev/null +++ b/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( + +, mountNode); +```` diff --git a/src/components/Charts/demo/timeline-chart.md b/src/components/Charts/demo/timeline-chart.md new file mode 100644 index 0000000..60773b5 --- /dev/null +++ b/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( + +, mountNode); +```` diff --git a/src/components/Charts/demo/waterwave.md b/src/components/Charts/demo/waterwave.md new file mode 100644 index 0000000..74d290f --- /dev/null +++ b/src/components/Charts/demo/waterwave.md @@ -0,0 +1,20 @@ +--- +order: 8 +title: 水波图 +--- + +水波图是一种比例的展示方式,可以更直观的展示关键值的占比。 + +````jsx +import { WaterWave } from 'ant-design-pro/lib/Charts'; + +ReactDOM.render( +
    + +
    +, mountNode); +```` diff --git a/src/components/Charts/g2.js b/src/components/Charts/g2.js new file mode 100644 index 0000000..21e22c2 --- /dev/null +++ b/src/components/Charts/g2.js @@ -0,0 +1,15 @@ +// 全局 G2 设置 +import { track, setTheme } from 'bizcharts'; + +track(false); + +const config = { + defaultColor: '#1089ff', + shape: { + interval: { + fillOpacity: 1, + }, + }, +}; + +setTheme(config); diff --git a/src/components/Charts/index.d.ts b/src/components/Charts/index.d.ts new file mode 100644 index 0000000..1ff27af --- /dev/null +++ b/src/components/Charts/index.d.ts @@ -0,0 +1,17 @@ +import * 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 }; diff --git a/src/components/Charts/index.js b/src/components/Charts/index.js new file mode 100644 index 0000000..cacedbc --- /dev/null +++ b/src/components/Charts/index.js @@ -0,0 +1,49 @@ +import numeral from 'numeral'; +import './g2'; +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 => `¥ ${numeral(val).format('0,0')}`; + +const Charts = { + yuan, + Bar, + Pie, + Gauge, + Radar, + MiniBar, + MiniArea, + MiniProgress, + ChartCard, + Field, + WaterWave, + TagCloud, + TimelineChart, +}; + +export { + Charts as default, + yuan, + Bar, + Pie, + Gauge, + Radar, + MiniBar, + MiniArea, + MiniProgress, + ChartCard, + Field, + WaterWave, + TagCloud, + TimelineChart, +}; diff --git a/src/components/Charts/index.less b/src/components/Charts/index.less new file mode 100644 index 0000000..190428b --- /dev/null +++ b/src/components/Charts/index.less @@ -0,0 +1,19 @@ +.miniChart { + position: relative; + width: 100%; + .chartContent { + position: absolute; + bottom: -28px; + width: 100%; + > div { + margin: 0 -5px; + overflow: hidden; + } + } + .chartLoading { + position: absolute; + top: 16px; + left: 50%; + margin-left: -7px; + } +} diff --git a/src/components/Charts/index.md b/src/components/Charts/index.md new file mode 100644 index 0000000..cb7c9c9 --- /dev/null +++ b/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 \| function | - | +| 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)` | +| padding | 图表内部间距 | [array](https://github.com/alibaba/BizCharts/blob/master/doc/api/chart.md#7padding-object--number--array-) | `'auto'` | +| 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` | +| padding | 图表内部间距 | [array](https://github.com/alibaba/BizCharts/blob/master/doc/api/chart.md#7padding-object--number--array-) | `'auto'` | +| percent | 占比 | number | - | +| tooltip | 是否显示 tooltip | boolean | true | +| valueFormat | 显示值的格式化函数 | function | - | +| title | 图表标题 | ReactNode\|string | - | +| subTitle | 图表子标题 | ReactNode\|string | - | +| total | 图标中央的总数 | string | function | - | + +### Radar + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| title | 图表标题 | ReactNode\|string | - | +| height | 图表高度 | number | - | +| hasLegend | 是否显示 legend | boolean | `false` | +| padding | 图表内部间距 | [array](https://github.com/alibaba/BizCharts/blob/master/doc/api/chart.md#7padding-object--number--array-) | `'auto'` | +| 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 | - | +| height | 高度值 | number | - | + +### TimelineChart + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| data | 标题 | Array | - | +| titleMap | 指标别名 | Object{y1: '客流量', y2: '支付笔数'} | - | +| height | 高度值 | number | 400 | + +### Field + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| label | 标题 | ReactNode\|string | - | +| value | 值 | ReactNode\|string | - | diff --git a/src/components/CountDown/demo/simple.md b/src/components/CountDown/demo/simple.md new file mode 100644 index 0000000..e42cbf1 --- /dev/null +++ b/src/components/CountDown/demo/simple.md @@ -0,0 +1,24 @@ +--- +order: 0 +title: + zh-CN: 基本 + en-US: Basic +--- + +## zh-CN + +简单的倒计时组件使用。 + +## en-US + +The simplest usage. + +````jsx +import CountDown from 'ant-design-pro/lib/CountDown'; + +const targetTime = new Date().getTime() + 3900000; + +ReactDOM.render( + +, mountNode); +```` diff --git a/src/components/CountDown/index.d.ts b/src/components/CountDown/index.d.ts new file mode 100644 index 0000000..d39a2e9 --- /dev/null +++ b/src/components/CountDown/index.d.ts @@ -0,0 +1,9 @@ +import * as React from 'react'; +export interface ICountDownProps { + format?: (time: number) => void; + target: Date | number; + onEnd?: () => void; + style?: React.CSSProperties; +} + +export default class CountDown extends React.Component {} diff --git a/src/components/CountDown/index.en-US.md b/src/components/CountDown/index.en-US.md new file mode 100644 index 0000000..7b45240 --- /dev/null +++ b/src/components/CountDown/index.en-US.md @@ -0,0 +1,15 @@ +--- +title: CountDown +cols: 1 +order: 3 +--- + +Simple CountDown Component. + +## API + +| Property | Description | Type | Default | +|----------|------------------------------------------|-------------|-------| +| format | Formatter of time | Function(time) | | +| target | Target time | Date | - | +| onEnd | Countdown to the end callback | funtion | -| diff --git a/src/components/CountDown/index.js b/src/components/CountDown/index.js new file mode 100644 index 0000000..875fb1e --- /dev/null +++ b/src/components/CountDown/index.js @@ -0,0 +1,117 @@ +import React, { Component } from 'react'; + +function fixedZero(val) { + return val * 1 < 10 ? `0${val}` : val; +} + +class CountDown extends Component { + constructor(props) { + super(props); + + const { lastTime } = this.initTime(props); + + this.state = { + lastTime, + }; + } + + componentDidMount() { + this.tick(); + } + + componentWillReceiveProps(nextProps) { + if (this.props.target !== nextProps.target) { + clearTimeout(this.timer); + const { lastTime } = this.initTime(nextProps); + this.setState( + { + lastTime, + }, + () => { + this.tick(); + } + ); + } + } + + componentWillUnmount() { + clearTimeout(this.timer); + } + + timer = 0; + interval = 1000; + initTime = props => { + let lastTime = 0; + let targetTime = 0; + try { + if (Object.prototype.toString.call(props.target) === '[object Date]') { + targetTime = props.target.getTime(); + } else { + targetTime = new Date(props.target).getTime(); + } + } catch (e) { + throw new Error('invalid target prop', e); + } + + lastTime = targetTime - new Date().getTime(); + return { + lastTime: lastTime < 0 ? 0 : lastTime, + }; + }; + // defaultFormat = time => ( + // {moment(time).format('hh:mm:ss')} + // ); + defaultFormat = time => { + const hours = 60 * 60 * 1000; + const minutes = 60 * 1000; + + const h = Math.floor(time / hours); + const m = Math.floor((time - h * hours) / minutes); + const s = Math.floor((time - h * hours - m * minutes) / 1000); + return ( + + {fixedZero(h)}:{fixedZero(m)}:{fixedZero(s)} + + ); + }; + tick = () => { + const { onEnd } = this.props; + let { lastTime } = this.state; + + this.timer = setTimeout(() => { + if (lastTime < this.interval) { + clearTimeout(this.timer); + this.setState( + { + lastTime: 0, + }, + () => { + if (onEnd) { + onEnd(); + } + } + ); + } else { + lastTime -= this.interval; + this.setState( + { + lastTime, + }, + () => { + this.tick(); + } + ); + } + }, this.interval); + }; + + render() { + const { format = this.defaultFormat, onEnd, ...rest } = this.props; + const { lastTime } = this.state; + const result = format(lastTime); + + return {result}; + } +} + +export default CountDown; diff --git a/src/components/CountDown/index.zh-CN.md b/src/components/CountDown/index.zh-CN.md new file mode 100644 index 0000000..7e00ba1 --- /dev/null +++ b/src/components/CountDown/index.zh-CN.md @@ -0,0 +1,16 @@ +--- +title: CountDown +subtitle: 倒计时 +cols: 1 +order: 3 +--- + +倒计时组件。 + +## API + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| format | 时间格式化显示 | Function(time) | | +| target | 目标时间 | Date | - | +| onEnd | 倒计时结束回调 | funtion | -| diff --git a/src/components/DescriptionList/Description.d.ts b/src/components/DescriptionList/Description.d.ts new file mode 100644 index 0000000..2a17be3 --- /dev/null +++ b/src/components/DescriptionList/Description.d.ts @@ -0,0 +1,9 @@ +import * as React from 'react'; + +export default class Description extends React.Component< + { + term: React.ReactNode; + style?: React.CSSProperties; + }, + any +> {} diff --git a/src/components/DescriptionList/Description.js b/src/components/DescriptionList/Description.js new file mode 100644 index 0000000..e024796 --- /dev/null +++ b/src/components/DescriptionList/Description.js @@ -0,0 +1,26 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +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 ( + + {term &&
    {term}
    } + {children &&
    {children}
    } + + ); +}; + +Description.defaultProps = { + term: '', +}; + +Description.propTypes = { + term: PropTypes.node, +}; + +export default Description; diff --git a/src/components/DescriptionList/DescriptionList.js b/src/components/DescriptionList/DescriptionList.js new file mode 100644 index 0000000..128b0d3 --- /dev/null +++ b/src/components/DescriptionList/DescriptionList.js @@ -0,0 +1,29 @@ +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.small]: size === 'small', + [styles.large]: size === 'large', + }); + const column = col > 4 ? 4 : col; + return ( +
    + {title ?
    {title}
    : null} + + {React.Children.map(children, child => React.cloneElement(child, { column }))} + +
    + ); +}; diff --git a/src/components/DescriptionList/demo/basic.md b/src/components/DescriptionList/demo/basic.md new file mode 100644 index 0000000..8795455 --- /dev/null +++ b/src/components/DescriptionList/demo/basic.md @@ -0,0 +1,43 @@ +--- +order: 0 +title: + zh-CN: 基本 + en-US: Basic +--- + +## zh-CN + +基本描述列表。 + +## en-US + +Basic DescriptionList. + +````jsx +import DescriptionList from 'ant-design-pro/lib/DescriptionList'; + +const { Description } = DescriptionList; + +ReactDOM.render( + + + A free, open source, cross-platform, + graphical web browser developed by the + Mozilla Corporation and hundreds of + volunteers. + + + A free, open source, cross-platform, + graphical web browser developed by the + Mozilla Corporation and hundreds of + volunteers. + + + A free, open source, cross-platform, + graphical web browser developed by the + Mozilla Corporation and hundreds of + volunteers. + + +, mountNode); +```` diff --git a/src/components/DescriptionList/demo/vertical.md b/src/components/DescriptionList/demo/vertical.md new file mode 100644 index 0000000..2742f7c --- /dev/null +++ b/src/components/DescriptionList/demo/vertical.md @@ -0,0 +1,43 @@ +--- +order: 1 +title: + zh-CN: 垂直型 + en-US: Vertical +--- + +## zh-CN + +垂直布局。 + +## en-US + +Vertical layout. + +````jsx +import DescriptionList from 'ant-design-pro/lib/DescriptionList'; + +const { Description } = DescriptionList; + +ReactDOM.render( + + + A free, open source, cross-platform, + graphical web browser developed by the + Mozilla Corporation and hundreds of + volunteers. + + + A free, open source, cross-platform, + graphical web browser developed by the + Mozilla Corporation and hundreds of + volunteers. + + + A free, open source, cross-platform, + graphical web browser developed by the + Mozilla Corporation and hundreds of + volunteers. + + +, mountNode); +```` diff --git a/src/components/DescriptionList/index.d.ts b/src/components/DescriptionList/index.d.ts new file mode 100644 index 0000000..96ccfa7 --- /dev/null +++ b/src/components/DescriptionList/index.d.ts @@ -0,0 +1,15 @@ +import * as React from 'react'; +import Description from './Description'; + +export interface IDescriptionListProps { + layout?: 'horizontal' | 'vertical'; + col?: number; + title: React.ReactNode; + gutter?: number; + size?: 'large' | 'small'; + style?: React.CSSProperties; +} + +export default class DescriptionList extends React.Component { + public static Description: typeof Description; +} diff --git a/src/components/DescriptionList/index.en-US.md b/src/components/DescriptionList/index.en-US.md new file mode 100644 index 0000000..089f30b --- /dev/null +++ b/src/components/DescriptionList/index.en-US.md @@ -0,0 +1,33 @@ +--- +title: DescriptionList +cols: 1 +order: 4 +--- + +Groups display multiple read-only fields, which are common to informational displays on detail pages. + +## API + +### DescriptionList + +| Property | Description | Type | Default | +|----------|------------------------------------------|-------------|---------| +| layout | type of layout | Enum{'horizontal', 'vertical'} | 'horizontal' | +| col | specify the maximum number of columns to display, the final columns number is determined by col setting combined with [Responsive Rules](/components/DescriptionList#Responsive-Rules) | number(0 < col <= 4) | 3 | +| title | title | ReactNode | - | +| gutter | specify the distance between two items, unit is `px` | number | 32 | +| size | size of list | Enum{'large', 'small'} | - | + +#### Responsive Rules + +| Window Width | Columns Number | +|---------------------|---------------------------------------------| +| `≥768px` | `col` | +| `≥576px` | `col < 2 ? col : 2` | +| `<576px` | `1` | + +### DescriptionList.Description + +| Property | Description | Type | Default | +|----------|------------------------------------------|-------------|-------| +| term | item title | ReactNode | - | diff --git a/src/components/DescriptionList/index.js b/src/components/DescriptionList/index.js new file mode 100644 index 0000000..357f479 --- /dev/null +++ b/src/components/DescriptionList/index.js @@ -0,0 +1,5 @@ +import DescriptionList from './DescriptionList'; +import Description from './Description'; + +DescriptionList.Description = Description; +export default DescriptionList; diff --git a/src/components/DescriptionList/index.less b/src/components/DescriptionList/index.less new file mode 100644 index 0000000..ebffa55 --- /dev/null +++ b/src/components/DescriptionList/index.less @@ -0,0 +1,76 @@ +@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: -0.5px; + } + } + + .detail { + line-height: 22px; + width: 100%; + padding-bottom: 16px; + color: @text-color; + display: table-cell; + } + + &.small { + // 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; + } + } + + &.large { + .title { + font-size: 16px; + } + } + + &.vertical { + .term { + padding-bottom: 8px; + display: block; + } + + .detail { + display: block; + } + } +} diff --git a/src/components/DescriptionList/index.zh-CN.md b/src/components/DescriptionList/index.zh-CN.md new file mode 100644 index 0000000..b16a7fe --- /dev/null +++ b/src/components/DescriptionList/index.zh-CN.md @@ -0,0 +1,37 @@ +--- +title: 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 | 列表型号 | Enum{'large', 'small'} | - | + +#### 响应式规则 + +| 窗口宽度 | 展示列数 | +|---------------------|---------------------------------------------| +| `≥768px` | `col` | +| `≥576px` | `col < 2 ? col : 2` | +| `<576px` | `1` | + +### DescriptionList.Description + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| term | 列表项标题 | ReactNode | - | + + + diff --git a/src/components/DescriptionList/responsive.js b/src/components/DescriptionList/responsive.js new file mode 100644 index 0000000..a5aa73f --- /dev/null +++ b/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 }, +}; diff --git a/src/components/EditableItem/index.js b/src/components/EditableItem/index.js new file mode 100644 index 0000000..fcda844 --- /dev/null +++ b/src/components/EditableItem/index.js @@ -0,0 +1,41 @@ +import React, { PureComponent } from 'react'; +import { Input, Icon } from 'antd'; +import styles from './index.less'; + +export default class EditableItem extends PureComponent { + state = { + value: this.props.value, + editable: false, + }; + handleChange = e => { + const { value } = e.target; + this.setState({ value }); + }; + check = () => { + this.setState({ editable: false }); + if (this.props.onChange) { + this.props.onChange(this.state.value); + } + }; + edit = () => { + this.setState({ editable: true }); + }; + render() { + const { value, editable } = this.state; + return ( +
    + {editable ? ( +
    + + +
    + ) : ( +
    + {value || ' '} + +
    + )} +
    + ); + } +} diff --git a/src/components/EditableItem/index.less b/src/components/EditableItem/index.less new file mode 100644 index 0000000..457a18b --- /dev/null +++ b/src/components/EditableItem/index.less @@ -0,0 +1,25 @@ +@import '~antd/lib/style/themes/default.less'; + +.editableItem { + line-height: @input-height-base; + display: table; + width: 100%; + margin-top: (@font-size-base * @line-height-base - @input-height-base) / 2; + + .wrapper { + display: table-row; + + & > * { + display: table-cell; + } + + & > *:first-child { + width: 85%; + } + + .icon { + cursor: pointer; + text-align: right; + } + } +} diff --git a/src/components/EditableLinkGroup/index.js b/src/components/EditableLinkGroup/index.js new file mode 100644 index 0000000..e230c3d --- /dev/null +++ b/src/components/EditableLinkGroup/index.js @@ -0,0 +1,46 @@ +import React, { PureComponent, createElement } from 'react'; +import PropTypes from 'prop-types'; +import { Button } from 'antd'; +import styles from './index.less'; + +// TODO: 添加逻辑 + +class EditableLinkGroup extends PureComponent { + static defaultProps = { + links: [], + onAdd: () => {}, + linkElement: 'a', + }; + + static propTypes = { + links: PropTypes.array, + onAdd: PropTypes.func, + linkElement: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + }; + + render() { + const { links, linkElement, onAdd } = this.props; + return ( +
    + {links.map(link => + createElement( + linkElement, + { + key: `linkGroup-item-${link.id || link.title}`, + to: link.href, + href: link.href, + }, + link.title + ) + )} + { + + } +
    + ); + } +} + +export default EditableLinkGroup; diff --git a/src/components/EditableLinkGroup/index.less b/src/components/EditableLinkGroup/index.less new file mode 100644 index 0000000..a421df6 --- /dev/null +++ b/src/components/EditableLinkGroup/index.less @@ -0,0 +1,16 @@ +@import '~antd/lib/style/themes/default.less'; + +.linkGroup { + padding: 20px 0 8px 24px; + font-size: 0; + & > a { + color: @text-color; + display: inline-block; + font-size: @font-size-base; + margin-bottom: 13px; + width: 25%; + &:hover { + color: @primary-color; + } + } +} diff --git a/src/components/Ellipsis/demo/line.md b/src/components/Ellipsis/demo/line.md new file mode 100644 index 0000000..bc31170 --- /dev/null +++ b/src/components/Ellipsis/demo/line.md @@ -0,0 +1,31 @@ +--- +order: 1 +title: + zh-CN: 按照行数省略 + en-US: Truncate according to the number of rows +--- + +## zh-CN + +通过设置 `lines` 属性指定最大行数,如果超过这个行数的文本会自动截取。但是在这种模式下所有 `children` 将会被转换成纯文本。 + +并且注意在这种模式下,外容器需要有指定的宽度(或设置自身宽度)。 + +## en-US + +`lines` attribute specifies the maximum number of rows where the text will automatically be truncated when exceeded. In this mode, all children will be converted to plain text. + +Also note that, in this mode, the outer container needs to have a specified width (or set its own width). + + +````jsx +import Ellipsis from 'ant-design-pro/lib/Ellipsis'; + +const article =

    There were injuries alleged in three cases in 2015, and a fourth incident in September, according to the safety recall report. After meeting with US regulators in October, the firm decided to issue a voluntary recall.

    ; + +ReactDOM.render( +
    + {article} +
    +, mountNode); +```` diff --git a/src/components/Ellipsis/demo/number.md b/src/components/Ellipsis/demo/number.md new file mode 100644 index 0000000..0bc1a0f --- /dev/null +++ b/src/components/Ellipsis/demo/number.md @@ -0,0 +1,28 @@ +--- +order: 0 +title: + zh-CN: 按照字符数省略 + en-US: Truncate according to the number of character +--- + +## zh-CN + +通过设置 `length` 属性指定文本最长长度,如果超过这个长度会自动截取。 + +## en-US + +`length` attribute specifies the maximum length where the text will automatically be truncated when exceeded. + +````jsx +import Ellipsis from 'ant-design-pro/lib/Ellipsis'; + +const article = 'There were injuries alleged in three cases in 2015, and a fourth incident in September, according to the safety recall report. After meeting with US regulators in October, the firm decided to issue a voluntary recall.'; + +ReactDOM.render( +
    + {article} +

    Show Tooltip

    + {article} +
    +, mountNode); +```` diff --git a/src/components/Ellipsis/index.d.ts b/src/components/Ellipsis/index.d.ts new file mode 100644 index 0000000..4643ee7 --- /dev/null +++ b/src/components/Ellipsis/index.d.ts @@ -0,0 +1,10 @@ +import * as React from 'react'; +export interface IEllipsisProps { + tooltip?: boolean; + length?: number; + lines?: number; + style?: React.CSSProperties; + className?: string; +} + +export default class Ellipsis extends React.Component {} diff --git a/src/components/Ellipsis/index.en-US.md b/src/components/Ellipsis/index.en-US.md new file mode 100644 index 0000000..fa5beb6 --- /dev/null +++ b/src/components/Ellipsis/index.en-US.md @@ -0,0 +1,15 @@ +--- +title: Ellipsis +cols: 1 +order: 10 +--- + +When the text is too long, the Ellipsis automatically shortens it according to its length or the maximum number of lines. + +## API + +Property | Description | Type | Default +----|------|-----|------ +tooltip | tooltip for showing the full text content when hovering over | boolean | - +length | maximum number of characters in the text before being truncated | number | - +lines | maximum number of rows in the text before being truncated | number | `1` diff --git a/src/components/Ellipsis/index.js b/src/components/Ellipsis/index.js new file mode 100644 index 0000000..93c5c23 --- /dev/null +++ b/src/components/Ellipsis/index.js @@ -0,0 +1,224 @@ +import React, { Component } from 'react'; +import { Tooltip } from 'antd'; +import classNames from 'classnames'; +import styles from './index.less'; + +/* eslint react/no-did-mount-set-state: 0 */ +/* eslint no-param-reassign: 0 */ + +const isSupportLineClamp = document.body.style.webkitLineClamp !== undefined; + +const EllipsisText = ({ text, length, tooltip, ...other }) => { + if (typeof text !== 'string') { + throw new Error('Ellipsis children must be string.'); + } + if (text.length <= length || length < 0) { + return {text}; + } + const tail = '...'; + let displayText; + if (length - tail.length <= 0) { + displayText = ''; + } else { + displayText = text.slice(0, length - tail.length); + } + + if (tooltip) { + return ( + + + {displayText} + {tail} + + + ); + } + + return ( + + {displayText} + {tail} + + ); +}; + +export default class Ellipsis extends Component { + state = { + text: '', + targetCount: 0, + }; + + componentDidMount() { + if (this.node) { + this.computeLine(); + } + } + + componentWillReceiveProps(nextProps) { + if (this.props.lines !== nextProps.lines) { + this.computeLine(); + } + } + + computeLine = () => { + const { lines } = this.props; + if (lines && !isSupportLineClamp) { + const text = this.shadowChildren.innerText; + const lineHeight = parseInt(getComputedStyle(this.root).lineHeight, 10); + const targetHeight = lines * lineHeight; + this.content.style.height = `${targetHeight}px`; + const totalHeight = this.shadowChildren.offsetHeight; + const shadowNode = this.shadow.firstChild; + + if (totalHeight <= targetHeight) { + this.setState({ + text, + targetCount: text.length, + }); + return; + } + + // bisection + const len = text.length; + const mid = Math.floor(len / 2); + + const count = this.bisection(targetHeight, mid, 0, len, text, shadowNode); + + this.setState({ + text, + targetCount: count, + }); + } + }; + + bisection = (th, m, b, e, text, shadowNode) => { + const suffix = '...'; + let mid = m; + let end = e; + let begin = b; + shadowNode.innerHTML = text.substring(0, mid) + suffix; + let sh = shadowNode.offsetHeight; + + if (sh <= th) { + shadowNode.innerHTML = text.substring(0, mid + 1) + suffix; + sh = shadowNode.offsetHeight; + if (sh > th) { + return mid; + } else { + begin = mid; + mid = Math.floor((end - begin) / 2) + begin; + return this.bisection(th, mid, begin, end, text, shadowNode); + } + } else { + if (mid - 1 < 0) { + return mid; + } + shadowNode.innerHTML = text.substring(0, mid - 1) + suffix; + sh = shadowNode.offsetHeight; + if (sh <= th) { + return mid - 1; + } else { + end = mid; + mid = Math.floor((end - begin) / 2) + begin; + return this.bisection(th, mid, begin, end, text, shadowNode); + } + } + }; + + handleRoot = n => { + this.root = n; + }; + + handleContent = n => { + this.content = n; + }; + + handleNode = n => { + this.node = n; + }; + + handleShadow = n => { + this.shadow = n; + }; + + handleShadowChildren = n => { + this.shadowChildren = n; + }; + + render() { + const { text, targetCount } = this.state; + const { children, lines, length, className, tooltip, ...restProps } = this.props; + + const cls = classNames(styles.ellipsis, className, { + [styles.lines]: lines && !isSupportLineClamp, + [styles.lineClamp]: lines && isSupportLineClamp, + }); + + if (!lines && !length) { + return ( + + {children} + + ); + } + + // length + if (!lines) { + return ( + + ); + } + + const id = `antd-pro-ellipsis-${`${new Date().getTime()}${Math.floor(Math.random() * 100)}`}`; + + // support document.body.style.webkitLineClamp + if (isSupportLineClamp) { + const style = `#${id}{-webkit-line-clamp:${lines};-webkit-box-orient: vertical;}`; + return ( +
    + + {tooltip ? ( + + {children} + + ) : ( + children + )} +
    + ); + } + + const childNode = ( + + {targetCount > 0 && text.substring(0, targetCount)} + {targetCount > 0 && targetCount < text.length && '...'} + + ); + + return ( +
    +
    + {tooltip ? ( + + {childNode} + + ) : ( + childNode + )} +
    + {children} +
    +
    + {text} +
    +
    +
    + ); + } +} diff --git a/src/components/Ellipsis/index.less b/src/components/Ellipsis/index.less new file mode 100644 index 0000000..dd59e3f --- /dev/null +++ b/src/components/Ellipsis/index.less @@ -0,0 +1,24 @@ +.ellipsis { + overflow: hidden; + display: inline-block; + word-break: break-all; + width: 100%; +} + +.lines { + position: relative; + .shadow { + display: block; + position: relative; + color: transparent; + opacity: 0; + z-index: -999; + } +} + +.lineClamp { + position: relative; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; +} diff --git a/src/components/Ellipsis/index.zh-CN.md b/src/components/Ellipsis/index.zh-CN.md new file mode 100644 index 0000000..8fe98bb --- /dev/null +++ b/src/components/Ellipsis/index.zh-CN.md @@ -0,0 +1,16 @@ +--- +title: Ellipsis +subtitle: 文本自动省略号 +cols: 1 +order: 10 +--- + +文本过长自动处理省略号,支持按照文本长度和最大行数两种方式截取。 + +## API + +参数 | 说明 | 类型 | 默认值 +----|------|-----|------ +tooltip | 移动到文本展示完整内容的提示 | boolean | - +length | 在按照长度截取下的文本最大字符数,超过则截取省略 | number | - +lines | 在按照行数截取下最大的行数,超过则截取省略 | number | `1` diff --git a/src/components/Exception/demo/403.md b/src/components/Exception/demo/403.md new file mode 100644 index 0000000..bb46037 --- /dev/null +++ b/src/components/Exception/demo/403.md @@ -0,0 +1,29 @@ +--- +order: 2 +title: + zh-CN: 403 + en-US: 403 +--- + +## zh-CN + +403 页面,配合自定义操作。 + +## en-US + +403 page with custom operations. + +````jsx +import Exception from 'ant-design-pro/lib/Exception'; +import { Button } from 'antd'; + +const actions = ( +
    + + +
    +); +ReactDOM.render( + +, mountNode); +```` diff --git a/src/components/Exception/demo/404.md b/src/components/Exception/demo/404.md new file mode 100644 index 0000000..db50de6 --- /dev/null +++ b/src/components/Exception/demo/404.md @@ -0,0 +1,22 @@ +--- +order: 0 +title: + zh-CN: 404 + en-US: 404 +--- + +## zh-CN + +404 页面。 + +## en-US + +404 page. + +````jsx +import Exception from 'ant-design-pro/lib/Exception'; + +ReactDOM.render( + +, mountNode); +```` diff --git a/src/components/Exception/demo/500.md b/src/components/Exception/demo/500.md new file mode 100644 index 0000000..096ca8e --- /dev/null +++ b/src/components/Exception/demo/500.md @@ -0,0 +1,22 @@ +--- +order: 1 +title: + zh-CN: 500 + en-US: 500 +--- + +## zh-CN + +500 页面。 + +## en-US + +500 page. + +````jsx +import Exception from 'ant-design-pro/lib/Exception'; + +ReactDOM.render( + +, mountNode); +```` diff --git a/src/components/Exception/index.d.ts b/src/components/Exception/index.d.ts new file mode 100644 index 0000000..5e32516 --- /dev/null +++ b/src/components/Exception/index.d.ts @@ -0,0 +1,12 @@ +import * as React from 'react'; +export interface IExceptionProps { + type?: '403' | '404' | '500'; + title?: React.ReactNode; + desc?: React.ReactNode; + img?: string; + actions?: React.ReactNode; + linkElement?: React.ReactNode; + style?: React.CSSProperties; +} + +export default class Exception extends React.Component {} diff --git a/src/components/Exception/index.en-US.md b/src/components/Exception/index.en-US.md new file mode 100644 index 0000000..320b0e7 --- /dev/null +++ b/src/components/Exception/index.en-US.md @@ -0,0 +1,18 @@ +--- +title: Exception +cols: 1 +order: 5 +--- + +Exceptions page is used to provide feedback on specific abnormal state. Usually, it contains an explanation of the error status, and provides users with suggestions or operations, to prevent users from feeling lost and confused. + +## API + +Property | Description | Type | Default +---------|-------------|------|-------- +type | type of exception, the corresponding default `title`, `desc`, `img` will be given if set, which can be overridden by explicit setting of `title`, `desc`, `img` | Enum {'403', '404', '500'} | - +title | title | ReactNode | - +desc | supplementary description | ReactNode | - +img | the url of background image | string | - +actions | suggested operations, a default 'Home' link will show if not set | ReactNode | - +linkElement | to specify the element of link | string\|ReactElement | 'a' \ No newline at end of file diff --git a/src/components/Exception/index.js b/src/components/Exception/index.js new file mode 100644 index 0000000..4fc813f --- /dev/null +++ b/src/components/Exception/index.js @@ -0,0 +1,35 @@ +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 ( +
    +
    +
    +
    +
    +

    {title || config[pageType].title}

    +
    {desc || config[pageType].desc}
    +
    + {actions || + createElement( + linkElement, + { + to: '/', + href: '/', + }, + + )} +
    +
    +
    + ); +}; diff --git a/src/components/Exception/index.less b/src/components/Exception/index.less new file mode 100644 index 0000000..5ef378b --- /dev/null +++ b/src/components/Exception/index.less @@ -0,0 +1,88 @@ +@import '~antd/lib/style/themes/default.less'; + +.exception { + display: flex; + align-items: center; + height: 100%; + + .imgBlock { + flex: 0 0 62.5%; + width: 62.5%; + padding-right: 152px; + zoom: 1; + &:before, + &:after { + content: ' '; + display: table; + } + &:after { + clear: both; + visibility: hidden; + font-size: 0; + height: 0; + } + } + + .imgEle { + height: 360px; + width: 100%; + max-width: 430px; + float: right; + background-repeat: no-repeat; + background-position: 50% 50%; + background-size: contain; + } + + .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; + } + } +} diff --git a/src/components/Exception/index.zh-CN.md b/src/components/Exception/index.zh-CN.md new file mode 100644 index 0000000..13e4e7e --- /dev/null +++ b/src/components/Exception/index.zh-CN.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 | 定义链接的元素 | string\|ReactElement | 'a' | diff --git a/src/components/Exception/typeConfig.js b/src/components/Exception/typeConfig.js new file mode 100644 index 0000000..b6e1ee5 --- /dev/null +++ b/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; diff --git a/src/components/FooterToolbar/demo/basic.md b/src/components/FooterToolbar/demo/basic.md new file mode 100644 index 0000000..3043dbf --- /dev/null +++ b/src/components/FooterToolbar/demo/basic.md @@ -0,0 +1,44 @@ +--- +order: 0 +title: + zh-CN: 演示 + en-US: demo +iframe: 400 +--- + +## zh-CN + +浮动固定页脚。 + +## en-US + +Fixed to the footer. + +````jsx +import FooterToolbar from 'ant-design-pro/lib/FooterToolbar'; +import { Button } from 'antd'; + +ReactDOM.render( +
    +

    Content Content Content Content

    +

    Content Content Content Content

    +

    Content Content Content Content

    +

    Content Content Content Content

    +

    Content Content Content Content

    +

    Content Content Content Content

    +

    Content Content Content Content

    +

    Content Content Content Content

    +

    Content Content Content Content

    +

    Content Content Content Content

    +

    Content Content Content Content

    +

    Content Content Content Content

    +

    Content Content Content Content

    +

    Content Content Content Content

    +

    Content Content Content Content

    + + + + +
    +, mountNode); +```` \ No newline at end of file diff --git a/src/components/FooterToolbar/index.d.ts b/src/components/FooterToolbar/index.d.ts new file mode 100644 index 0000000..9c6ac5b --- /dev/null +++ b/src/components/FooterToolbar/index.d.ts @@ -0,0 +1,7 @@ +import * as React from 'react'; +export interface IFooterToolbarProps { + extra: React.ReactNode; + style?: React.CSSProperties; +} + +export default class FooterToolbar extends React.Component {} diff --git a/src/components/FooterToolbar/index.en-US.md b/src/components/FooterToolbar/index.en-US.md new file mode 100644 index 0000000..69fd80b --- /dev/null +++ b/src/components/FooterToolbar/index.en-US.md @@ -0,0 +1,18 @@ +--- +title: FooterToolbar +cols: 1 +order: 6 +--- + +A toolbar fixed at the bottom. + +## Usage + +It is fixed at the bottom of the content area and does not move along with the scroll bar, which is usually used for data collection and submission for long pages. + +## API + +Property | Description | Type | Default +---------|-------------|------|-------- +children | toolbar content, align to the right | ReactNode | - +extra | extra information, align to the left | ReactNode | - \ No newline at end of file diff --git a/src/components/FooterToolbar/index.js b/src/components/FooterToolbar/index.js new file mode 100644 index 0000000..d5ce75b --- /dev/null +++ b/src/components/FooterToolbar/index.js @@ -0,0 +1,15 @@ +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 ( +
    +
    {extra}
    +
    {children}
    +
    + ); + } +} diff --git a/src/components/FooterToolbar/index.less b/src/components/FooterToolbar/index.less new file mode 100644 index 0000000..de6606b --- /dev/null +++ b/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, 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; + } +} diff --git a/src/components/FooterToolbar/index.zh-CN.md b/src/components/FooterToolbar/index.zh-CN.md new file mode 100644 index 0000000..421ac08 --- /dev/null +++ b/src/components/FooterToolbar/index.zh-CN.md @@ -0,0 +1,19 @@ +--- +title: FooterToolbar +subtitle: 底部工具栏 +cols: 1 +order: 6 +--- + +固定在底部的工具栏。 + +## 何时使用 + +固定在内容区域的底部,不随滚动条移动,常用于长页面的数据搜集和提交工作。 + +## API + +参数 | 说明 | 类型 | 默认值 +----|------|-----|------ +children | 工具栏内容,向右对齐 | ReactNode | - +extra | 额外信息,向左对齐 | ReactNode | - diff --git a/src/components/GlobalFooter/demo/basic.md b/src/components/GlobalFooter/demo/basic.md new file mode 100644 index 0000000..9a06bad --- /dev/null +++ b/src/components/GlobalFooter/demo/basic.md @@ -0,0 +1,37 @@ +--- +order: 0 +title: 演示 +iframe: 400 +--- + +基本页脚。 + +````jsx +import GlobalFooter from 'ant-design-pro/lib/GlobalFooter'; +import { Icon } from 'antd'; + +const links = [{ + key: '帮助', + title: '帮助', + href: '', +}, { + key: 'github', + title: , + href: 'https://github.com/ant-design/ant-design-pro', + blankTarget: true, +}, { + key: '条款', + title: '条款', + href: '', + blankTarget: true, +}]; + +const copyright =
    Copyright 2017 蚂蚁金服体验技术部出品
    ; + +ReactDOM.render( +
    +
    + +
    +, mountNode); +```` diff --git a/src/components/GlobalFooter/index.d.ts b/src/components/GlobalFooter/index.d.ts new file mode 100644 index 0000000..85a9ca6 --- /dev/null +++ b/src/components/GlobalFooter/index.d.ts @@ -0,0 +1,12 @@ +import * as React from 'react'; +export interface IGlobalFooterProps { + links?: Array<{ + title: React.ReactNode; + href: string; + blankTarget?: boolean; + }>; + copyright?: React.ReactNode; + style?: React.CSSProperties; +} + +export default class GlobalFooter extends React.Component {} diff --git a/src/components/GlobalFooter/index.js b/src/components/GlobalFooter/index.js new file mode 100644 index 0000000..837fe6f --- /dev/null +++ b/src/components/GlobalFooter/index.js @@ -0,0 +1,21 @@ +import React from 'react'; +import classNames from 'classnames'; +import styles from './index.less'; + +export default ({ className, links, copyright }) => { + const clsString = classNames(styles.globalFooter, className); + return ( +
    + {links && ( +
    + {links.map(link => ( + + {link.title} + + ))} +
    + )} + {copyright &&
    {copyright}
    } +
    + ); +}; diff --git a/src/components/GlobalFooter/index.less b/src/components/GlobalFooter/index.less new file mode 100644 index 0000000..101dcf0 --- /dev/null +++ b/src/components/GlobalFooter/index.less @@ -0,0 +1,29 @@ +@import '~antd/lib/style/themes/default.less'; + +.globalFooter { + padding: 0 16px; + margin: 48px 0 24px 0; + text-align: center; + + .links { + margin-bottom: 8px; + + a { + color: @text-color-secondary; + transition: all 0.3s; + + &:not(:last-child) { + margin-right: 40px; + } + + &:hover { + color: @text-color; + } + } + } + + .copyright { + color: @text-color-secondary; + font-size: @font-size-base; + } +} diff --git a/src/components/GlobalFooter/index.md b/src/components/GlobalFooter/index.md new file mode 100644 index 0000000..55b4be4 --- /dev/null +++ b/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 | - diff --git a/src/components/GlobalHeader/index.js b/src/components/GlobalHeader/index.js new file mode 100644 index 0000000..559b876 --- /dev/null +++ b/src/components/GlobalHeader/index.js @@ -0,0 +1,166 @@ +import React, { PureComponent } from 'react'; +import { Menu, Icon, Spin, Tag, Dropdown, Avatar, Divider, Tooltip } 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 '../NoticeIcon'; +import HeaderSearch from '../HeaderSearch'; +import styles from './index.less'; + +export default class GlobalHeader extends PureComponent { + 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 = ( + + {newNotice.extra} + + ); + } + return newNotice; + }); + return groupBy(newNotices, 'type'); + } + toggle = () => { + const { collapsed, onCollapse } = this.props; + onCollapse(!collapsed); + this.triggerResizeEvent(); + }; + /* eslint-disable*/ + @Debounce(600) + triggerResizeEvent() { + const event = document.createEvent('HTMLEvents'); + event.initEvent('resize', true, false); + window.dispatchEvent(event); + } + render() { + const { + currentUser, + collapsed, + fetchingNotices, + isMobile, + logo, + onNoticeVisibleChange, + onMenuClick, + onNoticeClear, + } = this.props; + const menu = ( + + + 个人中心 + + + 设置 + + + 触发报错 + + + + 退出登录 + + + ); + const noticeData = this.getNoticeData(); + return ( +
    + {isMobile && [ + + logo + , + , + ]} + +
    + { + console.log('input', value); // eslint-disable-line + }} + onPressEnter={value => { + console.log('enter', value); // eslint-disable-line + }} + /> + + + + + + { + console.log(item, tabProps); // eslint-disable-line + }} + onClear={onNoticeClear} + onPopupVisibleChange={onNoticeVisibleChange} + loading={fetchingNotices} + popupAlign={{ offset: [20, -16] }} + > + + + + + {currentUser.name ? ( + + + + {currentUser.name} + + + ) : ( + + )} +
    +
    + ); + } +} diff --git a/src/components/GlobalHeader/index.less b/src/components/GlobalHeader/index.less new file mode 100644 index 0000000..8508930 --- /dev/null +++ b/src/components/GlobalHeader/index.less @@ -0,0 +1,115 @@ +@import '~antd/lib/style/themes/default.less'; + +.header { + height: 64px; + padding: 0 12px 0 0; + background: #fff; + box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); + position: relative; +} + +:global { + .ant-layout { + min-height: 100vh; + 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) { + width: 160px; + } +} + +i.trigger { + font-size: 20px; + line-height: 64px; + cursor: pointer; + transition: all 0.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 0.3s; + height: 100%; + > i { + font-size: 16px; + vertical-align: middle; + color: @text-color; + } + &:hover, + &:global(.ant-popover-open) { + 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, 0.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; + } + } + } + } +} diff --git a/src/components/HeaderSearch/demo/basic.md b/src/components/HeaderSearch/demo/basic.md new file mode 100644 index 0000000..2139207 --- /dev/null +++ b/src/components/HeaderSearch/demo/basic.md @@ -0,0 +1,34 @@ +--- +order: 0 +title: 全局搜索 +--- + +通常放置在导航工具条右侧。(点击搜索图标预览效果) + +````jsx +import HeaderSearch from 'ant-design-pro/lib/HeaderSearch'; + +ReactDOM.render( +
    + { + console.log('input', value); // eslint-disable-line + }} + onPressEnter={(value) => { + console.log('enter', value); // eslint-disable-line + }} + /> +
    +, mountNode); +```` diff --git a/src/components/HeaderSearch/index.d.ts b/src/components/HeaderSearch/index.d.ts new file mode 100644 index 0000000..3a06d75 --- /dev/null +++ b/src/components/HeaderSearch/index.d.ts @@ -0,0 +1,11 @@ +import * as React from 'react'; +export interface IHeaderSearchProps { + placeholder?: string; + dataSource?: string[]; + onSearch?: (value: string) => void; + onChange?: (value: string) => void; + onPressEnter?: (value: string) => void; + style?: React.CSSProperties; +} + +export default class HeaderSearch extends React.Component {} diff --git a/src/components/HeaderSearch/index.js b/src/components/HeaderSearch/index.js new file mode 100644 index 0000000..295a837 --- /dev/null +++ b/src/components/HeaderSearch/index.js @@ -0,0 +1,87 @@ +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: [], + defaultOpen: false, + }; + static propTypes = { + className: PropTypes.string, + placeholder: PropTypes.string, + onSearch: PropTypes.func, + onPressEnter: PropTypes.func, + defaultActiveFirstOption: PropTypes.bool, + dataSource: PropTypes.array, + defaultOpen: PropTypes.bool, + }; + state = { + searchMode: this.props.defaultOpen, + 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; + delete restProps.defaultOpen; // for rc-select not affected + const inputClass = classNames(styles.input, { + [styles.show]: this.state.searchMode, + }); + return ( + + + + { + this.input = node; + }} + onKeyDown={this.onKeyDown} + onBlur={this.leaveSearchMode} + /> + + + ); + } +} diff --git a/src/components/HeaderSearch/index.less b/src/components/HeaderSearch/index.less new file mode 100644 index 0000000..e97386d --- /dev/null +++ b/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 0.3s, margin-left 0.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; + } + } +} diff --git a/src/components/HeaderSearch/index.md b/src/components/HeaderSearch/index.md new file mode 100644 index 0000000..3d4d3c2 --- /dev/null +++ b/src/components/HeaderSearch/index.md @@ -0,0 +1,21 @@ +--- +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) | - +defaultOpen | 输入框首次显示是否打开 | boolean | false diff --git a/src/components/Login/LoginItem.js b/src/components/Login/LoginItem.js new file mode 100644 index 0000000..98efb33 --- /dev/null +++ b/src/components/Login/LoginItem.js @@ -0,0 +1,104 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Form, Button, Row, Col } from 'antd'; +import omit from 'omit.js'; +import styles from './index.less'; +import map from './map'; + +const FormItem = Form.Item; + +function generator({ defaultProps, defaultRules, type }) { + return WrappedComponent => { + return class BasicComponent extends Component { + static contextTypes = { + form: PropTypes.object, + updateActive: PropTypes.func, + }; + constructor(props) { + super(props); + this.state = { + count: 0, + }; + } + componentDidMount() { + if (this.context.updateActive) { + this.context.updateActive(this.props.name); + } + } + componentWillUnmount() { + clearInterval(this.interval); + } + onGetCaptcha = () => { + let count = 59; + this.setState({ count }); + if (this.props.onGetCaptcha) { + this.props.onGetCaptcha(); + } + this.interval = setInterval(() => { + count -= 1; + this.setState({ count }); + if (count === 0) { + clearInterval(this.interval); + } + }, 1000); + }; + render() { + const { getFieldDecorator } = this.context.form; + const options = {}; + let otherProps = {}; + const { onChange, defaultValue, rules, name, ...restProps } = this.props; + const { count } = this.state; + options.rules = rules || defaultRules; + if (onChange) { + options.onChange = onChange; + } + if (defaultValue) { + options.initialValue = defaultValue; + } + otherProps = restProps || otherProps; + if (type === 'Captcha') { + const inputProps = omit(otherProps, ['onGetCaptcha']); + return ( + + + + {getFieldDecorator(name, options)( + + )} + + + + + + + ); + } + return ( + + {getFieldDecorator(name, options)( + + )} + + ); + } + }; + }; +} + +const LoginItem = {}; +Object.keys(map).forEach(item => { + LoginItem[item] = generator({ + defaultProps: map[item].props, + defaultRules: map[item].rules, + type: item, + })(map[item].component); +}); + +export default LoginItem; diff --git a/src/components/Login/LoginSubmit.js b/src/components/Login/LoginSubmit.js new file mode 100644 index 0000000..8770a97 --- /dev/null +++ b/src/components/Login/LoginSubmit.js @@ -0,0 +1,15 @@ +import React from 'react'; +import classNames from 'classnames'; +import { Button, Form } from 'antd'; +import styles from './index.less'; + +const FormItem = Form.Item; + +export default ({ className, ...rest }) => { + const clsString = classNames(styles.submit, className); + return ( + + + + + + + + +
    +); + +const extra = ( + + +
    状态
    +
    待审批
    + + +
    订单金额
    +
    ¥ 568.08
    + +
    +); + +const breadcrumbList = [{ + title: '一级菜单', + href: '/', +}, { + title: '二级菜单', + href: '/', +}, { + title: '三级菜单', +}]; + +const tabList = [{ + key: 'detail', + tab: '详情', +}, { + key: 'rule', + tab: '规则', +}]; + +function onTabChange(key) { + console.log(key); +} + +ReactDOM.render( +
    + } + action={action} + content={description} + extraContent={extra} + breadcrumbList={breadcrumbList} + tabList={tabList} + tabActiveKey="detail" + onTabChange={onTabChange} + /> +
    +, mountNode); +```` + + diff --git a/src/components/PageHeader/demo/structure.md b/src/components/PageHeader/demo/structure.md new file mode 100644 index 0000000..429eed6 --- /dev/null +++ b/src/components/PageHeader/demo/structure.md @@ -0,0 +1,68 @@ +--- +order: 0 +title: Structure +--- + +基本结构,具备响应式布局功能,主要断点为 768px 和 576px,拖动窗口改变大小试试看。 + +````jsx +import PageHeader from 'ant-design-pro/lib/PageHeader'; + +const breadcrumbList = [{ + title: '面包屑', +}]; + +const tabList = [{ + key: '1', + tab: '页签一', +}, { + key: '2', + tab: '页签二', +}, { + key: '3', + tab: '页签三', +}]; + +ReactDOM.render( +
    + Title
    } + logo={
    logo
    } + action={
    action
    } + content={
    content
    } + extraContent={
    extraContent
    } + breadcrumbList={breadcrumbList} + tabList={tabList} + tabActiveKey="1" + /> +
    +, mountNode); +```` + + diff --git a/src/components/PageHeader/index.d.ts b/src/components/PageHeader/index.d.ts new file mode 100644 index 0000000..a1c356e --- /dev/null +++ b/src/components/PageHeader/index.d.ts @@ -0,0 +1,20 @@ +import * as React from 'react'; +export interface IPageHeaderProps { + title?: React.ReactNode | string; + logo?: React.ReactNode | string; + action?: React.ReactNode | string; + content?: React.ReactNode; + extraContent?: React.ReactNode; + routes?: any[]; + params?: any; + breadcrumbList?: Array<{ title: React.ReactNode; href?: string }>; + tabList?: Array<{ key: string; tab: React.ReactNode }>; + tabActiveKey?: string; + tabDefaultActiveKey?: string; + onTabChange?: (key: string) => void; + tabBarExtraContent?: React.ReactNode; + linkElement?: React.ReactNode; + style?: React.CSSProperties; +} + +export default class PageHeader extends React.Component {} diff --git a/src/components/PageHeader/index.js b/src/components/PageHeader/index.js new file mode 100644 index 0000000..a36fa7f --- /dev/null +++ b/src/components/PageHeader/index.js @@ -0,0 +1,201 @@ +import React, { PureComponent, createElement } from 'react'; +import PropTypes from 'prop-types'; +import pathToRegexp from 'path-to-regexp'; +import { Breadcrumb, Tabs } from 'antd'; +import classNames from 'classnames'; +import styles from './index.less'; +import { urlToList } from '../_utils/pathTools'; + +const { TabPane } = Tabs; +export function getBreadcrumb(breadcrumbNameMap, url) { + let breadcrumb = breadcrumbNameMap[url]; + if (!breadcrumb) { + Object.keys(breadcrumbNameMap).forEach(item => { + if (pathToRegexp(item).test(url)) { + breadcrumb = breadcrumbNameMap[item]; + } + }); + } + return breadcrumb || {}; +} + +export default class PageHeader extends PureComponent { + static contextTypes = { + routes: PropTypes.array, + params: PropTypes.object, + location: PropTypes.object, + breadcrumbNameMap: PropTypes.object, + }; + onChange = key => { + if (this.props.onTabChange) { + this.props.onTabChange(key); + } + }; + getBreadcrumbProps = () => { + return { + routes: this.props.routes || this.context.routes, + params: this.props.params || this.context.params, + routerLocation: this.props.location || this.context.location, + breadcrumbNameMap: this.props.breadcrumbNameMap || this.context.breadcrumbNameMap, + }; + }; + // Generated according to props + conversionFromProps = () => { + const { breadcrumbList, breadcrumbSeparator, linkElement = 'a' } = this.props; + return ( + + {breadcrumbList.map(item => ( + + {item.href + ? createElement( + linkElement, + { + [linkElement === 'a' ? 'href' : 'to']: item.href, + }, + item.title + ) + : item.title} + + ))} + + ); + }; + conversionFromLocation = (routerLocation, breadcrumbNameMap) => { + const { breadcrumbSeparator, linkElement = 'a' } = this.props; + // Convert the url to an array + const pathSnippets = urlToList(routerLocation.pathname); + // Loop data mosaic routing + const extraBreadcrumbItems = pathSnippets.map((url, index) => { + const currentBreadcrumb = getBreadcrumb(breadcrumbNameMap, url); + const isLinkable = index !== pathSnippets.length - 1 && currentBreadcrumb.component; + return currentBreadcrumb.name && !currentBreadcrumb.hideInBreadcrumb ? ( + + {createElement( + isLinkable ? linkElement : 'span', + { [linkElement === 'a' ? 'href' : 'to']: url }, + currentBreadcrumb.name + )} + + ) : null; + }); + // Add home breadcrumbs to your head + extraBreadcrumbItems.unshift( + + {createElement( + linkElement, + { + [linkElement === 'a' ? 'href' : 'to']: '/', + }, + '首页' + )} + + ); + return ( + + {extraBreadcrumbItems} + + ); + }; + /** + * 将参数转化为面包屑 + * Convert parameters into breadcrumbs + */ + conversionBreadcrumbList = () => { + const { breadcrumbList, breadcrumbSeparator } = this.props; + const { routes, params, routerLocation, breadcrumbNameMap } = this.getBreadcrumbProps(); + if (breadcrumbList && breadcrumbList.length) { + return this.conversionFromProps(); + } + // 如果传入 routes 和 params 属性 + // If pass routes and params attributes + if (routes && params) { + return ( + route.breadcrumbName)} + params={params} + itemRender={this.itemRender} + separator={breadcrumbSeparator} + /> + ); + } + // 根据 location 生成 面包屑 + // Generate breadcrumbs based on location + if (routerLocation && routerLocation.pathname) { + return this.conversionFromLocation(routerLocation, breadcrumbNameMap); + } + return null; + }; + // 渲染Breadcrumb 子节点 + // Render the Breadcrumb child node + itemRender = (route, params, routes, paths) => { + const { linkElement = 'a' } = this.props; + const last = routes.indexOf(route) === routes.length - 1; + return last || !route.component ? ( + {route.breadcrumbName} + ) : ( + createElement( + linkElement, + { + href: paths.join('/') || '/', + to: paths.join('/') || '/', + }, + route.breadcrumbName + ) + ); + }; + + render() { + const { + title, + logo, + action, + content, + extraContent, + tabList, + className, + tabActiveKey, + tabDefaultActiveKey, + tabBarExtraContent, + } = this.props; + const clsString = classNames(styles.pageHeader, className); + const breadcrumb = this.conversionBreadcrumbList(); + const activeKeyProps = {}; + if (tabDefaultActiveKey !== undefined) { + activeKeyProps.defaultActiveKey = tabDefaultActiveKey; + } + if (tabActiveKey !== undefined) { + activeKeyProps.activeKey = tabActiveKey; + } + + return ( +
    + {breadcrumb} +
    + {logo &&
    {logo}
    } +
    +
    + {title &&

    {title}

    } + {action &&
    {action}
    } +
    +
    + {content &&
    {content}
    } + {extraContent &&
    {extraContent}
    } +
    +
    +
    + {tabList && + tabList.length && ( + + {tabList.map(item => )} + + )} +
    + ); + } +} diff --git a/src/components/PageHeader/index.less b/src/components/PageHeader/index.less new file mode 100644 index 0000000..ae0fddc --- /dev/null +++ b/src/components/PageHeader/index.less @@ -0,0 +1,148 @@ +@import '~antd/lib/style/themes/default.less'; + +.pageHeader { + background: @component-background; + padding: 16px 32px 0 32px; + border-bottom: @border-width-base @border-style-base @border-color-split; + + .detail { + display: flex; + } + + .row { + display: flex; + } + + .breadcrumb { + margin-bottom: 16px; + } + + .tabs { + margin: 0 0 -17px -8px; + + :global { + .ant-tabs-bar { + border-bottom: @border-width-base @border-style-base @border-color-split; + } + } + } + + .logo { + flex: 0 1 auto; + margin-right: 16px; + padding-top: 1px; + > img { + width: 28px; + height: 28px; + border-radius: @border-radius-base; + display: block; + } + } + + .title { + font-size: 20px; + font-weight: 500; + color: @heading-color; + } + + .action { + margin-left: 56px; + min-width: 266px; + + :global { + .ant-btn-group:not(:last-child), + .ant-btn:not(:last-child) { + margin-right: 8px; + } + + .ant-btn-group > .ant-btn { + margin-right: 0; + } + } + } + + .title, + .action, + .content, + .extraContent, + .main { + flex: auto; + } + + .title, + .action { + margin-bottom: 16px; + } + + .logo, + .content, + .extraContent { + margin-bottom: 16px; + } + + .action, + .extraContent { + text-align: right; + } + + .extraContent { + margin-left: 88px; + min-width: 242px; + } +} + +@media screen and (max-width: @screen-xl) { + .pageHeader { + .extraContent { + margin-left: 44px; + } + } +} + +@media screen and (max-width: @screen-lg) { + .pageHeader { + .extraContent { + margin-left: 20px; + } + } +} + +@media screen and (max-width: @screen-md) { + .pageHeader { + .row { + display: block; + } + + .action, + .extraContent { + margin-left: 0; + text-align: left; + } + } +} + +@media screen and (max-width: @screen-sm) { + .pageHeader { + .detail { + display: block; + } + } +} + +@media screen and (max-width: @screen-xs) { + .pageHeader { + .action { + :global { + .ant-btn-group, + .ant-btn { + display: block; + margin-bottom: 8px; + } + .ant-btn-group > .ant-btn { + display: inline-block; + margin-bottom: 0; + } + } + } + } +} diff --git a/src/components/PageHeader/index.md b/src/components/PageHeader/index.md new file mode 100644 index 0000000..288f8b6 --- /dev/null +++ b/src/components/PageHeader/index.md @@ -0,0 +1,32 @@ +--- +title: + en-US: PageHeader + zh-CN: PageHeader +subtitle: 页头 +cols: 1 +order: 11 +--- + +页头用来声明页面的主题,包含了用户所关注的最重要的信息,使用户可以快速理解当前页面是什么以及它的功能。 + +## API + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| title | title 区域 | ReactNode | - | +| logo | logo区域 | ReactNode | - | +| action | 操作区,位于 title 行的行尾 | ReactNode | - | +| content | 内容区 | ReactNode | - | +| extraContent | 额外内容区,位于content的右侧 | ReactNode | - | +| breadcrumbList | 面包屑数据,配置了此属性时 `routes` `params` `location` `breadcrumbNameMap` 无效 | array<{title: ReactNode, href?: string}> | - | +| routes | 面包屑相关属性,router 的路由栈信息 | object[] | - | +| params | 面包屑相关属性,路由的参数 | object | - | +| location | 面包屑相关属性,当前的路由信息 | object | - | +| breadcrumbNameMap | 面包屑相关属性,路由的地址-名称映射表 | object | - | +| tabList | tab 标题列表 | array<{key: string, tab: ReactNode}> | - | +| tabActiveKey | 当前高亮的 tab 项 | string | - | +| tabDefaultActiveKey | 默认高亮的 tab 项 | string | 第一项 | +| onTabChange | 切换面板的回调 | (key) => void | - | +| linkElement | 定义链接的元素,默认为 `a`,可传入 react-router 的 Link | string\|ReactElement | - | + +> 面包屑的配置方式有三种,一是直接配置 `breadcrumbList`,二是结合 `react-router@2` `react-router@3`,配置 `routes` 及 `params` 实现,类似 [面包屑 Demo](https://ant.design/components/breadcrumb-cn/#components-breadcrumb-demo-router),三是结合 `react-router@4`,配置 `location` `breadcrumbNameMap`,优先级依次递减,脚手架中使用最后一种。 对于后两种用法,你也可以将 `routes` `params` 及 `location` `breadcrumbNameMap` 放到 context 中,组件会自动获取。 diff --git a/src/components/PageHeader/index.test.js b/src/components/PageHeader/index.test.js new file mode 100644 index 0000000..5238ac5 --- /dev/null +++ b/src/components/PageHeader/index.test.js @@ -0,0 +1,47 @@ +import { getBreadcrumb } from './index'; +import { urlToList } from '../_utils/pathTools'; + +const routerData = { + '/dashboard/analysis': { + name: '分析页', + }, + '/userinfo': { + name: '用户列表', + }, + '/userinfo/:id': { + name: '用户信息', + }, + '/userinfo/:id/addr': { + name: '收货订单', + }, +}; +describe('test getBreadcrumb', () => { + it('Simple url', () => { + expect(getBreadcrumb(routerData, '/dashboard/analysis').name).toEqual('分析页'); + }); + it('Parameters url', () => { + expect(getBreadcrumb(routerData, '/userinfo/2144').name).toEqual('用户信息'); + }); + it('The middle parameter url', () => { + expect(getBreadcrumb(routerData, '/userinfo/2144/addr').name).toEqual('收货订单'); + }); + it('Loop through the parameters', () => { + const urlNameList = urlToList('/userinfo/2144/addr').map(url => { + return getBreadcrumb(routerData, url).name; + }); + expect(urlNameList).toEqual(['用户列表', '用户信息', '收货订单']); + }); + + it('a path', () => { + const urlNameList = urlToList('/userinfo').map(url => { + return getBreadcrumb(routerData, url).name; + }); + expect(urlNameList).toEqual(['用户列表']); + }); + it('Secondary path', () => { + const urlNameList = urlToList('/userinfo/2144').map(url => { + return getBreadcrumb(routerData, url).name; + }); + expect(urlNameList).toEqual(['用户列表', '用户信息']); + }); +}); diff --git a/src/components/Result/demo/classic.md b/src/components/Result/demo/classic.md new file mode 100644 index 0000000..b4ccdcf --- /dev/null +++ b/src/components/Result/demo/classic.md @@ -0,0 +1,80 @@ +--- +order: 1 +title: Classic +--- + +典型结果页面。 + +````jsx +import Result from 'ant-design-pro/lib/Result'; +import { Button, Row, Col, Icon, Steps } from 'antd'; + +const { Step } = Steps; + +const desc1 = ( +
    +
    + 曲丽丽 + +
    +
    2016-12-12 12:32
    +
    +); + +const desc2 = ( +
    +
    + 周毛毛 + +
    + +
    +); + +const extra = ( +
    +
    + 项目名称 +
    + + + 项目 ID: + 23421 + + + 负责人: + 曲丽丽 + + + 生效时间: + 2016-12-12 ~ 2017-12-12 + + + + + + + + +
    +); + +const actions = ( +
    + + + +
    +); + +ReactDOM.render( + +, mountNode); +```` diff --git a/src/components/Result/demo/error.md b/src/components/Result/demo/error.md new file mode 100644 index 0000000..836bd8c --- /dev/null +++ b/src/components/Result/demo/error.md @@ -0,0 +1,39 @@ +--- +order: 2 +title: Failed +--- + +提交失败。 + +````jsx +import Result from 'ant-design-pro/lib/Result'; +import { Button, Icon } from 'antd'; + +const extra = ( +
    +
    + 您提交的内容有如下错误: +
    +
    + 您的账户已被冻结 + 立即解冻 +
    +
    + 您的账户还不具备申请资格 + 立即升级 +
    +
    +); + +const actions = ; + +ReactDOM.render( + +, mountNode); +```` diff --git a/src/components/Result/demo/structure.md b/src/components/Result/demo/structure.md new file mode 100644 index 0000000..7fcecfd --- /dev/null +++ b/src/components/Result/demo/structure.md @@ -0,0 +1,20 @@ +--- +order: 0 +title: Structure +--- + +结构包含 `处理结果`,`补充信息` 以及 `操作建议` 三个部分,其中 `处理结果` 由 `提示图标`,`标题` 和 `结果描述` 组成。 + +````jsx +import Result from 'ant-design-pro/lib/Result'; + +ReactDOM.render( + 标题
    } + description={
    结果描述
    } + extra="其他补充信息,自带灰底效果" + actions={
    操作建议,一般放置按钮组
    } + /> +, mountNode); +```` diff --git a/src/components/Result/index.d.ts b/src/components/Result/index.d.ts new file mode 100644 index 0000000..0c34c25 --- /dev/null +++ b/src/components/Result/index.d.ts @@ -0,0 +1,11 @@ +import * as React from 'react'; +export interface IResultProps { + type: 'success' | 'error'; + title: React.ReactNode; + description?: React.ReactNode; + extra?: React.ReactNode; + actions?: React.ReactNode; + style?: React.CSSProperties; +} + +export default class Result extends React.Component {} diff --git a/src/components/Result/index.js b/src/components/Result/index.js new file mode 100644 index 0000000..ada2f5f --- /dev/null +++ b/src/components/Result/index.js @@ -0,0 +1,29 @@ +import React from 'react'; +import classNames from 'classnames'; +import { Icon } from 'antd'; +import styles from './index.less'; + +export default function Result({ + className, + type, + title, + description, + extra, + actions, + ...restProps +}) { + const iconMap = { + error: , + success: , + }; + const clsString = classNames(styles.result, className); + return ( +
    +
    {iconMap[type]}
    +
    {title}
    + {description &&
    {description}
    } + {extra &&
    {extra}
    } + {actions &&
    {actions}
    } +
    + ); +} diff --git a/src/components/Result/index.less b/src/components/Result/index.less new file mode 100644 index 0000000..9953392 --- /dev/null +++ b/src/components/Result/index.less @@ -0,0 +1,51 @@ +@import '~antd/lib/style/themes/default.less'; + +.result { + text-align: center; + width: 72%; + margin: 0 auto; + + .icon { + font-size: 72px; + line-height: 72px; + margin-bottom: 24px; + + & > .success { + color: @success-color; + } + + & > .error { + color: @error-color; + } + } + + .title { + font-size: 24px; + color: @heading-color; + font-weight: 500; + line-height: 32px; + margin-bottom: 16px; + } + + .description { + font-size: 14px; + line-height: 22px; + color: @text-color-secondary; + margin-bottom: 24px; + } + + .extra { + background: #fafafa; + padding: 24px 40px; + border-radius: @border-radius-sm; + text-align: left; + } + + .actions { + margin-top: 32px; + + button:not(:last-child) { + margin-right: 8px; + } + } +} diff --git a/src/components/Result/index.md b/src/components/Result/index.md new file mode 100644 index 0000000..dc11206 --- /dev/null +++ b/src/components/Result/index.md @@ -0,0 +1,20 @@ +--- +title: + en-US: Result + zh-CN: Result +subtitle: 处理结果 +cols: 1 +order: 12 +--- + +结果页用于对用户进行的一系列任务处理结果进行反馈。 + +## API + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| type | 类型,不同类型自带对应的图标 | Enum {'success', 'error'} | - | +| title | 标题 | ReactNode | - | +| description | 结果描述 | ReactNode | - | +| extra | 补充信息,有默认的灰色背景 | ReactNode | - | +| actions | 操作建议,推荐放置跳转链接,按钮组等 | ReactNode | - | diff --git a/src/components/SiderMenu/SiderMenu.js b/src/components/SiderMenu/SiderMenu.js new file mode 100644 index 0000000..639d4ad --- /dev/null +++ b/src/components/SiderMenu/SiderMenu.js @@ -0,0 +1,235 @@ +import React, { PureComponent } from 'react'; +import { Layout, Menu, Icon } from 'antd'; +import pathToRegexp from 'path-to-regexp'; +import { Link } from 'dva/router'; +import styles from './index.less'; +import { urlToList } from '../_utils/pathTools'; + +const { Sider } = Layout; +const { SubMenu } = Menu; + +// Allow menu.js config icon as string or ReactNode +// icon: 'setting', +// icon: 'http://demo.com/icon.png', +// icon: , +const getIcon = icon => { + if (typeof icon === 'string' && icon.indexOf('http') === 0) { + return icon; + } + if (typeof icon === 'string') { + return ; + } + return icon; +}; + +export const getMeunMatcheys = (flatMenuKeys, path) => { + return flatMenuKeys.filter(item => { + return pathToRegexp(item).test(path); + }); +}; + +export default class SiderMenu extends PureComponent { + constructor(props) { + super(props); + this.menus = props.menuData; + this.flatMenuKeys = this.getFlatMenuKeys(props.menuData); + this.state = { + openKeys: this.getDefaultCollapsedSubMenus(props), + }; + } + componentWillReceiveProps(nextProps) { + if (nextProps.location.pathname !== this.props.location.pathname) { + this.setState({ + openKeys: this.getDefaultCollapsedSubMenus(nextProps), + }); + } + } + /** + * Convert pathname to openKeys + * /list/search/articles = > ['list','/list/search'] + * @param props + */ + getDefaultCollapsedSubMenus(props) { + const { location: { pathname } } = props || this.props; + return urlToList(pathname) + .map(item => { + return getMeunMatcheys(this.flatMenuKeys, item)[0]; + }) + .filter(item => item); + } + /** + * Recursively flatten the data + * [{path:string},{path:string}] => {path,path2} + * @param menus + */ + getFlatMenuKeys(menus) { + let keys = []; + menus.forEach(item => { + if (item.children) { + keys = keys.concat(this.getFlatMenuKeys(item.children)); + } + keys.push(item.path); + }); + return keys; + } + /** + * 判断是否是http链接.返回 Link 或 a + * Judge whether it is http link.return a or Link + * @memberof SiderMenu + */ + getMenuItemPath = item => { + const itemPath = this.conversionPath(item.path); + const icon = getIcon(item.icon); + const { target, name } = item; + // Is it a http link + if (/^https?:\/\//.test(itemPath)) { + return ( + + {icon} + {name} + + ); + } + return ( + { + this.props.onCollapse(true); + } + : undefined + } + > + {icon} + {name} + + ); + }; + /** + * get SubMenu or Item + */ + getSubMenuOrItem = item => { + if (item.children && item.children.some(child => child.name)) { + const childrenItems = this.getNavMenuItems(item.children); + // 当无子菜单时就不展示菜单 + if (childrenItems && childrenItems.length > 0) { + return ( + + {getIcon(item.icon)} + {item.name} + + ) : ( + item.name + ) + } + key={item.path} + > + {childrenItems} + + ); + } + return null; + } else { + return {this.getMenuItemPath(item)}; + } + }; + /** + * 获得菜单子节点 + * @memberof SiderMenu + */ + getNavMenuItems = menusData => { + if (!menusData) { + return []; + } + return menusData + .filter(item => item.name && !item.hideInMenu) + .map(item => { + // make dom + const ItemDom = this.getSubMenuOrItem(item); + return this.checkPermissionItem(item.authority, ItemDom); + }) + .filter(item => item); + }; + // Get the currently selected menu + getSelectedMenuKeys = () => { + const { location: { pathname } } = this.props; + return urlToList(pathname).map(itemPath => getMeunMatcheys(this.flatMenuKeys, itemPath).pop()); + }; + // conversion Path + // 转化路径 + conversionPath = path => { + if (path && path.indexOf('http') === 0) { + return path; + } else { + return `/${path || ''}`.replace(/\/+/g, '/'); + } + }; + // permission to check + checkPermissionItem = (authority, ItemDom) => { + if (this.props.Authorized && this.props.Authorized.check) { + const { check } = this.props.Authorized; + return check(authority, ItemDom); + } + return ItemDom; + }; + isMainMenu = key => { + return this.menus.some(item => key && (item.key === key || item.path === key)); + }; + handleOpenChange = openKeys => { + const lastOpenKey = openKeys[openKeys.length - 1]; + const moreThanOne = openKeys.filter(openKey => this.isMainMenu(openKey)).length > 1; + this.setState({ + openKeys: moreThanOne ? [lastOpenKey] : [...openKeys], + }); + }; + render() { + const { logo, collapsed, onCollapse } = this.props; + const { openKeys } = this.state; + // Don't show popup menu when it is been collapsed + const menuProps = collapsed + ? {} + : { + openKeys, + }; + // if pathname can't match, use the nearest parent's key + let selectedKeys = this.getSelectedMenuKeys(); + if (!selectedKeys.length) { + selectedKeys = [openKeys[openKeys.length - 1]]; + } + return ( + +
    + + logo +

    Ant Design Pro

    + +
    + + {this.getNavMenuItems(this.menus)} + +
    + ); + } +} diff --git a/src/components/SiderMenu/SilderMenu.test.js b/src/components/SiderMenu/SilderMenu.test.js new file mode 100644 index 0000000..d74a5f5 --- /dev/null +++ b/src/components/SiderMenu/SilderMenu.test.js @@ -0,0 +1,24 @@ +import { getMeunMatcheys } from './SiderMenu'; + +const meun = ['/dashboard', '/userinfo', '/dashboard/name', '/userinfo/:id', '/userinfo/:id/info']; + +describe('test meun match', () => { + it('simple path', () => { + expect(getMeunMatcheys(meun, '/dashboard')).toEqual(['/dashboard']); + }); + it('error path', () => { + expect(getMeunMatcheys(meun, '/dashboardname')).toEqual([]); + }); + + it('Secondary path', () => { + expect(getMeunMatcheys(meun, '/dashboard/name')).toEqual(['/dashboard/name']); + }); + + it('Parameter path', () => { + expect(getMeunMatcheys(meun, '/userinfo/2144')).toEqual(['/userinfo/:id']); + }); + + it('three parameter path', () => { + expect(getMeunMatcheys(meun, '/userinfo/2144/info')).toEqual(['/userinfo/:id/info']); + }); +}); diff --git a/src/components/SiderMenu/index.js b/src/components/SiderMenu/index.js new file mode 100644 index 0000000..640d6ba --- /dev/null +++ b/src/components/SiderMenu/index.js @@ -0,0 +1,22 @@ +import 'rc-drawer-menu/assets/index.css'; +import React from 'react'; +import DrawerMenu from 'rc-drawer-menu'; +import SiderMenu from './SiderMenu'; + +export default props => + props.isMobile ? ( + { + props.onCollapse(true); + }} + width="256px" + > + + + ) : ( + + ); diff --git a/src/components/SiderMenu/index.less b/src/components/SiderMenu/index.less new file mode 100644 index 0000000..9fa1a91 --- /dev/null +++ b/src/components/SiderMenu/index.less @@ -0,0 +1,71 @@ +@import '~antd/lib/style/themes/default.less'; +@ease-in-out-circ: cubic-bezier(0.78, 0.14, 0.15, 0.86); +.logo { + height: 64px; + position: relative; + line-height: 64px; + padding-left: (@menu-collapsed-width - 32px) / 2; + transition: all 0.3s; + background: #002140; + overflow: hidden; + img { + display: inline-block; + vertical-align: middle; + height: 32px; + } + h1 { + color: white; + display: inline-block; + vertical-align: middle; + font-size: 20px; + margin: 0 0 0 12px; + font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif; + font-weight: 600; + } +} + +.sider { + min-height: 100vh; + box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35); + position: relative; + z-index: 10; + &.ligth { + background-color: white; + .logo { + background: white; + h1 { + color: #002140; + } + } + } +} + +.icon { + width: 14px; + margin-right: 10px; +} + +:global { + .drawer .drawer-content { + background: #001529; + } + .ant-menu-inline-collapsed { + & > .ant-menu-item .sider-menu-item-img + span, + & + > .ant-menu-item-group + > .ant-menu-item-group-list + > .ant-menu-item + .sider-menu-item-img + + span, + & > .ant-menu-submenu > .ant-menu-submenu-title .sider-menu-item-img + span { + max-width: 0; + display: inline-block; + opacity: 0; + } + } + .ant-menu-item .sider-menu-item-img + span, + .ant-menu-submenu-title .sider-menu-item-img + span { + transition: opacity 0.3s @ease-in-out, width 0.3s @ease-in-out; + opacity: 1; + } +} diff --git a/src/components/StandardFormRow/index.js b/src/components/StandardFormRow/index.js new file mode 100644 index 0000000..ec8ae1d --- /dev/null +++ b/src/components/StandardFormRow/index.js @@ -0,0 +1,22 @@ +import React from 'react'; +import classNames from 'classnames'; +import styles from './index.less'; + +export default ({ title, children, last, block, grid, ...rest }) => { + const cls = classNames(styles.standardFormRow, { + [styles.standardFormRowBlock]: block, + [styles.standardFormRowLast]: last, + [styles.standardFormRowGrid]: grid, + }); + + return ( +
    + {title && ( +
    + {title} +
    + )} +
    {children}
    +
    + ); +}; diff --git a/src/components/StandardFormRow/index.less b/src/components/StandardFormRow/index.less new file mode 100644 index 0000000..83ab019 --- /dev/null +++ b/src/components/StandardFormRow/index.less @@ -0,0 +1,72 @@ +@import '~antd/lib/style/themes/default.less'; + +.standardFormRow { + border-bottom: 1px dashed @border-color-split; + padding-bottom: 16px; + margin-bottom: 16px; + display: flex; + :global { + .ant-form-item { + margin-right: 24px; + } + .ant-form-item-label label { + color: @text-color; + margin-right: 0; + } + .ant-form-item-label, + .ant-form-item-control { + padding: 0; + line-height: 32px; + } + } + .label { + color: @heading-color; + font-size: @font-size-base; + margin-right: 24px; + flex: 0 0 auto; + text-align: right; + & > span { + display: inline-block; + height: 32px; + line-height: 32px; + &:after { + content: ':'; + } + } + } + .content { + flex: 1 1 0; + :global { + .ant-form-item:last-child { + margin-right: 0; + } + } + } +} + +.standardFormRowLast { + border: none; + padding-bottom: 0; + margin-bottom: 0; +} + +.standardFormRowBlock { + :global { + .ant-form-item, + div.ant-form-item-control-wrapper { + display: block; + } + } +} + +.standardFormRowGrid { + :global { + .ant-form-item, + div.ant-form-item-control-wrapper { + display: block; + } + .ant-form-item-label { + float: left; + } + } +} diff --git a/src/components/StandardTable/index.js b/src/components/StandardTable/index.js new file mode 100644 index 0000000..281d60e --- /dev/null +++ b/src/components/StandardTable/index.js @@ -0,0 +1,120 @@ +import React, { PureComponent, Fragment } from 'react'; +import { Table, Alert } from 'antd'; +import styles from './index.less'; + +function initTotalList(columns) { + const totalList = []; + columns.forEach(column => { + if (column.needTotal) { + totalList.push({ ...column, total: 0 }); + } + }); + return totalList; +} + +class StandardTable extends PureComponent { + constructor(props) { + super(props); + const { columns } = props; + const needTotalList = initTotalList(columns); + + this.state = { + selectedRowKeys: [], + needTotalList, + }; + } + + componentWillReceiveProps(nextProps) { + // clean state + if (nextProps.selectedRows.length === 0) { + const needTotalList = initTotalList(nextProps.columns); + this.setState({ + selectedRowKeys: [], + needTotalList, + }); + } + } + + handleRowSelectChange = (selectedRowKeys, selectedRows) => { + let needTotalList = [...this.state.needTotalList]; + needTotalList = needTotalList.map(item => { + return { + ...item, + total: selectedRows.reduce((sum, val) => { + return sum + parseFloat(val[item.dataIndex], 10); + }, 0), + }; + }); + + if (this.props.onSelectRow) { + this.props.onSelectRow(selectedRows); + } + + this.setState({ selectedRowKeys, needTotalList }); + }; + + handleTableChange = (pagination, filters, sorter) => { + this.props.onChange(pagination, filters, sorter); + }; + + cleanSelectedKeys = () => { + this.handleRowSelectChange([], []); + }; + + render() { + const { selectedRowKeys, needTotalList } = this.state; + const { data: { list, pagination }, loading, columns, rowKey } = this.props; + + const paginationProps = { + showSizeChanger: true, + showQuickJumper: true, + ...pagination, + }; + + const rowSelection = { + selectedRowKeys, + onChange: this.handleRowSelectChange, + getCheckboxProps: record => ({ + disabled: record.disabled, + }), + }; + + return ( +
    +
    + + 已选择 {selectedRowKeys.length} 项   + {needTotalList.map(item => ( + + {item.title}总计  + + {item.render ? item.render(item.total) : item.total} + + + ))} + + 清空 + + + } + type="info" + showIcon + /> +
    + + + ); + } +} + +export default StandardTable; diff --git a/src/components/StandardTable/index.less b/src/components/StandardTable/index.less new file mode 100644 index 0000000..817be99 --- /dev/null +++ b/src/components/StandardTable/index.less @@ -0,0 +1,13 @@ +@import '~antd/lib/style/themes/default.less'; + +.standardTable { + :global { + .ant-table-pagination { + margin-top: 24px; + } + } + + .tableAlert { + margin-bottom: 16px; + } +} diff --git a/src/components/TagSelect/TagSelectOption.d.ts b/src/components/TagSelect/TagSelectOption.d.ts new file mode 100644 index 0000000..366b297 --- /dev/null +++ b/src/components/TagSelect/TagSelectOption.d.ts @@ -0,0 +1,8 @@ +import * as React from 'react'; + +export interface ITagSelectOptionProps { + value: string | number; + style?: React.CSSProperties; +} + +export default class TagSelectOption extends React.Component {} diff --git a/src/components/TagSelect/demo/expandable.md b/src/components/TagSelect/demo/expandable.md new file mode 100644 index 0000000..c45a30a --- /dev/null +++ b/src/components/TagSelect/demo/expandable.md @@ -0,0 +1,31 @@ +--- +order: 1 +title: 可展开和收起 +--- + +使用 `expandable` 属性,让标签组可以收起,避免过高。 + +````jsx +import TagSelect from 'ant-design-pro/lib/TagSelect'; + +function handleFormSubmit(checkedValue) { + console.log(checkedValue); +} + +ReactDOM.render( + + 类目一 + 类目二 + 类目三 + 类目四 + 类目五 + 类目六 + 类目七 + 类目八 + 类目九 + 类目十 + 类目十一 + 类目十二 + +, mountNode); +```` diff --git a/src/components/TagSelect/demo/simple.md b/src/components/TagSelect/demo/simple.md new file mode 100644 index 0000000..9e7a13a --- /dev/null +++ b/src/components/TagSelect/demo/simple.md @@ -0,0 +1,25 @@ +--- +order: 0 +title: 基础样例 +--- + +结合 `Tag` 的 `TagSelect` 组件,方便的应用于筛选类目的业务场景中。 + +````jsx +import TagSelect from 'ant-design-pro/lib/TagSelect'; + +function handleFormSubmit(checkedValue) { + console.log(checkedValue); +} + +ReactDOM.render( + + 类目一 + 类目二 + 类目三 + 类目四 + 类目五 + 类目六 + +, mountNode); +```` diff --git a/src/components/TagSelect/index.d.ts b/src/components/TagSelect/index.d.ts new file mode 100644 index 0000000..b17d34d --- /dev/null +++ b/src/components/TagSelect/index.d.ts @@ -0,0 +1,16 @@ +import * as React from 'react'; +import TagSelectOption from './TagSelectOption'; + +export interface ITagSelectProps { + onChange?: (value: string[]) => void; + expandable?: boolean; + value?: string[] | number[]; + style?: React.CSSProperties; +} + +export default class TagSelect extends React.Component { + public static Option: typeof TagSelectOption; + private children: + | React.ReactElement + | Array>; +} diff --git a/src/components/TagSelect/index.js b/src/components/TagSelect/index.js new file mode 100644 index 0000000..773b9c8 --- /dev/null +++ b/src/components/TagSelect/index.js @@ -0,0 +1,120 @@ +import React, { Component } from 'react'; +import classNames from 'classnames'; +import { Tag, Icon } from 'antd'; + +import styles from './index.less'; + +const { CheckableTag } = Tag; + +const TagSelectOption = ({ children, checked, onChange, value }) => ( + onChange(value, state)}> + {children} + +); + +TagSelectOption.isTagSelectOption = true; + +class TagSelect extends Component { + state = { + expand: false, + value: this.props.value || this.props.defaultValue || [], + }; + componentWillReceiveProps(nextProps) { + if ('value' in nextProps && nextProps.value) { + this.setState({ value: nextProps.value }); + } + } + + onChange = value => { + const { onChange } = this.props; + if (!('value' in this.props)) { + this.setState({ value }); + } + if (onChange) { + onChange(value); + } + }; + + onSelectAll = checked => { + let checkedTags = []; + if (checked) { + checkedTags = this.getAllTags(); + } + this.onChange(checkedTags); + }; + + getAllTags() { + let { children } = this.props; + children = React.Children.toArray(children); + const checkedTags = children + .filter(child => this.isTagSelectOption(child)) + .map(child => child.props.value); + return checkedTags || []; + } + + handleTagChange = (value, checked) => { + const checkedTags = [...this.state.value]; + + const index = checkedTags.indexOf(value); + if (checked && index === -1) { + checkedTags.push(value); + } else if (!checked && index > -1) { + checkedTags.splice(index, 1); + } + this.onChange(checkedTags); + }; + + handleExpand = () => { + this.setState({ + expand: !this.state.expand, + }); + }; + + isTagSelectOption = node => { + return ( + node && + node.type && + (node.type.isTagSelectOption || node.type.displayName === 'TagSelectOption') + ); + }; + + render() { + const { value, expand } = this.state; + const { children, className, style, expandable } = this.props; + + const checkedAll = this.getAllTags().length === value.length; + + const cls = classNames(styles.tagSelect, className, { + [styles.hasExpandTag]: expandable, + [styles.expanded]: expand, + }); + return ( +
    + + 全部 + + {value && + React.Children.map(children, child => { + if (this.isTagSelectOption(child)) { + return React.cloneElement(child, { + key: `tag-select-${child.props.value}`, + value: child.props.value, + checked: value.indexOf(child.props.value) > -1, + onChange: this.handleTagChange, + }); + } + return child; + })} + {expandable && ( + + {expand ? '收起' : '展开'} + + )} +
    + ); + } +} + +TagSelect.Option = TagSelectOption; + +export default TagSelect; diff --git a/src/components/TagSelect/index.less b/src/components/TagSelect/index.less new file mode 100644 index 0000000..834b39a --- /dev/null +++ b/src/components/TagSelect/index.less @@ -0,0 +1,33 @@ +@import '~antd/lib/style/themes/default.less'; + +.tagSelect { + user-select: none; + margin-left: -8px; + position: relative; + overflow: hidden; + max-height: 32px; + line-height: 32px; + transition: all 0.3s; + :global { + .ant-tag { + padding: 0 8px; + margin-right: 24px; + font-size: @font-size-base; + } + } + &.expanded { + transition: all 0.3s; + max-height: 200px; + } + .trigger { + position: absolute; + top: 0; + right: 0; + i { + font-size: 12px; + } + } + &.hasExpandTag { + padding-right: 50px; + } +} diff --git a/src/components/TagSelect/index.md b/src/components/TagSelect/index.md new file mode 100644 index 0000000..608219c --- /dev/null +++ b/src/components/TagSelect/index.md @@ -0,0 +1,29 @@ +--- +title: + en-US: TagSelect + zh-CN: TagSelect +subtitle: 标签选择器 +cols: 1 +order: 13 +--- + +可进行多选,带折叠收起和展开更多功能,常用于对列表进行筛选。 + +## API + +### TagSelect + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| value |选中的项 |string[] \| number[] | | +| defaultValue |默认选中的项 |string[] \| number[] | | +| onChange | 标签选择的回调函数 | Function(checkedTags) | | +| expandable | 是否展示 `展开/收起` 按钮 | Boolean | false | + + +### TagSelectOption + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| value | TagSelect的值 | string\| number | - | +| children | tag的内容 | string \| ReactNode | - | diff --git a/src/components/Trend/demo/basic.md b/src/components/Trend/demo/basic.md new file mode 100644 index 0000000..82afcda --- /dev/null +++ b/src/components/Trend/demo/basic.md @@ -0,0 +1,17 @@ +--- +order: 0 +title: 演示 +--- + +在数值背后添加一个小图标来标识涨跌情况。 + +````jsx +import Trend from 'ant-design-pro/lib/Trend'; + +ReactDOM.render( +
    + 12% + 11% +
    +, mountNode); +```` diff --git a/src/components/Trend/index.d.ts b/src/components/Trend/index.d.ts new file mode 100644 index 0000000..fafcb05 --- /dev/null +++ b/src/components/Trend/index.d.ts @@ -0,0 +1,9 @@ +import * as React from 'react'; + +export interface ITrendProps { + colorful?: boolean; + flag: 'up' | 'down'; + style?: React.CSSProperties; +} + +export default class Trend extends React.Component {} diff --git a/src/components/Trend/index.js b/src/components/Trend/index.js new file mode 100644 index 0000000..cde4ef6 --- /dev/null +++ b/src/components/Trend/index.js @@ -0,0 +1,26 @@ +import React from 'react'; +import { Icon } from 'antd'; +import classNames from 'classnames'; +import styles from './index.less'; + +const Trend = ({ colorful = true, flag, children, className, ...rest }) => { + const classString = classNames( + styles.trendItem, + { + [styles.trendItemGrey]: !colorful, + }, + className + ); + return ( +
    + {children} + {flag && ( + + + + )} +
    + ); +}; + +export default Trend; diff --git a/src/components/Trend/index.less b/src/components/Trend/index.less new file mode 100644 index 0000000..ea66fcf --- /dev/null +++ b/src/components/Trend/index.less @@ -0,0 +1,30 @@ +@import '~antd/lib/style/themes/default.less'; + +.trendItem { + display: inline-block; + font-size: @font-size-base; + line-height: 22px; + + .up, + .down { + margin-left: 4px; + position: relative; + top: 1px; + i { + font-size: 12px; + transform: scale(0.83); + } + } + .up { + color: @red-6; + } + .down { + color: @green-6; + top: -1px; + } + + &.trendItemGrey .up, + &.trendItemGrey .down { + color: @text-color; + } +} diff --git a/src/components/Trend/index.md b/src/components/Trend/index.md new file mode 100644 index 0000000..683ed61 --- /dev/null +++ b/src/components/Trend/index.md @@ -0,0 +1,21 @@ +--- +title: + en-US: Trend + zh-CN: Trend +subtitle: 趋势标记 +cols: 1 +order: 14 +--- + +趋势符号,标记上升和下降趋势。通常用绿色代表“好”,红色代表“不好”,股票涨跌场景除外。 + +## API + +```html +50% +``` + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| colorful | 是否彩色标记 | Boolean | true | +| flag | 上升下降标识:`up|down` | string | - | diff --git a/src/components/_utils/pathTools.js b/src/components/_utils/pathTools.js new file mode 100644 index 0000000..7d49400 --- /dev/null +++ b/src/components/_utils/pathTools.js @@ -0,0 +1,7 @@ +// /userinfo/2144/id => ['/userinfo','/useinfo/2144,'/userindo/2144/id'] +export function urlToList(url) { + const urllist = url.split('/').filter(i => i); + return urllist.map((urlItem, index) => { + return `/${urllist.slice(0, index + 1).join('/')}`; + }); +} diff --git a/src/components/_utils/pathTools.test.js b/src/components/_utils/pathTools.test.js new file mode 100644 index 0000000..a9b9315 --- /dev/null +++ b/src/components/_utils/pathTools.test.js @@ -0,0 +1,17 @@ +import { urlToList } from './pathTools'; + +describe('test urlToList', () => { + it('A path', () => { + expect(urlToList('/userinfo')).toEqual(['/userinfo']); + }); + it('Secondary path', () => { + expect(urlToList('/userinfo/2144')).toEqual(['/userinfo', '/userinfo/2144']); + }); + it('Three paths', () => { + expect(urlToList('/userinfo/2144/addr')).toEqual([ + '/userinfo', + '/userinfo/2144', + '/userinfo/2144/addr', + ]); + }); +}); diff --git a/src/e2e/home.e2e.js b/src/e2e/home.e2e.js new file mode 100644 index 0000000..1a5d951 --- /dev/null +++ b/src/e2e/home.e2e.js @@ -0,0 +1,14 @@ +import puppeteer from 'puppeteer'; + +describe('Homepage', () => { + it('it should have logo text', async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('http://localhost:8000'); + await page.waitForSelector('h1'); + const text = await page.evaluate(() => document.body.innerHTML); + expect(text).toContain('

    Ant Design Pro

    '); + await page.close(); + browser.close(); + }); +}); diff --git a/src/e2e/login.e2e.js b/src/e2e/login.e2e.js new file mode 100644 index 0000000..5211a3b --- /dev/null +++ b/src/e2e/login.e2e.js @@ -0,0 +1,36 @@ +import puppeteer from 'puppeteer'; + +describe('Login', () => { + let browser; + let page; + + beforeAll(async () => { + browser = await puppeteer.launch(); + }); + + beforeEach(async () => { + page = await browser.newPage(); + await page.goto('http://localhost:8000/#/user/login'); + await page.evaluate(() => window.localStorage.setItem('antd-pro-authority', 'guest')); + }); + + afterEach(() => page.close()); + + it('should login with failure', async () => { + await page.type('#userName', 'mockuser'); + await page.type('#password', 'wrong_password'); + await page.click('button[type="submit"]'); + await page.waitForSelector('.ant-alert-error'); // should display error + }); + + it('should login successfully', async () => { + await page.type('#userName', 'admin'); + await page.type('#password', '888888'); + await page.click('button[type="submit"]'); + await page.waitForSelector('.ant-layout-sider h1'); // should display error + const text = await page.evaluate(() => document.body.innerHTML); + expect(text).toContain('

    Ant Design Pro

    '); + }); + + afterAll(() => browser.close()); +}); diff --git a/src/index.ejs b/src/index.ejs new file mode 100644 index 0000000..586f62e --- /dev/null +++ b/src/index.ejs @@ -0,0 +1,16 @@ + + + + + + + + Ant Design Pro + + + + +
    + + + \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..cfa30d2 --- /dev/null +++ b/src/index.js @@ -0,0 +1,30 @@ +import '@babel/polyfill'; +import 'url-polyfill'; +import dva from 'dva'; + +import createHistory from 'history/createHashHistory'; +// user BrowserHistory +// import createHistory from 'history/createBrowserHistory'; +import createLoading from 'dva-loading'; +import 'moment/locale/zh-cn'; +import './rollbar'; + +import './index.less'; +// 1. Initialize +const app = dva({ + history: createHistory(), +}); + +// 2. Plugins +app.use(createLoading()); + +// 3. Register global model +app.model(require('./models/global').default); + +// 4. Router +app.router(require('./router').default); + +// 5. Start +app.start('#root'); + +export default app._store; // eslint-disable-line diff --git a/src/index.less b/src/index.less new file mode 100644 index 0000000..5cfe914 --- /dev/null +++ b/src/index.less @@ -0,0 +1,31 @@ +html, +body, +:global(#root) { + height: 100%; +} + +:global(.ant-layout) { + min-height: 100%; +} + +canvas { + display: block; +} + +body { + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.globalSpin { + width: 100%; + margin: 40px 0 !important; +} + +// temp fix for https://github.com/ant-design/ant-design/commit/a1fafb5b727b62cb0be29ce6e9eca8f579d4f8b7 +:global { + .ant-spin-container { + overflow: visible !important; + } +} diff --git a/src/layouts/BasicLayout.js b/src/layouts/BasicLayout.js new file mode 100644 index 0000000..ec5e2f7 --- /dev/null +++ b/src/layouts/BasicLayout.js @@ -0,0 +1,281 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { Layout, Icon, message } from 'antd'; +import DocumentTitle from 'react-document-title'; +import { connect } from 'dva'; +import { Route, Redirect, Switch, routerRedux } from 'dva/router'; +import { ContainerQuery } from 'react-container-query'; +import classNames from 'classnames'; +import { enquireScreen, unenquireScreen } from 'enquire-js'; +import GlobalHeader from '../components/GlobalHeader'; +import GlobalFooter from '../components/GlobalFooter'; +import SiderMenu from '../components/SiderMenu'; +import NotFound from '../routes/Exception/404'; +import { getRoutes } from '../utils/utils'; +import Authorized from '../utils/Authorized'; +import { getMenuData } from '../common/menu'; +import logo from '../assets/logo.svg'; + +const { Content, Header, Footer } = Layout; +const { AuthorizedRoute, check } = Authorized; + +/** + * 根据菜单取得重定向地址. + */ +const redirectData = []; +const getRedirect = item => { + if (item && item.children) { + if (item.children[0] && item.children[0].path) { + redirectData.push({ + from: `${item.path}`, + to: `${item.children[0].path}`, + }); + item.children.forEach(children => { + getRedirect(children); + }); + } + } +}; +getMenuData().forEach(getRedirect); + +/** + * 获取面包屑映射 + * @param {Object} menuData 菜单配置 + * @param {Object} routerData 路由配置 + */ +const getBreadcrumbNameMap = (menuData, routerData) => { + const result = {}; + const childResult = {}; + for (const i of menuData) { + if (!routerData[i.path]) { + result[i.path] = i; + } + if (i.children) { + Object.assign(childResult, getBreadcrumbNameMap(i.children, routerData)); + } + } + return Object.assign({}, routerData, result, childResult); +}; + +const query = { + 'screen-xs': { + maxWidth: 575, + }, + 'screen-sm': { + minWidth: 576, + maxWidth: 767, + }, + 'screen-md': { + minWidth: 768, + maxWidth: 991, + }, + 'screen-lg': { + minWidth: 992, + maxWidth: 1199, + }, + 'screen-xl': { + minWidth: 1200, + }, +}; + +let isMobile; +enquireScreen(b => { + isMobile = b; +}); + +class BasicLayout extends React.PureComponent { + static childContextTypes = { + location: PropTypes.object, + breadcrumbNameMap: PropTypes.object, + }; + state = { + isMobile, + }; + getChildContext() { + const { location, routerData } = this.props; + return { + location, + breadcrumbNameMap: getBreadcrumbNameMap(getMenuData(), routerData), + }; + } + componentDidMount() { + this.enquireHandler = enquireScreen(mobile => { + this.setState({ + isMobile: mobile, + }); + }); + this.props.dispatch({ + type: 'user/fetchCurrent', + }); + } + componentWillUnmount() { + unenquireScreen(this.enquireHandler); + } + getPageTitle() { + const { routerData, location } = this.props; + const { pathname } = location; + let title = 'Ant Design Pro'; + if (routerData[pathname] && routerData[pathname].name) { + title = `${routerData[pathname].name} - Ant Design Pro`; + } + return title; + } + getBashRedirect = () => { + // According to the url parameter to redirect + // 这里是重定向的,重定向到 url 的 redirect 参数所示地址 + const urlParams = new URL(window.location.href); + + const redirect = urlParams.searchParams.get('redirect'); + // Remove the parameters in the url + if (redirect) { + urlParams.searchParams.delete('redirect'); + window.history.replaceState(null, 'redirect', urlParams.href); + } else { + const { routerData } = this.props; + // get the first authorized route path in routerData + const authorizedPath = Object.keys(routerData).find( + item => check(routerData[item].authority, item) && item !== '/' + ); + return authorizedPath; + } + return redirect; + }; + handleMenuCollapse = collapsed => { + this.props.dispatch({ + type: 'global/changeLayoutCollapsed', + payload: collapsed, + }); + }; + handleNoticeClear = type => { + message.success(`清空了${type}`); + this.props.dispatch({ + type: 'global/clearNotices', + payload: type, + }); + }; + handleMenuClick = ({ key }) => { + if (key === 'triggerError') { + this.props.dispatch(routerRedux.push('/exception/trigger')); + return; + } + if (key === 'logout') { + this.props.dispatch({ + type: 'login/logout', + }); + } + }; + handleNoticeVisibleChange = visible => { + if (visible) { + this.props.dispatch({ + type: 'global/fetchNotices', + }); + } + }; + render() { + const { + currentUser, + collapsed, + fetchingNotices, + notices, + routerData, + match, + location, + } = this.props; + const bashRedirect = this.getBashRedirect(); + const layout = ( + + + +
    + +
    + + + {redirectData.map(item => ( + + ))} + {getRoutes(match.path, routerData).map(item => ( + + ))} + + + + +
    + , + href: 'https://github.com/ant-design/ant-design-pro', + blankTarget: true, + }, + { + key: 'Ant Design', + title: 'Ant Design', + href: 'http://ant.design', + blankTarget: true, + }, + ]} + copyright={ + + Copyright 2018 蚂蚁金服体验技术部出品 + + } + /> +
    +
    +
    + ); + + return ( + + + {params =>
    {layout}
    } +
    +
    + ); + } +} + +export default connect(({ user, global, loading }) => ({ + currentUser: user.currentUser, + collapsed: global.collapsed, + fetchingNotices: loading.effects['global/fetchNotices'], + notices: global.notices, +}))(BasicLayout); diff --git a/src/layouts/BlankLayout.js b/src/layouts/BlankLayout.js new file mode 100644 index 0000000..505270f --- /dev/null +++ b/src/layouts/BlankLayout.js @@ -0,0 +1,3 @@ +import React from 'react'; + +export default props =>
    ; diff --git a/src/layouts/PageHeaderLayout.js b/src/layouts/PageHeaderLayout.js new file mode 100644 index 0000000..2c615e8 --- /dev/null +++ b/src/layouts/PageHeaderLayout.js @@ -0,0 +1,12 @@ +import React from 'react'; +import { Link } from 'dva/router'; +import PageHeader from '../components/PageHeader'; +import styles from './PageHeaderLayout.less'; + +export default ({ children, wrapperClassName, top, ...restProps }) => ( +
    + {top} + + {children ?
    {children}
    : null} +
    +); diff --git a/src/layouts/PageHeaderLayout.less b/src/layouts/PageHeaderLayout.less new file mode 100644 index 0000000..39a4496 --- /dev/null +++ b/src/layouts/PageHeaderLayout.less @@ -0,0 +1,11 @@ +@import '~antd/lib/style/themes/default.less'; + +.content { + margin: 24px 24px 0; +} + +@media screen and (max-width: @screen-sm) { + .content { + margin: 24px 0 0; + } +} diff --git a/src/layouts/UserLayout.js b/src/layouts/UserLayout.js new file mode 100644 index 0000000..8fb5353 --- /dev/null +++ b/src/layouts/UserLayout.js @@ -0,0 +1,78 @@ +import React, { Fragment } from 'react'; +import { Link, Redirect, Switch, Route } from 'dva/router'; +import DocumentTitle from 'react-document-title'; +import { Icon } from 'antd'; +import GlobalFooter from '../components/GlobalFooter'; +import styles from './UserLayout.less'; +import logo from '../assets/logo.svg'; +import { getRoutes } from '../utils/utils'; + +const links = [ + { + key: 'help', + title: '帮助', + href: '', + }, + { + key: 'privacy', + title: '隐私', + href: '', + }, + { + key: 'terms', + title: '条款', + href: '', + }, +]; + +const copyright = ( + + Copyright 2018 蚂蚁金服体验技术部出品 + +); + +class UserLayout extends React.PureComponent { + getPageTitle() { + const { routerData, location } = this.props; + const { pathname } = location; + let title = 'Ant Design Pro'; + if (routerData[pathname] && routerData[pathname].name) { + title = `${routerData[pathname].name} - Ant Design Pro`; + } + return title; + } + render() { + const { routerData, match } = this.props; + return ( + +
    +
    +
    +
    + + logo + Ant Design + +
    +
    Ant Design 是西湖区最具影响力的 Web 设计规范
    +
    + + {getRoutes(match.path, routerData).map(item => ( + + ))} + + +
    + +
    +
    + ); + } +} + +export default UserLayout; diff --git a/src/layouts/UserLayout.less b/src/layouts/UserLayout.less new file mode 100644 index 0000000..a6d8df0 --- /dev/null +++ b/src/layouts/UserLayout.less @@ -0,0 +1,60 @@ +@import '~antd/lib/style/themes/default.less'; + +.container { + display: flex; + flex-direction: column; + min-height: 100%; + background: #f0f2f5; +} + +.content { + padding: 32px 0; + flex: 1; +} + +@media (min-width: @screen-md-min) { + .container { + background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg'); + background-repeat: no-repeat; + background-position: center 110px; + background-size: 100%; + } + + .content { + padding: 112px 0 24px 0; + } +} + +.top { + text-align: center; +} + +.header { + height: 44px; + line-height: 44px; + a { + text-decoration: none; + } +} + +.logo { + height: 44px; + vertical-align: top; + margin-right: 16px; +} + +.title { + font-size: 33px; + color: @heading-color; + font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif; + font-weight: 600; + position: relative; + top: 2px; +} + +.desc { + font-size: @font-size-base; + color: @text-color-secondary; + margin-top: 12px; + margin-bottom: 40px; +} diff --git a/src/models/activities.js b/src/models/activities.js new file mode 100644 index 0000000..76a7d50 --- /dev/null +++ b/src/models/activities.js @@ -0,0 +1,28 @@ +import { queryActivities } from '../services/api'; + +export default { + namespace: 'activities', + + state: { + list: [], + }, + + effects: { + *fetchList(_, { call, put }) { + const response = yield call(queryActivities); + yield put({ + type: 'saveList', + payload: Array.isArray(response) ? response : [], + }); + }, + }, + + reducers: { + saveList(state, action) { + return { + ...state, + list: action.payload, + }; + }, + }, +}; diff --git a/src/models/chart.js b/src/models/chart.js new file mode 100644 index 0000000..17bd306 --- /dev/null +++ b/src/models/chart.js @@ -0,0 +1,61 @@ +import { fakeChartData } from '../services/api'; + +export default { + namespace: 'chart', + + state: { + visitData: [], + visitData2: [], + salesData: [], + searchData: [], + offlineData: [], + offlineChartData: [], + salesTypeData: [], + salesTypeDataOnline: [], + salesTypeDataOffline: [], + radarData: [], + loading: false, + }, + + effects: { + *fetch(_, { call, put }) { + const response = yield call(fakeChartData); + yield put({ + type: 'save', + payload: response, + }); + }, + *fetchSalesData(_, { call, put }) { + const response = yield call(fakeChartData); + yield put({ + type: 'save', + payload: { + salesData: response.salesData, + }, + }); + }, + }, + + reducers: { + save(state, { payload }) { + return { + ...state, + ...payload, + }; + }, + clear() { + return { + visitData: [], + visitData2: [], + salesData: [], + searchData: [], + offlineData: [], + offlineChartData: [], + salesTypeData: [], + salesTypeDataOnline: [], + salesTypeDataOffline: [], + radarData: [], + }; + }, + }, +}; diff --git a/src/models/error.js b/src/models/error.js new file mode 100644 index 0000000..01431f4 --- /dev/null +++ b/src/models/error.js @@ -0,0 +1,31 @@ +import { routerRedux } from 'dva/router'; +import { query } from '../services/error'; + +export default { + namespace: 'error', + + state: { + error: '', + isloading: false, + }, + + effects: { + *query({ payload }, { call, put }) { + yield call(query, payload.code); + // redirect on client when network broken + yield put(routerRedux.push(`/exception/${payload.code}`)); + yield put({ + type: 'trigger', + payload: payload.code, + }); + }, + }, + + reducers: { + trigger(state, action) { + return { + error: action.payload, + }; + }, + }, +}; diff --git a/src/models/form.js b/src/models/form.js new file mode 100644 index 0000000..64a1d49 --- /dev/null +++ b/src/models/form.js @@ -0,0 +1,47 @@ +import { routerRedux } from 'dva/router'; +import { message } from 'antd'; +import { fakeSubmitForm } from '../services/api'; + +export default { + namespace: 'form', + + state: { + step: { + payAccount: 'ant-design@alipay.com', + receiverAccount: 'test@example.com', + receiverName: 'Alex', + amount: '500', + }, + }, + + effects: { + *submitRegularForm({ payload }, { call }) { + yield call(fakeSubmitForm, payload); + message.success('提交成功'); + }, + *submitStepForm({ payload }, { call, put }) { + yield call(fakeSubmitForm, payload); + yield put({ + type: 'saveStepFormData', + payload, + }); + yield put(routerRedux.push('/form/step-form/result')); + }, + *submitAdvancedForm({ payload }, { call }) { + yield call(fakeSubmitForm, payload); + message.success('提交成功'); + }, + }, + + reducers: { + saveStepFormData(state, { payload }) { + return { + ...state, + step: { + ...state.step, + ...payload, + }, + }; + }, + }, +}; diff --git a/src/models/global.js b/src/models/global.js new file mode 100644 index 0000000..55841e1 --- /dev/null +++ b/src/models/global.js @@ -0,0 +1,67 @@ +import { queryNotices } from '../services/api'; + +export default { + namespace: 'global', + + state: { + collapsed: false, + notices: [], + }, + + effects: { + *fetchNotices(_, { call, put }) { + const data = yield call(queryNotices); + yield put({ + type: 'saveNotices', + payload: data, + }); + yield put({ + type: 'user/changeNotifyCount', + payload: data.length, + }); + }, + *clearNotices({ payload }, { put, select }) { + yield put({ + type: 'saveClearedNotices', + payload, + }); + const count = yield select(state => state.global.notices.length); + yield put({ + type: 'user/changeNotifyCount', + payload: count, + }); + }, + }, + + reducers: { + changeLayoutCollapsed(state, { payload }) { + return { + ...state, + collapsed: payload, + }; + }, + saveNotices(state, { payload }) { + return { + ...state, + notices: payload, + }; + }, + saveClearedNotices(state, { payload }) { + return { + ...state, + notices: state.notices.filter(item => item.type !== payload), + }; + }, + }, + + subscriptions: { + setup({ history }) { + // Subscribe history(url) change, trigger `load` action if pathname is `/` + return history.listen(({ pathname, search }) => { + if (typeof window.ga !== 'undefined') { + window.ga('send', 'pageview', pathname + search); + } + }); + }, + }, +}; diff --git a/src/models/index.js b/src/models/index.js new file mode 100644 index 0000000..902cb5c --- /dev/null +++ b/src/models/index.js @@ -0,0 +1,7 @@ +// Use require.context to require reducers automatically +// Ref: https://webpack.js.org/guides/dependency-management/#require-context +const context = require.context('./', false, /\.js$/); +export default context + .keys() + .filter(item => item !== './index.js') + .map(key => context(key)); diff --git a/src/models/list.js b/src/models/list.js new file mode 100644 index 0000000..147eeca --- /dev/null +++ b/src/models/list.js @@ -0,0 +1,41 @@ +import { queryFakeList } from '../services/api'; + +export default { + namespace: 'list', + + state: { + list: [], + }, + + effects: { + *fetch({ payload }, { call, put }) { + const response = yield call(queryFakeList, payload); + yield put({ + type: 'queryList', + payload: Array.isArray(response) ? response : [], + }); + }, + *appendFetch({ payload }, { call, put }) { + const response = yield call(queryFakeList, payload); + yield put({ + type: 'appendList', + payload: Array.isArray(response) ? response : [], + }); + }, + }, + + reducers: { + queryList(state, action) { + return { + ...state, + list: action.payload, + }; + }, + appendList(state, action) { + return { + ...state, + list: state.list.concat(action.payload), + }; + }, + }, +}; diff --git a/src/models/login.js b/src/models/login.js new file mode 100644 index 0000000..ea9fd30 --- /dev/null +++ b/src/models/login.js @@ -0,0 +1,58 @@ +import { routerRedux } from 'dva/router'; +import { fakeAccountLogin } from '../services/api'; +import { setAuthority } from '../utils/authority'; +import { reloadAuthorized } from '../utils/Authorized'; + +export default { + namespace: 'login', + + state: { + status: undefined, + }, + + effects: { + *login({ payload }, { call, put }) { + const response = yield call(fakeAccountLogin, payload); + yield put({ + type: 'changeLoginStatus', + payload: response, + }); + // Login successfully + if (response.status === 'ok') { + reloadAuthorized(); + yield put(routerRedux.push('/')); + } + }, + *logout(_, { put, select }) { + try { + // get location pathname + const urlParams = new URL(window.location.href); + const pathname = yield select(state => state.routing.location.pathname); + // add the parameters in the url + urlParams.searchParams.set('redirect', pathname); + window.history.replaceState(null, 'login', urlParams.href); + } finally { + yield put({ + type: 'changeLoginStatus', + payload: { + status: false, + currentAuthority: 'guest', + }, + }); + reloadAuthorized(); + yield put(routerRedux.push('/user/login')); + } + }, + }, + + reducers: { + changeLoginStatus(state, { payload }) { + setAuthority(payload.currentAuthority); + return { + ...state, + status: payload.status, + type: payload.type, + }; + }, + }, +}; diff --git a/src/models/monitor.js b/src/models/monitor.js new file mode 100644 index 0000000..a5f96f8 --- /dev/null +++ b/src/models/monitor.js @@ -0,0 +1,28 @@ +import { queryTags } from '../services/api'; + +export default { + namespace: 'monitor', + + state: { + tags: [], + }, + + effects: { + *fetchTags(_, { call, put }) { + const response = yield call(queryTags); + yield put({ + type: 'saveTags', + payload: response.list, + }); + }, + }, + + reducers: { + saveTags(state, action) { + return { + ...state, + tags: action.payload, + }; + }, + }, +}; diff --git a/src/models/profile.js b/src/models/profile.js new file mode 100644 index 0000000..7c9d019 --- /dev/null +++ b/src/models/profile.js @@ -0,0 +1,38 @@ +import { queryBasicProfile, queryAdvancedProfile } from '../services/api'; + +export default { + namespace: 'profile', + + state: { + basicGoods: [], + advancedOperation1: [], + advancedOperation2: [], + advancedOperation3: [], + }, + + effects: { + *fetchBasic(_, { call, put }) { + const response = yield call(queryBasicProfile); + yield put({ + type: 'show', + payload: response, + }); + }, + *fetchAdvanced(_, { call, put }) { + const response = yield call(queryAdvancedProfile); + yield put({ + type: 'show', + payload: response, + }); + }, + }, + + reducers: { + show(state, { payload }) { + return { + ...state, + ...payload, + }; + }, + }, +}; diff --git a/src/models/project.js b/src/models/project.js new file mode 100644 index 0000000..f68bef5 --- /dev/null +++ b/src/models/project.js @@ -0,0 +1,28 @@ +import { queryProjectNotice } from '../services/api'; + +export default { + namespace: 'project', + + state: { + notice: [], + }, + + effects: { + *fetchNotice(_, { call, put }) { + const response = yield call(queryProjectNotice); + yield put({ + type: 'saveNotice', + payload: Array.isArray(response) ? response : [], + }); + }, + }, + + reducers: { + saveNotice(state, action) { + return { + ...state, + notice: action.payload, + }; + }, + }, +}; diff --git a/src/models/register.js b/src/models/register.js new file mode 100644 index 0000000..54c4963 --- /dev/null +++ b/src/models/register.js @@ -0,0 +1,32 @@ +import { fakeRegister } from '../services/api'; +import { setAuthority } from '../utils/authority'; +import { reloadAuthorized } from '../utils/Authorized'; + +export default { + namespace: 'register', + + state: { + status: undefined, + }, + + effects: { + *submit(_, { call, put }) { + const response = yield call(fakeRegister); + yield put({ + type: 'registerHandle', + payload: response, + }); + }, + }, + + reducers: { + registerHandle(state, { payload }) { + setAuthority('user'); + reloadAuthorized(); + return { + ...state, + status: payload.status, + }; + }, + }, +}; diff --git a/src/models/rule.js b/src/models/rule.js new file mode 100644 index 0000000..9ae968f --- /dev/null +++ b/src/models/rule.js @@ -0,0 +1,47 @@ +import { queryRule, removeRule, addRule } from '../services/api'; + +export default { + namespace: 'rule', + + state: { + data: { + list: [], + pagination: {}, + }, + }, + + effects: { + *fetch({ payload }, { call, put }) { + const response = yield call(queryRule, payload); + yield put({ + type: 'save', + payload: response, + }); + }, + *add({ payload, callback }, { call, put }) { + const response = yield call(addRule, payload); + yield put({ + type: 'save', + payload: response, + }); + if (callback) callback(); + }, + *remove({ payload, callback }, { call, put }) { + const response = yield call(removeRule, payload); + yield put({ + type: 'save', + payload: response, + }); + if (callback) callback(); + }, + }, + + reducers: { + save(state, action) { + return { + ...state, + data: action.payload, + }; + }, + }, +}; diff --git a/src/models/user.js b/src/models/user.js new file mode 100644 index 0000000..b45afc3 --- /dev/null +++ b/src/models/user.js @@ -0,0 +1,51 @@ +import { query as queryUsers, queryCurrent } from '../services/user'; + +export default { + namespace: 'user', + + state: { + list: [], + currentUser: {}, + }, + + effects: { + *fetch(_, { call, put }) { + const response = yield call(queryUsers); + yield put({ + type: 'save', + payload: response, + }); + }, + *fetchCurrent(_, { call, put }) { + const response = yield call(queryCurrent); + yield put({ + type: 'saveCurrentUser', + payload: response, + }); + }, + }, + + reducers: { + save(state, action) { + return { + ...state, + list: action.payload, + }; + }, + saveCurrentUser(state, action) { + return { + ...state, + currentUser: action.payload, + }; + }, + changeNotifyCount(state, action) { + return { + ...state, + currentUser: { + ...state.currentUser, + notifyCount: action.payload, + }, + }; + }, + }, +}; diff --git a/src/rollbar.js b/src/rollbar.js new file mode 100644 index 0000000..f333309 --- /dev/null +++ b/src/rollbar.js @@ -0,0 +1,13 @@ +import Rollbar from 'rollbar'; + +// Track error by rollbar.com +if (location.host === 'preview.pro.ant.design') { + Rollbar.init({ + accessToken: '033ca6d7c0eb4cc1831cf470c2649971', + captureUncaught: true, + captureUnhandledRejections: true, + payload: { + environment: 'production', + }, + }); +} diff --git a/src/router.js b/src/router.js new file mode 100644 index 0000000..3c3a978 --- /dev/null +++ b/src/router.js @@ -0,0 +1,37 @@ +import React from 'react'; +import { routerRedux, Route, Switch } from 'dva/router'; +import { LocaleProvider, Spin } from 'antd'; +import zhCN from 'antd/lib/locale-provider/zh_CN'; +import dynamic from 'dva/dynamic'; +import { getRouterData } from './common/router'; +import Authorized from './utils/Authorized'; +import styles from './index.less'; + +const { ConnectedRouter } = routerRedux; +const { AuthorizedRoute } = Authorized; +dynamic.setDefaultLoadingComponent(() => { + return ; +}); + +function RouterConfig({ history, app }) { + const routerData = getRouterData(app); + const UserLayout = routerData['/user'].component; + const BasicLayout = routerData['/'].component; + return ( + + + + + } + authority={['admin', 'user']} + redirectPath="/user/login" + /> + + + + ); +} + +export default RouterConfig; diff --git a/src/routes/Dashboard/Analysis.js b/src/routes/Dashboard/Analysis.js new file mode 100644 index 0000000..0d2d09b --- /dev/null +++ b/src/routes/Dashboard/Analysis.js @@ -0,0 +1,494 @@ +import React, { Component, Fragment } from 'react'; +import { connect } from 'dva'; +import { + Row, + Col, + Icon, + Card, + Tabs, + Table, + Radio, + DatePicker, + Tooltip, + Menu, + Dropdown, +} from 'antd'; +import numeral from 'numeral'; +import { + ChartCard, + yuan, + MiniArea, + MiniBar, + MiniProgress, + Field, + Bar, + Pie, + TimelineChart, +} from 'components/Charts'; +import Trend from 'components/Trend'; +import NumberInfo from 'components/NumberInfo'; +import { getTimeDistance } from '../../utils/utils'; + +import styles from './Analysis.less'; + +const { TabPane } = Tabs; +const { RangePicker } = DatePicker; + +const rankingListData = []; +for (let i = 0; i < 7; i += 1) { + rankingListData.push({ + title: `工专路 ${i} 号店`, + total: 323234, + }); +} + +@connect(({ chart, loading }) => ({ + chart, + loading: loading.effects['chart/fetch'], +})) +export default class Analysis extends Component { + state = { + salesType: 'all', + currentTabKey: '', + rangePickerValue: getTimeDistance('year'), + }; + + componentDidMount() { + this.props.dispatch({ + type: 'chart/fetch', + }); + } + + componentWillUnmount() { + const { dispatch } = this.props; + dispatch({ + type: 'chart/clear', + }); + } + + handleChangeSalesType = e => { + this.setState({ + salesType: e.target.value, + }); + }; + + handleTabChange = key => { + this.setState({ + currentTabKey: key, + }); + }; + + handleRangePickerChange = rangePickerValue => { + this.setState({ + rangePickerValue, + }); + + this.props.dispatch({ + type: 'chart/fetchSalesData', + }); + }; + + selectDate = type => { + this.setState({ + rangePickerValue: getTimeDistance(type), + }); + + this.props.dispatch({ + type: 'chart/fetchSalesData', + }); + }; + + isActive(type) { + const { rangePickerValue } = this.state; + const value = getTimeDistance(type); + if (!rangePickerValue[0] || !rangePickerValue[1]) { + return; + } + if ( + rangePickerValue[0].isSame(value[0], 'day') && + rangePickerValue[1].isSame(value[1], 'day') + ) { + return styles.currentDate; + } + } + + render() { + const { rangePickerValue, salesType, currentTabKey } = this.state; + const { chart, loading } = this.props; + const { + visitData, + visitData2, + salesData, + searchData, + offlineData, + offlineChartData, + salesTypeData, + salesTypeDataOnline, + salesTypeDataOffline, + } = chart; + + const salesPieData = + salesType === 'all' + ? salesTypeData + : salesType === 'online' ? salesTypeDataOnline : salesTypeDataOffline; + + const menu = ( + + 操作一 + 操作二 + + ); + + const iconGroup = ( + + + + + + ); + + const salesExtra = ( + + ); + + const columns = [ + { + title: '排名', + dataIndex: 'index', + key: 'index', + }, + { + title: '搜索关键词', + dataIndex: 'keyword', + key: 'keyword', + render: text => {text}, + }, + { + title: '用户数', + dataIndex: 'count', + key: 'count', + sorter: (a, b) => a.count - b.count, + className: styles.alignRight, + }, + { + title: '周涨幅', + dataIndex: 'range', + key: 'range', + sorter: (a, b) => a.range - b.range, + render: (text, record) => ( + + {text}% + + ), + align: 'right', + }, + ]; + + const activeKey = currentTabKey || (offlineData[0] && offlineData[0].name); + + const CustomTab = ({ data, currentTabKey: currentKey }) => ( + +
    + + + + + + + ); + + const topColResponsiveProps = { + xs: 24, + sm: 12, + md: 12, + lg: 12, + xl: 6, + style: { marginBottom: 24 }, + }; + + return ( + + + + + + + } + total={() => } + footer={} + contentHeight={46} + > + + 周同比12% + + + 日环比11% + + + + + + + + } + total={numeral(8846).format('0,0')} + footer={} + contentHeight={46} + > + + + + + + + + } + total={numeral(6560).format('0,0')} + footer={} + contentHeight={46} + > + + + + + + + + } + total="78%" + footer={ +
    + + 周同比12% + + + 日环比11% + +
    + } + contentHeight={46} + > + +
    + + + + +
    + + + +
    +
    + +
    + + +
    +

    门店销售额排名

    +
      + {rankingListData.map((item, i) => ( +
    • + {i + 1} + {item.title} + {numeral(item.total).format('0,0')} +
    • + ))} +
    +
    + + + + + + +
    + +
    + + +
    +

    门店访问量排名

    +
      + {rankingListData.map((item, i) => ( +
    • + {i + 1} + {item.title} + {numeral(item.total).format('0,0')} +
    • + ))} +
    +
    + + + + + + + + + + + + + + 搜索用户数 + + + + + } + gap={8} + total={numeral(12321).format('0,0')} + status="up" + subTotal={17.1} + /> + + + + + + + +
    record.index} + size="small" + columns={columns} + dataSource={searchData} + pagination={{ + style: { marginBottom: 0 }, + pageSize: 5, + }} + /> + + + + + {iconGroup} +
    + + 全部渠道 + 线上 + 门店 + +
    + + } + style={{ marginTop: 24, minHeight: 509 }} + > +

    销售额

    + ( + now.y + pre, 0)), + }} + /> + )} + data={salesPieData} + valueFormat={val => } + height={248} + lineWidth={4} + /> +
    + + + + + + {offlineData.map(shop => ( + } key={shop.name}> +
    + +
    +
    + ))} +
    +
    + + ); + } +} diff --git a/src/routes/Dashboard/Analysis.less b/src/routes/Dashboard/Analysis.less new file mode 100644 index 0000000..cd1d43b --- /dev/null +++ b/src/routes/Dashboard/Analysis.less @@ -0,0 +1,170 @@ +@import '~antd/lib/style/themes/default.less'; +@import '../../utils/utils.less'; + +.iconGroup { + i { + transition: color 0.32s; + color: @text-color-secondary; + cursor: pointer; + margin-left: 16px; + &:hover { + color: @text-color; + } + } +} + +.rankingList { + margin: 25px 0 0; + padding: 0; + list-style: none; + li { + .clearfix(); + margin-top: 16px; + span { + color: @text-color; + font-size: 14px; + line-height: 22px; + } + span:first-child { + background-color: @background-color-base; + border-radius: 20px; + display: inline-block; + font-size: 12px; + font-weight: 600; + margin-right: 24px; + height: 20px; + line-height: 20px; + width: 20px; + text-align: center; + } + span.active { + //background-color: @primary-color; + background-color: #314659; + color: #fff; + } + span:last-child { + float: right; + } + } +} + +.salesExtra { + display: inline-block; + margin-right: 24px; + a { + color: @text-color; + margin-left: 24px; + &:hover { + color: @primary-color; + } + &.currentDate { + color: @primary-color; + } + } +} + +.salesCard { + .salesBar { + padding: 0 0 32px 32px; + } + .salesRank { + padding: 0 32px 32px 72px; + } + :global { + .ant-tabs-bar { + padding-left: 16px; + .ant-tabs-nav .ant-tabs-tab { + padding-top: 16px; + padding-bottom: 14px; + line-height: 24px; + } + } + .ant-tabs-extra-content { + padding-right: 24px; + line-height: 55px; + } + .ant-card-head { + position: relative; + } + } +} + +.salesCardExtra { + height: 68px; +} + +.salesTypeRadio { + position: absolute; + left: 24px; + bottom: 15px; +} + +.offlineCard { + :global { + .ant-tabs-ink-bar { + bottom: auto; + } + .ant-tabs-bar { + border-bottom: none; + } + .ant-tabs-nav-container-scrolling { + padding-left: 40px; + padding-right: 40px; + } + .ant-tabs-tab-prev-icon:before { + position: relative; + left: 6px; + } + .ant-tabs-tab-next-icon:before { + position: relative; + right: 6px; + } + } + + :global(.ant-tabs-tab-active) h4 { + color: @primary-color; + } +} + +.trendText { + margin-left: 8px; + color: @heading-color; +} + +@media screen and (max-width: @screen-lg) { + .salesExtra { + display: none; + } + + .rankingList { + li { + span:first-child { + margin-right: 8px; + } + } + } +} + +@media screen and (max-width: @screen-md) { + .rankingTitle { + margin-top: 16px; + } + + .salesCard .salesBar { + padding: 16px; + } +} + +@media screen and (max-width: @screen-sm) { + .salesExtraWrap { + display: none; + } + + .salesCard { + :global { + .ant-tabs-content { + padding-top: 30px; + } + } + } +} diff --git a/src/routes/Dashboard/Monitor.js b/src/routes/Dashboard/Monitor.js new file mode 100644 index 0000000..96fbfed --- /dev/null +++ b/src/routes/Dashboard/Monitor.js @@ -0,0 +1,150 @@ +import React, { PureComponent, Fragment } from 'react'; +import { connect } from 'dva'; +import { Row, Col, Card, Tooltip } from 'antd'; +import numeral from 'numeral'; +import { Pie, WaterWave, Gauge, TagCloud } from 'components/Charts'; +import NumberInfo from 'components/NumberInfo'; +import CountDown from 'components/CountDown'; +import ActiveChart from 'components/ActiveChart'; +import Authorized from '../../utils/Authorized'; +import styles from './Monitor.less'; + +const { Secured } = Authorized; + +const targetTime = new Date().getTime() + 3900000; + +// use permission as a parameter +const havePermissionAsync = new Promise(resolve => { + // Call resolve on behalf of passed + setTimeout(() => resolve(), 1000); +}); +@Secured(havePermissionAsync) +@connect(({ monitor, loading }) => ({ + monitor, + loading: loading.models.monitor, +})) +export default class Monitor extends PureComponent { + componentDidMount() { + this.props.dispatch({ + type: 'monitor/fetchTags', + }); + } + + render() { + const { monitor, loading } = this.props; + const { tags } = monitor; + + return ( + + + + + + + + + + + + + } /> + + + + + +
    + + map + +
    + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } +} diff --git a/src/routes/Dashboard/Monitor.less b/src/routes/Dashboard/Monitor.less new file mode 100644 index 0000000..3f0701f --- /dev/null +++ b/src/routes/Dashboard/Monitor.less @@ -0,0 +1,23 @@ +@import '~antd/lib/style/themes/default.less'; +@import '../../utils/utils.less'; + +.mapChart { + padding-top: 24px; + height: 457px; + text-align: center; + img { + display: inline-block; + max-width: 100%; + max-height: 437px; + } +} + +.pieCard :global(.pie-stat) { + font-size: 24px !important; +} + +@media screen and (max-width: @screen-lg) { + .mapChart { + height: auto; + } +} diff --git a/src/routes/Dashboard/Workplace.js b/src/routes/Dashboard/Workplace.js new file mode 100644 index 0000000..9c8bdc7 --- /dev/null +++ b/src/routes/Dashboard/Workplace.js @@ -0,0 +1,265 @@ +import React, { PureComponent } from 'react'; +import moment from 'moment'; +import { connect } from 'dva'; +import { Link } from 'dva/router'; +import { Row, Col, Card, List, Avatar } from 'antd'; + +import { Radar } from 'components/Charts'; +import EditableLinkGroup from 'components/EditableLinkGroup'; +import PageHeaderLayout from '../../layouts/PageHeaderLayout'; + +import styles from './Workplace.less'; + +const links = [ + { + title: '操作一', + href: '', + }, + { + title: '操作二', + href: '', + }, + { + title: '操作三', + href: '', + }, + { + title: '操作四', + href: '', + }, + { + title: '操作五', + href: '', + }, + { + title: '操作六', + href: '', + }, +]; + +const members = [ + { + id: 'members-1', + title: '科学搬砖组', + logo: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png', + link: '', + }, + { + id: 'members-2', + title: '程序员日常', + logo: 'https://gw.alipayobjects.com/zos/rmsportal/cnrhVkzwxjPwAaCfPbdc.png', + link: '', + }, + { + id: 'members-3', + title: '设计天团', + logo: 'https://gw.alipayobjects.com/zos/rmsportal/gaOngJwsRYRaVAuXXcmB.png', + link: '', + }, + { + id: 'members-4', + title: '中二少女团', + logo: 'https://gw.alipayobjects.com/zos/rmsportal/ubnKSIfAJTxIgXOKlciN.png', + link: '', + }, + { + id: 'members-5', + title: '骗你学计算机', + logo: 'https://gw.alipayobjects.com/zos/rmsportal/WhxKECPNujWoWEFNdnJE.png', + link: '', + }, +]; + +@connect(({ project, activities, chart, loading }) => ({ + project, + activities, + chart, + projectLoading: loading.effects['project/fetchNotice'], + activitiesLoading: loading.effects['activities/fetchList'], +})) +export default class Workplace extends PureComponent { + componentDidMount() { + const { dispatch } = this.props; + dispatch({ + type: 'project/fetchNotice', + }); + dispatch({ + type: 'activities/fetchList', + }); + dispatch({ + type: 'chart/fetch', + }); + } + + componentWillUnmount() { + const { dispatch } = this.props; + dispatch({ + type: 'chart/clear', + }); + } + + renderActivities() { + const { activities: { list } } = this.props; + return list.map(item => { + const events = item.template.split(/@\{([^{}]*)\}/gi).map(key => { + if (item[key]) { + return ( + + {item[key].name} + + ); + } + return key; + }); + return ( + + } + title={ + + {item.user.name} +   + {events} + + } + description={ + + {moment(item.updatedAt).fromNow()} + + } + /> + + ); + }); + } + + render() { + const { + project: { notice }, + projectLoading, + activitiesLoading, + chart: { radarData }, + } = this.props; + + const pageHeaderContent = ( +
    +
    + +
    +
    +
    早安,曲丽丽,祝你开心每一天!
    +
    交互专家 | 蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED
    +
    +
    + ); + + const extraContent = ( +
    +
    +

    项目数

    +

    56

    +
    +
    +

    团队内排名

    +

    + 8 / 24 +

    +
    +
    +

    项目访问

    +

    2,223

    +
    +
    + ); + + return ( + + + + 全部项目} + loading={projectLoading} + bodyStyle={{ padding: 0 }} + > + {notice.map(item => ( + + + + + {item.title} + + } + description={item.description} + /> +
    + {item.member || ''} + {item.updatedAt && ( + + {moment(item.updatedAt).fromNow()} + + )} +
    +
    +
    + ))} +
    + + +
    {this.renderActivities()}
    +
    +
    + + + + {}} links={links} linkElement={Link} /> + + +
    + +
    +
    + +
    + + {members.map(item => ( +
    + + + {item.title} + + + ))} + + + + + + + ); + } +} diff --git a/src/routes/Dashboard/Workplace.less b/src/routes/Dashboard/Workplace.less new file mode 100644 index 0000000..a8005fe --- /dev/null +++ b/src/routes/Dashboard/Workplace.less @@ -0,0 +1,233 @@ +@import '~antd/lib/style/themes/default.less'; +@import '../../utils/utils.less'; + +.activitiesList { + padding: 0 24px 8px 24px; + .username { + color: @text-color; + } + .event { + font-weight: normal; + } +} + +.pageHeaderContent { + display: flex; + .avatar { + flex: 0 1 72px; + margin-bottom: 8px; + & > span { + border-radius: 72px; + display: block; + width: 72px; + height: 72px; + } + } + .content { + position: relative; + top: 4px; + margin-left: 24px; + flex: 1 1 auto; + color: @text-color-secondary; + line-height: 22px; + .contentTitle { + font-size: 20px; + line-height: 28px; + font-weight: 500; + color: @heading-color; + margin-bottom: 12px; + } + } +} + +.extraContent { + .clearfix(); + float: right; + white-space: nowrap; + .statItem { + padding: 0 32px; + position: relative; + display: inline-block; + > p:first-child { + color: @text-color-secondary; + font-size: @font-size-base; + line-height: 22px; + margin-bottom: 4px; + } + > p { + color: @heading-color; + font-size: 30px; + line-height: 38px; + margin: 0; + > span { + color: @text-color-secondary; + font-size: 20px; + } + } + &:after { + background-color: @border-color-split; + position: absolute; + top: 8px; + right: 0; + width: 1px; + height: 40px; + content: ''; + } + &:last-child { + padding-right: 0; + &:after { + display: none; + } + } + } +} + +.members { + a { + display: block; + margin: 12px 0; + line-height: 24px; + height: 24px; + .textOverflow(); + .member { + font-size: @font-size-base; + color: @text-color; + line-height: 24px; + max-width: 100px; + vertical-align: top; + margin-left: 12px; + transition: all 0.3s; + display: inline-block; + .textOverflow(); + } + &:hover { + span { + color: @primary-color; + } + } + } +} + +.projectList { + :global { + .ant-card-meta-description { + color: @text-color-secondary; + height: 44px; + line-height: 22px; + overflow: hidden; + } + } + .cardTitle { + font-size: 0; + a { + color: @heading-color; + margin-left: 12px; + line-height: 24px; + height: 24px; + display: inline-block; + vertical-align: top; + font-size: @font-size-base; + &:hover { + color: @primary-color; + } + } + } + .projectGrid { + width: 33.33%; + } + .projectItemContent { + display: flex; + margin-top: 8px; + overflow: hidden; + font-size: 12px; + height: 20px; + line-height: 20px; + .textOverflow(); + a { + color: @text-color-secondary; + display: inline-block; + flex: 1 1 0; + .textOverflow(); + &:hover { + color: @primary-color; + } + } + .datetime { + color: @disabled-color; + flex: 0 0 auto; + float: right; + } + } +} + +.datetime { + color: @disabled-color; +} + +@media screen and (max-width: @screen-xl) and (min-width: @screen-lg) { + .activeCard { + margin-bottom: 24px; + } + .members { + margin-bottom: 0; + } + .extraContent { + margin-left: -44px; + .statItem { + padding: 0 16px; + } + } +} + +@media screen and (max-width: @screen-lg) { + .activeCard { + margin-bottom: 24px; + } + .members { + margin-bottom: 0; + } + .extraContent { + float: none; + margin-right: 0; + .statItem { + padding: 0 16px; + text-align: left; + &:after { + display: none; + } + } + } +} + +@media screen and (max-width: @screen-md) { + .extraContent { + margin-left: -16px; + } + .projectList { + .projectGrid { + width: 50%; + } + } +} + +@media screen and (max-width: @screen-sm) { + .pageHeaderContent { + display: block; + .content { + margin-left: 0; + } + } + .extraContent { + .statItem { + float: none; + } + } +} + +@media screen and (max-width: @screen-xs) { + .projectList { + .projectGrid { + width: 100%; + } + } +} diff --git a/src/routes/Exception/403.js b/src/routes/Exception/403.js new file mode 100644 index 0000000..c6d86fe --- /dev/null +++ b/src/routes/Exception/403.js @@ -0,0 +1,7 @@ +import React from 'react'; +import { Link } from 'dva/router'; +import Exception from 'components/Exception'; + +export default () => ( + +); diff --git a/src/routes/Exception/404.js b/src/routes/Exception/404.js new file mode 100644 index 0000000..0a3d876 --- /dev/null +++ b/src/routes/Exception/404.js @@ -0,0 +1,7 @@ +import React from 'react'; +import { Link } from 'dva/router'; +import Exception from 'components/Exception'; + +export default () => ( + +); diff --git a/src/routes/Exception/500.js b/src/routes/Exception/500.js new file mode 100644 index 0000000..40f659c --- /dev/null +++ b/src/routes/Exception/500.js @@ -0,0 +1,7 @@ +import React from 'react'; +import { Link } from 'dva/router'; +import Exception from 'components/Exception'; + +export default () => ( + +); diff --git a/src/routes/Exception/style.less b/src/routes/Exception/style.less new file mode 100644 index 0000000..91ec7dc --- /dev/null +++ b/src/routes/Exception/style.less @@ -0,0 +1,7 @@ +.trigger { + background: 'red'; + :global(.ant-btn) { + margin-right: 8px; + margin-bottom: 12px; + } +} diff --git a/src/routes/Exception/triggerException.js b/src/routes/Exception/triggerException.js new file mode 100644 index 0000000..2dccfb7 --- /dev/null +++ b/src/routes/Exception/triggerException.js @@ -0,0 +1,44 @@ +import React, { PureComponent } from 'react'; +import { Button, Spin, Card } from 'antd'; +import { connect } from 'dva'; +import styles from './style.less'; + +@connect(state => ({ + isloading: state.error.isloading, +})) +export default class TriggerException extends PureComponent { + state = { + isloading: false, + }; + triggerError = code => { + this.setState({ + isloading: true, + }); + this.props.dispatch({ + type: 'error/query', + payload: { + code, + }, + }); + }; + render() { + return ( + + + + + + + + + ); + } +} diff --git a/src/routes/Forms/AdvancedForm.js b/src/routes/Forms/AdvancedForm.js new file mode 100644 index 0000000..16b7e3b --- /dev/null +++ b/src/routes/Forms/AdvancedForm.js @@ -0,0 +1,300 @@ +import React, { PureComponent } from 'react'; +import { + Card, + Button, + Form, + Icon, + Col, + Row, + DatePicker, + TimePicker, + Input, + Select, + Popover, +} from 'antd'; +import { connect } from 'dva'; +import FooterToolbar from 'components/FooterToolbar'; +import PageHeaderLayout from '../../layouts/PageHeaderLayout'; +import TableForm from './TableForm'; +import styles from './style.less'; + +const { Option } = Select; +const { RangePicker } = DatePicker; + +const fieldLabels = { + name: '仓库名', + url: '仓库域名', + owner: '仓库管理员', + approver: '审批人', + dateRange: '生效日期', + type: '仓库类型', + name2: '任务名', + url2: '任务描述', + owner2: '执行人', + approver2: '责任人', + dateRange2: '生效日期', + type2: '任务类型', +}; + +const tableData = [ + { + key: '1', + workId: '00001', + name: 'John Brown', + department: 'New York No. 1 Lake Park', + }, + { + key: '2', + workId: '00002', + name: 'Jim Green', + department: 'London No. 1 Lake Park', + }, + { + key: '3', + workId: '00003', + name: 'Joe Black', + department: 'Sidney No. 1 Lake Park', + }, +]; + +class AdvancedForm extends PureComponent { + state = { + width: '100%', + }; + componentDidMount() { + window.addEventListener('resize', this.resizeFooterToolbar); + } + componentWillUnmount() { + window.removeEventListener('resize', this.resizeFooterToolbar); + } + resizeFooterToolbar = () => { + const sider = document.querySelectorAll('.ant-layout-sider')[0]; + const width = `calc(100% - ${sider.style.width})`; + if (this.state.width !== width) { + this.setState({ width }); + } + }; + render() { + const { form, dispatch, submitting } = this.props; + const { getFieldDecorator, validateFieldsAndScroll, getFieldsError } = form; + const validate = () => { + validateFieldsAndScroll((error, values) => { + if (!error) { + // submit the values + dispatch({ + type: 'form/submitAdvancedForm', + payload: values, + }); + } + }); + }; + const errors = getFieldsError(); + const getErrorInfo = () => { + const errorCount = Object.keys(errors).filter(key => errors[key]).length; + if (!errors || errorCount === 0) { + return null; + } + const scrollToField = fieldKey => { + const labelNode = document.querySelector(`label[for="${fieldKey}"]`); + if (labelNode) { + labelNode.scrollIntoView(true); + } + }; + const errorList = Object.keys(errors).map(key => { + if (!errors[key]) { + return null; + } + return ( +
  • scrollToField(key)}> + +
    {errors[key][0]}
    +
    {fieldLabels[key]}
    +
  • + ); + }); + return ( + + trigger.parentNode} + > + + + {errorCount} + + ); + }; + return ( + + +
    + +
    + + {getFieldDecorator('name', { + rules: [{ required: true, message: '请输入仓库名称' }], + })()} + + + + + {getFieldDecorator('url', { + rules: [{ required: true, message: '请选择' }], + })( + + )} + + + + + {getFieldDecorator('owner', { + rules: [{ required: true, message: '请选择管理员' }], + })( + + )} + + + + + + + {getFieldDecorator('approver', { + rules: [{ required: true, message: '请选择审批员' }], + })( + + )} + + + + + {getFieldDecorator('dateRange', { + rules: [{ required: true, message: '请选择生效日期' }], + })( + + )} + + + + + {getFieldDecorator('type', { + rules: [{ required: true, message: '请选择仓库类型' }], + })( + + )} + + + + + + +
    + +
    + + {getFieldDecorator('name2', { + rules: [{ required: true, message: '请输入' }], + })()} + + + + + {getFieldDecorator('url2', { + rules: [{ required: true, message: '请选择' }], + })()} + + + + + {getFieldDecorator('owner2', { + rules: [{ required: true, message: '请选择管理员' }], + })( + + )} + + + + + + + {getFieldDecorator('approver2', { + rules: [{ required: true, message: '请选择审批员' }], + })( + + )} + + + + + {getFieldDecorator('dateRange2', { + rules: [{ required: true, message: '请输入' }], + })( + trigger.parentNode} + /> + )} + + + + + {getFieldDecorator('type2', { + rules: [{ required: true, message: '请选择仓库类型' }], + })( + + )} + + + + + + + {getFieldDecorator('members', { + initialValue: tableData, + })()} + + + {getErrorInfo()} + + + + ); + } +} + +export default connect(({ global, loading }) => ({ + collapsed: global.collapsed, + submitting: loading.effects['form/submitAdvancedForm'], +}))(Form.create()(AdvancedForm)); diff --git a/src/routes/Forms/BasicForm.js b/src/routes/Forms/BasicForm.js new file mode 100644 index 0000000..967ced1 --- /dev/null +++ b/src/routes/Forms/BasicForm.js @@ -0,0 +1,196 @@ +import React, { PureComponent } from 'react'; +import { connect } from 'dva'; +import { + Form, + Input, + DatePicker, + Select, + Button, + Card, + InputNumber, + Radio, + Icon, + Tooltip, +} from 'antd'; +import PageHeaderLayout from '../../layouts/PageHeaderLayout'; +import styles from './style.less'; + +const FormItem = Form.Item; +const { Option } = Select; +const { RangePicker } = DatePicker; +const { TextArea } = Input; + +@connect(({ loading }) => ({ + submitting: loading.effects['form/submitRegularForm'], +})) +@Form.create() +export default class BasicForms extends PureComponent { + handleSubmit = e => { + e.preventDefault(); + this.props.form.validateFieldsAndScroll((err, values) => { + if (!err) { + this.props.dispatch({ + type: 'form/submitRegularForm', + payload: values, + }); + } + }); + }; + render() { + const { submitting } = this.props; + const { getFieldDecorator, getFieldValue } = this.props.form; + + const formItemLayout = { + labelCol: { + xs: { span: 24 }, + sm: { span: 7 }, + }, + wrapperCol: { + xs: { span: 24 }, + sm: { span: 12 }, + md: { span: 10 }, + }, + }; + + const submitFormLayout = { + wrapperCol: { + xs: { span: 24, offset: 0 }, + sm: { span: 10, offset: 7 }, + }, + }; + + return ( + + +
    + + {getFieldDecorator('title', { + rules: [ + { + required: true, + message: '请输入标题', + }, + ], + })()} + + + {getFieldDecorator('date', { + rules: [ + { + required: true, + message: '请选择起止日期', + }, + ], + })()} + + + {getFieldDecorator('goal', { + rules: [ + { + required: true, + message: '请输入目标描述', + }, + ], + })( +