From d7ddf3ab8d452ec926e9d856e3f8ca1c1a3b8284 Mon Sep 17 00:00:00 2001 From: nikogu <644506165@qq.com> Date: Mon, 28 Aug 2017 14:35:45 +0800 Subject: [PATCH] Extract scaffold to independent git repo --- .editorconfig | 16 + .eslintrc | 40 ++ .gitignore | 11 + .roadhogrc | 26 ++ .roadhogrc.mock.js | 79 ++++ mock/.gitkeep | 0 mock/api.js | 205 +++++++++ mock/chart.js | 184 +++++++++ mock/notices.js | 85 ++++ mock/profile.js | 74 ++++ mock/rule.js | 128 ++++++ mock/utils.js | 45 ++ package.json | 47 +++ public/index.html | 13 + src/assets/yay.jpg | Bin 0 -> 180902 bytes src/common/nav.js | 202 +++++++++ src/components/ActivitiesItem/index.js | 31 ++ src/components/ActivitiesItem/index.less | 41 ++ src/components/AvatarList/index.js | 43 ++ src/components/AvatarList/index.less | 29 ++ src/components/Charts/Bar/index.js | 86 ++++ src/components/Charts/ChartCard/index.js | 34 ++ src/components/Charts/ChartCard/index.less | 45 ++ src/components/Charts/Field/index.js | 12 + src/components/Charts/Field/index.less | 17 + src/components/Charts/Gauge/index.js | 195 +++++++++ src/components/Charts/Icon/index.js | 29 ++ src/components/Charts/MiniArea/index.js | 95 +++++ src/components/Charts/MiniBar/index.js | 78 ++++ src/components/Charts/MiniProgress/index.js | 27 ++ src/components/Charts/MiniProgress/index.less | 37 ++ src/components/Charts/NumberInfo/index.js | 32 ++ src/components/Charts/NumberInfo/index.less | 46 +++ src/components/Charts/Pie/index.js | 224 ++++++++++ src/components/Charts/Pie/index.less | 70 ++++ src/components/Charts/Radar/index.js | 155 +++++++ src/components/Charts/Radar/index.less | 38 ++ src/components/Charts/Trend/index.js | 22 + src/components/Charts/Trend/index.less | 49 +++ src/components/Charts/WaterWave/index.js | 189 +++++++++ src/components/Charts/WaterWave/index.less | 25 ++ src/components/Charts/index.js | 34 ++ src/components/Charts/index.less | 9 + src/components/Countdown/index.js | 108 +++++ src/components/DescriptionList/Description.js | 17 + .../DescriptionList/DescriptionList.js | 18 + src/components/DescriptionList/demo/basic.md | 35 ++ .../DescriptionList/demo/vertical.md | 35 ++ src/components/DescriptionList/index.js | 5 + src/components/DescriptionList/index.less | 50 +++ src/components/DescriptionList/index.md | 29 ++ src/components/DescriptionList/responsive.js | 6 + src/components/EditableLinkGroup/index.js | 46 +++ src/components/EditableLinkGroup/index.less | 29 ++ src/components/Exception/demo/403.md | 21 + src/components/Exception/demo/404.md | 14 + src/components/Exception/demo/500.md | 14 + src/components/Exception/index.js | 26 ++ src/components/Exception/index.less | 37 ++ src/components/Exception/index.md | 19 + src/components/Exception/typeConfig.js | 19 + src/components/FooterToolbar/index.js | 44 ++ src/components/FooterToolbar/index.less | 32 ++ src/components/FooterToolbar/index.md | 9 + src/components/GlobalFooter/demo/basic.md | 29 ++ src/components/GlobalFooter/index.js | 18 + src/components/GlobalFooter/index.less | 23 ++ src/components/GlobalFooter/index.md | 16 + src/components/HeaderSearch/index.js | 65 +++ src/components/HeaderSearch/index.less | 27 ++ src/components/MapChart/index.js | 32 ++ src/components/MapChart/index.less | 10 + src/components/NoticeIcon/NoticeList.js | 44 ++ src/components/NoticeIcon/NoticeList.less | 89 ++++ src/components/NoticeIcon/demo/basic.md | 12 + src/components/NoticeIcon/demo/popover.md | 41 ++ src/components/NoticeIcon/index.js | 93 +++++ src/components/NoticeIcon/index.less | 36 ++ src/components/NoticeIcon/index.md | 39 ++ src/components/PageHeader/demo/image.md | 71 ++++ src/components/PageHeader/demo/simple.md | 26 ++ src/components/PageHeader/demo/standard.md | 81 ++++ src/components/PageHeader/demo/structure.md | 67 +++ src/components/PageHeader/index.js | 98 +++++ src/components/PageHeader/index.less | 95 +++++ src/components/PageHeader/index.md | 26 ++ src/components/RadioText/index.js | 10 + src/components/RadioText/index.less | 12 + src/components/Result/demo/classic.md | 64 +++ src/components/Result/demo/error.md | 39 ++ src/components/Result/demo/structure.md | 20 + src/components/Result/index.js | 21 + src/components/Result/index.less | 45 ++ src/components/Result/index.md | 19 + src/components/SearchInput/index.js | 15 + src/components/SearchInput/index.less | 45 ++ src/components/StandardFormRow/index.js | 24 ++ src/components/StandardFormRow/index.less | 68 +++ src/components/StandardTable/index.js | 149 +++++++ src/components/StandardTable/index.less | 22 + src/components/TagCloud/index.js | 134 ++++++ src/components/TagSelect/index.js | 164 ++++++++ src/components/TagSelect/index.less | 26 ++ src/components/TimelineChart/index.js | 104 +++++ src/components/TimelineChart/index.less | 3 + src/index.js | 25 ++ src/index.less | 14 + src/layouts/BasicLayout.js | 260 ++++++++++++ src/layouts/BasicLayout.less | 95 +++++ src/layouts/BlankLayout.js | 3 + src/layouts/PageHeaderLayout.js | 9 + src/layouts/UserLayout.js | 60 +++ src/layouts/UserLayout.less | 46 +++ src/models/activities.js | 43 ++ src/models/chart.js | 64 +++ src/models/form.js | 88 ++++ src/models/global.js | 53 +++ src/models/index.js | 11 + src/models/list.js | 46 +++ src/models/login.js | 58 +++ src/models/monitor.js | 28 ++ src/models/profile.js | 45 ++ src/models/project.js | 43 ++ src/models/register.js | 42 ++ src/models/rule.js | 80 ++++ src/models/user.js | 57 +++ src/router.js | 47 +++ src/routes/Dashboard.css | 5 + src/routes/Dashboard.js | 100 +++++ src/routes/Dashboard/Analysis.js | 388 ++++++++++++++++++ src/routes/Dashboard/Analysis.less | 100 +++++ src/routes/Dashboard/Monitor.js | 191 +++++++++ src/routes/Dashboard/Monitor.less | 45 ++ src/routes/Dashboard/Workplace.js | 263 ++++++++++++ src/routes/Dashboard/Workplace.less | 191 +++++++++ src/routes/Exception/403.js | 4 + src/routes/Exception/404.js | 4 + src/routes/Exception/500.js | 4 + src/routes/Forms/AdvancedForm.js | 267 ++++++++++++ src/routes/Forms/BasicForm.js | 145 +++++++ src/routes/Forms/StepForm/Step1.js | 101 +++++ src/routes/Forms/StepForm/Step2.js | 89 ++++ src/routes/Forms/StepForm/Step3.js | 51 +++ src/routes/Forms/StepForm/index.js | 63 +++ src/routes/Forms/StepForm/style.less | 59 +++ src/routes/Forms/TableForm.js | 200 +++++++++ src/routes/Forms/style.less | 78 ++++ src/routes/List/BasicList.js | 145 +++++++ src/routes/List/BasicList.less | 103 +++++ src/routes/List/CardList.js | 91 ++++ src/routes/List/CardList.less | 71 ++++ src/routes/List/CoverCardList.js | 206 ++++++++++ src/routes/List/CoverCardList.less | 24 ++ src/routes/List/FilterCardList.js | 211 ++++++++++ src/routes/List/FilterCardList.less | 54 +++ src/routes/List/SearchList.js | 274 +++++++++++++ src/routes/List/SearchList.less | 45 ++ src/routes/List/TableList.js | 265 ++++++++++++ src/routes/List/TableList.less | 23 ++ src/routes/Profile.js | 255 ++++++++++++ src/routes/Profile.less | 27 ++ src/routes/Result/Error.js | 37 ++ src/routes/Result/Success.js | 69 ++++ src/routes/User/Login.js | 172 ++++++++ src/routes/User/Login.less | 79 ++++ src/routes/User/Register.js | 261 ++++++++++++ src/routes/User/Register.less | 84 ++++ src/routes/User/RegisterResult.js | 23 ++ src/services/api.js | 82 ++++ src/services/user.js | 9 + src/utils/request.js | 38 ++ src/utils/utils.js | 55 +++ src/utils/utils.less | 48 +++ 173 files changed, 11798 insertions(+) create mode 100755 .editorconfig create mode 100755 .eslintrc create mode 100755 .gitignore create mode 100755 .roadhogrc create mode 100644 .roadhogrc.mock.js create mode 100644 mock/.gitkeep create mode 100644 mock/api.js create mode 100644 mock/chart.js create mode 100644 mock/notices.js create mode 100644 mock/profile.js create mode 100644 mock/rule.js create mode 100644 mock/utils.js create mode 100755 package.json create mode 100755 public/index.html create mode 100644 src/assets/yay.jpg create mode 100644 src/common/nav.js create mode 100644 src/components/ActivitiesItem/index.js create mode 100644 src/components/ActivitiesItem/index.less create mode 100644 src/components/AvatarList/index.js create mode 100644 src/components/AvatarList/index.less create mode 100644 src/components/Charts/Bar/index.js create mode 100644 src/components/Charts/ChartCard/index.js create mode 100644 src/components/Charts/ChartCard/index.less create mode 100644 src/components/Charts/Field/index.js create mode 100644 src/components/Charts/Field/index.less create mode 100644 src/components/Charts/Gauge/index.js create mode 100644 src/components/Charts/Icon/index.js create mode 100644 src/components/Charts/MiniArea/index.js create mode 100644 src/components/Charts/MiniBar/index.js create mode 100644 src/components/Charts/MiniProgress/index.js create mode 100644 src/components/Charts/MiniProgress/index.less create mode 100644 src/components/Charts/NumberInfo/index.js create mode 100644 src/components/Charts/NumberInfo/index.less create mode 100644 src/components/Charts/Pie/index.js create mode 100644 src/components/Charts/Pie/index.less create mode 100644 src/components/Charts/Radar/index.js create mode 100644 src/components/Charts/Radar/index.less create mode 100644 src/components/Charts/Trend/index.js create mode 100644 src/components/Charts/Trend/index.less create mode 100644 src/components/Charts/WaterWave/index.js create mode 100644 src/components/Charts/WaterWave/index.less create mode 100644 src/components/Charts/index.js create mode 100644 src/components/Charts/index.less create mode 100644 src/components/Countdown/index.js create mode 100644 src/components/DescriptionList/Description.js create mode 100644 src/components/DescriptionList/DescriptionList.js create mode 100644 src/components/DescriptionList/demo/basic.md create mode 100644 src/components/DescriptionList/demo/vertical.md create mode 100644 src/components/DescriptionList/index.js create mode 100644 src/components/DescriptionList/index.less create mode 100644 src/components/DescriptionList/index.md create mode 100644 src/components/DescriptionList/responsive.js create mode 100644 src/components/EditableLinkGroup/index.js create mode 100644 src/components/EditableLinkGroup/index.less create mode 100644 src/components/Exception/demo/403.md create mode 100644 src/components/Exception/demo/404.md create mode 100644 src/components/Exception/demo/500.md create mode 100644 src/components/Exception/index.js create mode 100644 src/components/Exception/index.less create mode 100644 src/components/Exception/index.md create mode 100644 src/components/Exception/typeConfig.js create mode 100644 src/components/FooterToolbar/index.js create mode 100644 src/components/FooterToolbar/index.less create mode 100644 src/components/FooterToolbar/index.md create mode 100644 src/components/GlobalFooter/demo/basic.md create mode 100644 src/components/GlobalFooter/index.js create mode 100644 src/components/GlobalFooter/index.less create mode 100644 src/components/GlobalFooter/index.md create mode 100644 src/components/HeaderSearch/index.js create mode 100644 src/components/HeaderSearch/index.less create mode 100644 src/components/MapChart/index.js create mode 100644 src/components/MapChart/index.less create mode 100644 src/components/NoticeIcon/NoticeList.js create mode 100644 src/components/NoticeIcon/NoticeList.less create mode 100644 src/components/NoticeIcon/demo/basic.md create mode 100644 src/components/NoticeIcon/demo/popover.md create mode 100644 src/components/NoticeIcon/index.js create mode 100644 src/components/NoticeIcon/index.less create mode 100644 src/components/NoticeIcon/index.md create mode 100644 src/components/PageHeader/demo/image.md create mode 100644 src/components/PageHeader/demo/simple.md create mode 100644 src/components/PageHeader/demo/standard.md create mode 100644 src/components/PageHeader/demo/structure.md create mode 100644 src/components/PageHeader/index.js create mode 100644 src/components/PageHeader/index.less create mode 100644 src/components/PageHeader/index.md create mode 100644 src/components/RadioText/index.js create mode 100644 src/components/RadioText/index.less create mode 100644 src/components/Result/demo/classic.md create mode 100644 src/components/Result/demo/error.md create mode 100644 src/components/Result/demo/structure.md create mode 100644 src/components/Result/index.js create mode 100644 src/components/Result/index.less create mode 100644 src/components/Result/index.md create mode 100644 src/components/SearchInput/index.js create mode 100644 src/components/SearchInput/index.less create mode 100644 src/components/StandardFormRow/index.js create mode 100644 src/components/StandardFormRow/index.less create mode 100644 src/components/StandardTable/index.js create mode 100644 src/components/StandardTable/index.less create mode 100644 src/components/TagCloud/index.js create mode 100644 src/components/TagSelect/index.js create mode 100644 src/components/TagSelect/index.less create mode 100644 src/components/TimelineChart/index.js create mode 100644 src/components/TimelineChart/index.less create mode 100644 src/index.js create mode 100644 src/index.less create mode 100644 src/layouts/BasicLayout.js create mode 100644 src/layouts/BasicLayout.less create mode 100644 src/layouts/BlankLayout.js create mode 100644 src/layouts/PageHeaderLayout.js create mode 100644 src/layouts/UserLayout.js create mode 100644 src/layouts/UserLayout.less create mode 100644 src/models/activities.js create mode 100644 src/models/chart.js create mode 100644 src/models/form.js create mode 100644 src/models/global.js create mode 100644 src/models/index.js create mode 100644 src/models/list.js create mode 100644 src/models/login.js create mode 100644 src/models/monitor.js create mode 100644 src/models/profile.js create mode 100644 src/models/project.js create mode 100644 src/models/register.js create mode 100644 src/models/rule.js create mode 100644 src/models/user.js create mode 100644 src/router.js create mode 100644 src/routes/Dashboard.css create mode 100644 src/routes/Dashboard.js create mode 100644 src/routes/Dashboard/Analysis.js create mode 100644 src/routes/Dashboard/Analysis.less create mode 100644 src/routes/Dashboard/Monitor.js create mode 100644 src/routes/Dashboard/Monitor.less create mode 100644 src/routes/Dashboard/Workplace.js create mode 100644 src/routes/Dashboard/Workplace.less create mode 100644 src/routes/Exception/403.js create mode 100644 src/routes/Exception/404.js create mode 100644 src/routes/Exception/500.js create mode 100644 src/routes/Forms/AdvancedForm.js create mode 100644 src/routes/Forms/BasicForm.js create mode 100644 src/routes/Forms/StepForm/Step1.js create mode 100644 src/routes/Forms/StepForm/Step2.js create mode 100644 src/routes/Forms/StepForm/Step3.js create mode 100644 src/routes/Forms/StepForm/index.js create mode 100644 src/routes/Forms/StepForm/style.less create mode 100644 src/routes/Forms/TableForm.js create mode 100644 src/routes/Forms/style.less create mode 100644 src/routes/List/BasicList.js create mode 100644 src/routes/List/BasicList.less create mode 100644 src/routes/List/CardList.js create mode 100644 src/routes/List/CardList.less create mode 100644 src/routes/List/CoverCardList.js create mode 100644 src/routes/List/CoverCardList.less create mode 100644 src/routes/List/FilterCardList.js create mode 100644 src/routes/List/FilterCardList.less create mode 100644 src/routes/List/SearchList.js create mode 100644 src/routes/List/SearchList.less create mode 100644 src/routes/List/TableList.js create mode 100644 src/routes/List/TableList.less create mode 100644 src/routes/Profile.js create mode 100644 src/routes/Profile.less create mode 100644 src/routes/Result/Error.js create mode 100644 src/routes/Result/Success.js create mode 100644 src/routes/User/Login.js create mode 100644 src/routes/User/Login.less create mode 100644 src/routes/User/Register.js create mode 100644 src/routes/User/Register.less create mode 100644 src/routes/User/RegisterResult.js create mode 100644 src/services/api.js create mode 100644 src/services/user.js create mode 100644 src/utils/request.js create mode 100644 src/utils/utils.js create mode 100644 src/utils/utils.less diff --git a/.editorconfig b/.editorconfig new file mode 100755 index 00000000..7e3649ac --- /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 100755 index 00000000..a655538f --- /dev/null +++ b/.eslintrc @@ -0,0 +1,40 @@ +{ + "parser": "babel-eslint", + "extends": "airbnb", + "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], + "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], + "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" + }], + "require-yield": [1] + }, + "parserOptions": { + "ecmaFeatures": { + "experimentalObjectRestSpread": true + } + } +} diff --git a/.gitignore b/.gitignore new file mode 100755 index 00000000..aa6e4c4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules + +# production +/dist + +# misc +.DS_Store +npm-debug.log* diff --git a/.roadhogrc b/.roadhogrc new file mode 100755 index 00000000..b693be04 --- /dev/null +++ b/.roadhogrc @@ -0,0 +1,26 @@ +{ + "entry": "src/index.js", + "env": { + "development": { + "extraBabelPlugins": [ + "dva-hmr", + "transform-runtime", + "transform-decorators-legacy", + ["import", { "libraryName": "antd", "style": true }] + ] + }, + "production": { + "extraBabelPlugins": [ + "transform-runtime", + "transform-decorators-legacy", + ["import", { "libraryName": "antd", "style": true }] + ] + } + }, + "theme": { + "font-size-base": "14px", + "badge-font-size": "12px", + "btn-font-size-lg": "@font-size-base", + "layout-body-background": "#f5f5f5" + } +} diff --git a/.roadhogrc.mock.js b/.roadhogrc.mock.js new file mode 100644 index 00000000..d086fa8d --- /dev/null +++ b/.roadhogrc.mock.js @@ -0,0 +1,79 @@ +import mockjs from 'mockjs'; +import { getRule, postRule } from './mock/rule'; +import { getActivities, getNotice, getFakeList } from './mock/api'; +import { getFakeChartData } from './mock/chart'; +import { imgMap } from './mock/utils'; +import { getProfileData } from './mock/profile'; +import { getNotices } from './mock/notices'; +import { format, delay } from 'roadhog-api-doc'; + +// 代码中会兼容本地 service mock 以及部署站点的静态数据 + +const proxy = { + // 支持值为 Object 和 Array + 'GET /api/currentUser': { + $desc: "获取当前用户接口", + $params: { + pageSize: { + desc: '分页', + exp: 2, + }, + }, + $body: { + name: 'momo.zxy', + avatar: imgMap.user, + 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('Ok'); + }, + 'GET /api/tags': mockjs.mock({ + 'list|100': [{ name: '@city', 'value|1-100': 50, 'type|0-2': 1 }] + }), + 'GET /api/fake_list': getFakeList, + 'GET /api/fake_chart_data': getFakeChartData, + 'GET /api/profile': getProfileData, + 'POST /api/login/account': (req, res) => { + res.send({ status: 'error', type: 'account' }); + }, + 'POST /api/login/mobile': (req, res) => { + res.send({ status: 'ok', type: 'mobile' }); + }, + 'POST /api/register': (req, res) => { + res.send({ status: 'ok' }); + }, + 'GET /api/notices': getNotices, +}; + +export default delay(proxy, 1000); diff --git a/mock/.gitkeep b/mock/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/mock/api.js b/mock/api.js new file mode 100644 index 00000000..d652f5bd --- /dev/null +++ b/mock/api.js @@ -0,0 +1,205 @@ +import { imgMap, getUrlParams } from './utils'; + +export function fakeList(count) { + const titles = [ + '凤蝶', + 'AntDesignPro', + 'DesignLab', + 'Basement', + 'AntDesign', + '云雀', + '体验云', + 'AntDesignMobile', + ]; + const avatars = [ + 'https://gw.alipayobjects.com/zos/rmsportal/hYjIZrUoBfNxOAYBVDfc.png', // 凤蝶 + 'https://gw.alipayobjects.com/zos/rmsportal/HHWPIzPLCLYmVuPivyiA.png', // 云雀 + 'https://gw.alipayobjects.com/zos/rmsportal/irqByKtOdKfDojxIWTXF.png', // Basement + 'https://gw.alipayobjects.com/zos/rmsportal/VcmdbCBcwPTGYgbYeMzX.png', // DesignLab + ]; + const covers = [ + 'https://gw.alipayobjects.com/zos/rmsportal/JiqGstEfoWAOHiTxclqi.png', + 'https://gw.alipayobjects.com/zos/rmsportal/xMPpMvGSIXusgtgUPAdw.png', + 'https://gw.alipayobjects.com/zos/rmsportal/hQReiajgtqzIVFjLXjHp.png', + 'https://gw.alipayobjects.com/zos/rmsportal/nczfTaXEzhSpvgZZjBev.png', + ]; + + const list = []; + for (let i = 0; i < count; i += 1) { + list.push({ + id: `fake-list-${i}`, + owner: '曲丽丽', + title: titles[i % 8], + avatar: avatars[i % 4], + cover: covers[i % 4], + status: ['active', 'exception', 'normal'][i % 3], + percent: Math.ceil(Math.random() * 50) + 50, + logo: ['https://gw.alipayobjects.com/zos/rmsportal/KoJjkdbuTFxzJmmjuDVR.png', 'https://gw.alipayobjects.com/zos/rmsportal/UxGORCvEXJEsxOfEKZiA.png'][i % 2], + 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: '一句话描述一句话描述', + 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: '段落示意:蚂蚁金服设计平台 design.alipay.com,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。蚂蚁金服设计平台 design.alipay.com,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。', + members: [ + { + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/WPOxPBHGyqsgKPsFtVlJ.png', + name: '王昭君', + }, + { + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/WPOxPBHGyqsgKPsFtVlJ.png', + name: '王昭君', + }, + { + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/WPOxPBHGyqsgKPsFtVlJ.png', + name: '王昭君', + }, + { + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/WPOxPBHGyqsgKPsFtVlJ.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; + } + + const params = getUrlParams(url); + + 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: '消息列表体验优化', + logo: imgMap.b, + description: '这是一条描述信息这是一条描述信息', + updatedAt: new Date(), + member: '蜂鸟项目组', + }, + { + id: 'xxx2', + title: 'XX 平台', + logo: imgMap.c, + description: '这是一条描述信息', + updatedAt: new Date('2017-07-24 11:00:00'), + member: '凤蝶精英小分队', + }, + { + id: 'xxx3', + title: '消息列表体验优化', + logo: imgMap.a, + description: '这是一条描述信息这是一条描述信息', + updatedAt: new Date(), + member: '蜂鸟项目组', + }, + { + id: 'xxx4', + title: '文档中心1', + logo: imgMap.a, + description: '这是一条描述信息这是一条描述信息', + updatedAt: new Date('2017-07-23 06:23:00'), + member: '成都超级小分队', + }, + { + id: 'xxx5', + title: '文档中心2', + logo: imgMap.b, + description: '这是一条描述信息这是一条描述信息', + updatedAt: new Date('2017-07-23 06:23:00'), + member: '成都超级小分队', + }, + { + id: 'xxx6', + title: '智能运营中心', + logo: imgMap.c, + description: '这是一条描述信息这是一条描述信息', + updatedAt: new Date('2017-07-23 06:23:00'), + member: '成都超级小分队', + }, +]; + +export const getActivities = [ + { + id: 'trend-1', + updatedAt: new Date(), + user: { + name: '林东东', + avatar: imgMap.a, + }, + action: '在 [凤蝶精英小分队](http://github.com/) 新建项目 [六月迭代](http://github.com/)', + }, + { + id: 'trend-2', + updatedAt: new Date(), + user: { + name: '林嘻嘻', + avatar: imgMap.c, + }, + action: '在 [凤蝶精英小分队](http://github.com/) 新建项目 [六月迭代](http://github.com/)', + }, + { + id: 'trend-3', + updatedAt: new Date(), + user: { + name: '林囡囡', + avatar: imgMap.b, + }, + action: '在 [凤蝶精英小分队](http://github.com/) 新建项目 [六月迭代](http://github.com/)', + }, + { + id: 'trend-4', + updatedAt: new Date(), + user: { + name: '林贝贝', + avatar: imgMap.c, + }, + action: '在 [5 月日常迭代](http://github.com/) 更新至已发布状态', + }, + { + id: 'trend-5', + updatedAt: new Date(), + user: { + name: '林忠忠', + avatar: imgMap.a, + }, + action: '在 [工程效能](http://github.com/) 发布了 [留言](http://github.com/)', + }, + { + id: 'trend-6', + updatedAt: new Date(), + user: { + name: '林呜呜', + avatar: imgMap.d, + }, + action: '在 [云雀](http://github.com/) 新建项目 [品牌迭代](http://github.com/)', + }, +]; + + +export default { + getNotice, + getActivities, + getFakeList, +}; diff --git a/mock/chart.js b/mock/chart.js new file mode 100644 index 00000000..d28c5521 --- /dev/null +++ b/mock/chart.js @@ -0,0 +1,184 @@ +import moment from 'moment'; + +// mock data +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, + }); +} +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, + 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 00000000..782fac4f --- /dev/null +++ b/mock/notices.js @@ -0,0 +1,85 @@ +export default { + 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: 'urgent', + 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: '待办', + }]); + }, +}; diff --git a/mock/profile.js b/mock/profile.js new file mode 100644 index 00000000..d2e6083d --- /dev/null +++ b/mock/profile.js @@ -0,0 +1,74 @@ +const operation1 = [ + { + 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 operation2 = [ + { + key: 'op1', + type: '订购关系生效', + name: '曲丽丽', + status: 'agree', + updatedAt: '2017-10-03 19:23:12', + memo: '-', + }, +]; + +const operation3 = [ + { + key: 'op1', + type: '创建订单', + name: '汗牙牙', + status: 'agree', + updatedAt: '2017-10-03 19:23:12', + memo: '-', + }, +]; + +export const getProfileData = { + operation1, + operation2, + operation3, +}; + +export default { + getProfileData, +}; diff --git a/mock/rule.js b/mock/rule.js new file mode 100644 index 00000000..13a4f353 --- /dev/null +++ b/mock/rule.js @@ -0,0 +1,128 @@ +import { getUrlParams } from './utils'; + +// mock tableListDataSource +let tableListDataSource = []; +for (let i = 0; i < 46; i += 1) { + tableListDataSource.push({ + 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(`2017-07-${Math.floor(i / 2) + 1} ${Math.floor(i / 2) + 1}:00:00`), + createdAt: new Date(`2017-07-${Math.floor(i / 2) + 1} ${Math.floor(i / 2) + 1}:00:00`), + 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; + } + + const params = getUrlParams(url); + + 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 s = params.status.split(','); + if (s.length === 1) { + dataSource = dataSource.filter(data => parseInt(data.status, 10) === parseInt(s[0], 10)); + } + } + + 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; + } + + const body = (b && b.body) || req.body; + const method = body.method; + + switch (method) { + /* eslint no-case-declarations:0 */ + case 'delete': + const no = body.no; + tableListDataSource = tableListDataSource.filter(item => no.indexOf(item.no) === -1); + break; + case 'post': + const description = body.description; + 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/mock/utils.js b/mock/utils.js new file mode 100644 index 00000000..6e1c72fe --- /dev/null +++ b/mock/utils.js @@ -0,0 +1,45 @@ +export const imgMap = { + user: 'https://gw.alipayobjects.com/zos/rmsportal/YdMCpIJULitXfqHCFPbF.png', + a: 'https://gw.alipayobjects.com/zos/rmsportal/ZrkcSjizAKNWwJTwcadT.png', + b: 'https://gw.alipayobjects.com/zos/rmsportal/KYlwHMeomKQbhJDRUVvt.png', + c: 'https://gw.alipayobjects.com/zos/rmsportal/gabvleTstEvzkbQRfjxu.png', + d: 'https://gw.alipayobjects.com/zos/rmsportal/jvpNzacxUYLlNsHTtrAD.png', +}; + +// refers: https://www.sitepoint.com/get-url-parameters-with-javascript/ +export function getUrlParams(url) { + const d = decodeURIComponent; + let queryString = url ? url.split('?')[1] : window.location.search.slice(1); + const obj = {}; + if (queryString) { + queryString = queryString.split('#')[0]; + const arr = queryString.split('&'); + for (let i = 0; i < arr.length; i += 1) { + const a = arr[i].split('='); + let paramNum; + const paramName = a[0].replace(/\[\d*\]/, (v) => { + paramNum = v.slice(1, -1); + return ''; + }); + const paramValue = typeof (a[1]) === 'undefined' ? true : a[1]; + if (obj[paramName]) { + if (typeof obj[paramName] === 'string') { + obj[paramName] = d([obj[paramName]]); + } + if (typeof paramNum === 'undefined') { + obj[paramName].push(d(paramValue)); + } else { + obj[paramName][paramNum] = d(paramValue); + } + } else { + obj[paramName] = d(paramValue); + } + } + } + return obj; +} + +export default { + getUrlParams, + imgMap, +}; diff --git a/package.json b/package.json new file mode 100755 index 00000000..834aa128 --- /dev/null +++ b/package.json @@ -0,0 +1,47 @@ +{ + "name": "ant-design-admin", + "private": true, + "scripts": { + "start": "roadhog server", + "build": "roadhog build", + "lint": "eslint --ext .js src test", + "precommit": "npm run lint" + }, + "dependencies": { + "antd": "next", + "dva": "^1.2.1", + "g-cloud": "^1.0.2-beta", + "g2": "^2.3.8", + "g2-plugin-slider": "^1.2.1", + "lodash": "^4.17.4", + "marked": "^0.3.6", + "numeral": "^2.0.6", + "prop-types": "^15.5.10", + "qs": "^6.5.0", + "react": "^15.4.0", + "react-document-title": "^2.0.3", + "react-dom": "^15.4.0", + "react-redux": "4.x || 5.x", + "react-router": "2.x || 3.x" + }, + "devDependencies": { + "babel-eslint": "^7.1.1", + "babel-plugin-dva-hmr": "^0.3.2", + "babel-plugin-import": "^1.2.1", + "babel-plugin-transform-decorators-legacy": "^1.3.4", + "babel-plugin-transform-runtime": "^6.9.0", + "babel-runtime": "^6.9.2", + "eslint": "^3.0.0", + "eslint-config-airbnb": "latest", + "eslint-plugin-babel": "^4.0.0", + "eslint-plugin-import": "^2.2.0", + "eslint-plugin-jsx-a11y": "^5.0.1", + "eslint-plugin-react": "^7.0.1", + "expect": "^1.20.2", + "husky": "^0.13.4", + "mockjs": "^1.0.1-beta3", + "redbox-react": "^1.3.2", + "roadhog": "^1.0.2", + "roadhog-api-doc": "^0.1.0" + } +} diff --git a/public/index.html b/public/index.html new file mode 100755 index 00000000..60ed560f --- /dev/null +++ b/public/index.html @@ -0,0 +1,13 @@ + + + + + + Ant Design Pro + + + +
+ + + diff --git a/src/assets/yay.jpg b/src/assets/yay.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e72bd8ffaecc9de0e74a2bd7fa3e741efde4aa99 GIT binary patch literal 180902 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μ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

J>?r<- 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 { + if (item.children) { + userAdapter(item); + } else { + const userItem = item; + userItem.target = '_blank'; + userItem.noRoute = true; + } + }); + return userData; +} + +export const user = [{ + name: '帐户', + icon: 'setting', + path: 'user', + children: [{ + name: '登录', + path: 'login', + component: Login, + icon: 'setting', + }, { + name: '注册', + path: 'register', + component: Register, + icon: 'setting', + }, { + name: '注册结果', + path: 'register-result', + component: RegisterResult, + icon: 'setting', + }], +}]; + +export const menus = [{ + name: 'Dashboard', + icon: 'setting', + path: 'dashboard', + children: [{ + name: '分析页', + path: 'analysis', + component: Analysis, + icon: 'setting', + }, { + name: '监控页', + path: 'monitor', + component: Monitor, + icon: 'setting', + }, { + name: '工作台', + path: 'workplace', + component: Workplace, + icon: 'setting', + }], +}, { + name: '表单页', + path: 'form', + icon: 'setting', + children: [{ + name: '基础表单', + path: 'basic-form', + component: BasicForm, + icon: 'setting', + }, { + name: '分步表单', + path: 'step-form', + component: StepForm, + icon: 'setting', + children: [{ + path: 'confirm', + component: Step2, + }, { + path: 'result', + component: Step3, + }], + }, { + name: '高级表单', + path: 'advanced-form', + component: AdvancedForm, + icon: 'setting', + }], +}, { + name: '列表页', + path: 'list', + icon: 'setting', + children: [{ + name: '标准表格(表格查询)', + path: 'table-list', + component: TableList, + icon: 'setting', + }, { + name: '标准列表', + path: 'basic-list', + component: BasicList, + icon: 'setting', + }, { + name: '卡片列表', + path: 'card-list', + component: CardList, + icon: 'setting', + }, { + name: '卡片列表(封面)', + path: 'cover-card-list', + component: CoverCardList, + icon: 'setting', + }, { + name: '带筛选卡片列表', + path: 'filter-card-list', + component: FilterCardList, + icon: 'setting', + }, { + name: '搜索列表', + path: 'search', + component: SearchList, + icon: 'setting', + }], +}, { + name: '详情页', + path: 'profile', + component: Profile, + icon: 'setting', +}, { + name: '结果', + path: 'result', + icon: 'setting', + children: [{ + name: '成功', + path: 'success', + component: Success, + icon: 'setting', + }, { + name: '失败', + path: 'fail', + component: Error, + icon: 'setting', + }], +}, { + name: '错误', + path: 'error', + icon: 'setting', + children: [{ + name: '403', + path: '403', + component: Exception403, + icon: 'setting', + }, { + name: '404', + path: '404', + component: Exception404, + icon: 'setting', + }, { + name: '500', + path: '500', + component: Exception500, + icon: 'setting', + }], +}, userAdapter(JSON.parse(JSON.stringify(user[0])))]; + + +export default [{ + component: BasicLayout, + name: '首页', + children: menus, + path: '', +}, { + component: UserLayout, + name: '账户', + children: user, +}]; diff --git a/src/components/ActivitiesItem/index.js b/src/components/ActivitiesItem/index.js new file mode 100644 index 00000000..8f74749b --- /dev/null +++ b/src/components/ActivitiesItem/index.js @@ -0,0 +1,31 @@ +import React from 'react'; +import moment from 'moment'; +import marked from 'marked'; +import { Avatar } from 'antd'; + +import styles from './index.less'; + +/* eslint react/no-danger:0 */ +export default ({ data: { user, updatedAt, action } }) => ( +

+
+ { + user.link && + + + } + { + !user.link && {user.title} + } +
+
+
+ {user.name} +
+
+

{moment(updatedAt).fromNow()}

+
+
+); diff --git a/src/components/ActivitiesItem/index.less b/src/components/ActivitiesItem/index.less new file mode 100644 index 00000000..0c95640f --- /dev/null +++ b/src/components/ActivitiesItem/index.less @@ -0,0 +1,41 @@ +@import "~antd/lib/style/themes/default.less"; + +.activitiesItem { + padding: 24px 24px 0 24px; + position: relative; + + .avatar { + position: absolute; + top: 24px; + left: 24px; + img { + display: block; + border-radius: 32px; + width: 32px; + height: 32px; + } + } + .content { + border-bottom: 1px solid @border-color-split; + padding-left: 48px; + padding-bottom: 24px; + font-size: @font-size-base; + a { + color: @primary-color; + } + & > div { + line-height: 22px; + .name { + margin-right: 4px; + font-weight: 500; + } + div, p { + display: inline-block; + } + } + & > p { + margin-top: 4px; + line-height: 22px; + } + } +} diff --git a/src/components/AvatarList/index.js b/src/components/AvatarList/index.js new file mode 100644 index 00000000..7ae18577 --- /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', + }); + + 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 00000000..ba720535 --- /dev/null +++ b/src/components/AvatarList/index.less @@ -0,0 +1,29 @@ +@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; + overflow: hidden; + font-size: @font-size-base; + margin-left: -8px; + width: @avatar-size-base; + height: @avatar-size-base; +} + +.avatarItemLarge { + width: @avatar-size-lg; + height: @avatar-size-lg; +} + +.avatarItemSmall { + width: @avatar-size-sm; + height: @avatar-size-sm; +} diff --git a/src/components/Charts/Bar/index.js b/src/components/Charts/Bar/index.js new file mode 100644 index 00000000..537c4fc4 --- /dev/null +++ b/src/components/Charts/Bar/index.js @@ -0,0 +1,86 @@ +import React, { PureComponent } from 'react'; +import G2 from 'g2'; +import styles from '../index.less'; + +class Bar extends PureComponent { + componentDidMount() { + this.renderChart(this.props.data); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.data !== this.props.data) { + this.renderChart(nextProps.data); + } + } + + handleRef = (n) => { + this.node = n; + } + + renderChart(data) { + const { height = 0, fit = true, color = '#33abfb', margin = [32, 0, 32, 40] } = this.props; + + if (!data || (data && data.length < 1)) { + return; + } + + // clean + this.node.innerHTML = ''; + + const Frame = G2.Frame; + const frame = new Frame(data); + + const chart = new G2.Chart({ + container: this.node, + forceFit: fit, + height: height - 22, + legend: null, + plotCfg: { + margin, + }, + }); + + chart.axis('x', { + title: false, + }); + chart.axis('y', { + title: false, + line: false, + tickLine: false, + }); + + chart.source(frame, { + x: { + type: 'cat', + }, + y: { + min: 0, + }, + }); + + chart.tooltip({ + title: null, + crosshairs: false, + map: { + name: 'x', + }, + }); + chart.interval().position('x*y').color(color); + chart.render(); + } + + render() { + const { height, title } = this.props; + + return ( +
    +
    + { title &&

    {title}

    } +
    +
    +
    + ); + } +} + +export default Bar; diff --git a/src/components/Charts/ChartCard/index.js b/src/components/Charts/ChartCard/index.js new file mode 100644 index 00000000..40faf796 --- /dev/null +++ b/src/components/Charts/ChartCard/index.js @@ -0,0 +1,34 @@ +import React from 'react'; +import { Card } from 'antd'; + +import styles from './index.less'; + +const ChartCard = ({ contentHeight, title, action, total, footer, children, ...rest }) => ( + +
    +
    + {title} + {action} +
    + { + // eslint-disable-next-line + total &&

    + } +

    +
    + {children} +
    +
    + { + footer &&
    + {footer} +
    + } +
    +
    +); + +export default ChartCard; diff --git a/src/components/Charts/ChartCard/index.less b/src/components/Charts/ChartCard/index.less new file mode 100644 index 00000000..d204e63a --- /dev/null +++ b/src/components/Charts/ChartCard/index.less @@ -0,0 +1,45 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../../utils/utils.less"; + +.chartCard { + position: relative; + .meta { + color: @text-color-secondary; + font-size: @font-size-base; + position: relative; + line-height: 22px; + height: 22px; + } + .action { + cursor: pointer; + position: absolute; + top: 0; + right: 0; + } + .total { + .textOverflow(); + color: @heading-color; + margin-top: 8px; + font-size: 30px; + line-height: 38px; + height: 38px; + } + .content { + position: relative; + width: 100%; + } + .contentFixed { + position: absolute; + left: 0; + bottom: 0; + width: 100%; + } + .footer { + border-top: 1px solid @border-color-split; + padding-top: 8px; + margin-top: 11px; + & > * { + position: relative; + } + } +} diff --git a/src/components/Charts/Field/index.js b/src/components/Charts/Field/index.js new file mode 100644 index 00000000..ed525c56 --- /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 00000000..63894f9d --- /dev/null +++ b/src/components/Charts/Field/index.less @@ -0,0 +1,17 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../../utils/utils.less"; + +.field { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + span { + font-size: @font-size-base; + line-height: 22px; + } + span:last-child { + font-weight: 600; + margin-left: 8px; + } +} + diff --git a/src/components/Charts/Gauge/index.js b/src/components/Charts/Gauge/index.js new file mode 100644 index 00000000..0c53540e --- /dev/null +++ b/src/components/Charts/Gauge/index.js @@ -0,0 +1,195 @@ +import React, { PureComponent } from 'react'; +import G2 from 'g2'; + +const Shape = G2.Shape; + +/* eslint no-underscore-dangle: 0 */ +class Gauge extends PureComponent { + componentDidMount() { + this.renderChart(); + } + + componentWillReceiveProps(nextProps) { + this.renderChart(nextProps); + } + + handleRef = (n) => { + this.node = n; + } + + initChart(nextProps) { + const { title, color = '#00b1f8' } = nextProps || this.props; + + Shape.registShape('point', 'dashBoard', { + drawShape(cfg, group) { + const originPoint = cfg.points[0]; + const point = this.parsePoint({ x: originPoint.x, y: 0.4 }); + + const center = this.parsePoint({ + x: 0, + y: 0, + }); + + const shape = group.addShape('polygon', { + attrs: { + points: [ + [center.x, center.y], + [point.x + 8, point.y], + [point.x + 8, point.y - 2], + [center.x, center.y - 2], + ], + radius: 2, + lineWidth: 2, + arrow: false, + fill: color, + }, + }); + + group.addShape('Marker', { + attrs: { + symbol: 'circle', + lineWidth: 2, + fill: color, + radius: 8, + x: center.x, + y: center.y, + }, + }); + group.addShape('Marker', { + attrs: { + symbol: 'circle', + lineWidth: 2, + fill: '#fff', + radius: 5, + x: center.x, + y: center.y, + }, + }); + + const origin = cfg.origin; + group.addShape('text', { + attrs: { + x: center.x, + y: center.y + 80, + text: `${origin._origin.value}%`, + textAlign: 'center', + fontSize: 24, + fill: 'rgba(0, 0, 0, 0.85)', + }, + }); + group.addShape('text', { + attrs: { + x: center.x, + y: center.y + 45, + text: title, + textAlign: 'center', + fontSize: 14, + fill: 'rgba(0, 0, 0, 0.43)', + }, + }); + + return shape; + }, + }); + } + + renderChart(nextProps) { + const { height, color = '#00b1f8', bgColor = '#d3f3fe', title, percent } = nextProps || this.props; + const data = [{ name: title, value: percent }]; + + if (this.chart) { + this.chart.clear(); + } + if (this.node) { + this.node.innerHTML = ''; + } + + this.initChart(nextProps); + + const chart = new G2.Chart({ + container: this.node, + forceFit: true, + height, + animate: false, + plotCfg: { + margin: [10, 0, 30, 0], + }, + }); + + chart.source(data); + + chart.tooltip(false); + + chart.coord('gauge', { + startAngle: -1.2 * Math.PI, + endAngle: 0.20 * Math.PI, + }); + chart.col('value', { + type: 'linear', + nice: true, + min: 0, + max: 100, + tickCount: 6, + subTick: false, + }); + chart.axis('value', { + tickLine: { + stroke: color, + }, + labelOffset: -12, + formatter(val) { + switch (val * 1) { + case 20: + return '差'; + case 40: + return '中'; + case 60: + return '良'; + case 80: + return '优'; + default: + return ''; + } + }, + }); + chart.point().position('value').shape('dashBoard'); + draw(data); + + /* eslint no-shadow: 0 */ + function draw(data) { + const val = data[0].value; + const lineWidth = 18; + chart.guide().clear(); + + chart.guide().arc(() => { + return [0, 0.95]; + }, () => { + return [val, 0.95]; + }, { + stroke: color, + lineWidth, + }); + + chart.guide().arc(() => { + return [val, 0.95]; + }, (arg) => { + return [arg.max, 0.95]; + }, { + stroke: bgColor, + lineWidth, + }); + + chart.changeData(data); + } + + this.chart = chart; + } + + render() { + return ( +
    + ); + } +} + +export default Gauge; diff --git a/src/components/Charts/Icon/index.js b/src/components/Charts/Icon/index.js new file mode 100644 index 00000000..d03a3e38 --- /dev/null +++ b/src/components/Charts/Icon/index.js @@ -0,0 +1,29 @@ +import React from 'react'; +import { Icon } from 'antd'; + +const IconUp = ({ color }) => ( + +); + +const IconDown = ({ color }) => ( + +); + +export default { + IconUp, + IconDown, +}; diff --git a/src/components/Charts/MiniArea/index.js b/src/components/Charts/MiniArea/index.js new file mode 100644 index 00000000..5807c352 --- /dev/null +++ b/src/components/Charts/MiniArea/index.js @@ -0,0 +1,95 @@ +import React, { PureComponent } from 'react'; +import G2 from 'g2'; +import styles from '../index.less'; + +class MiniArea extends PureComponent { + componentDidMount() { + this.renderChart(this.props.data); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.data !== this.props.data) { + this.renderChart(nextProps.data); + } + } + + handleRef = (n) => { + this.node = n; + } + + renderChart(data) { + const { height = 0, fit = true, color = '#33abfb', line, xAxis, yAxis } = this.props; + + if (!data || (data && data.length < 1)) { + return; + } + + // clean + this.node.innerHTML = ''; + + const chart = new G2.Chart({ + container: this.node, + forceFit: fit, + height: height + 54, + plotCfg: { + margin: [36, 0, 30, 0], + }, + legend: null, + }); + + if (!xAxis && !yAxis) { + chart.axis(false); + } + + if (xAxis) { + chart.axis('x', xAxis); + } else { + chart.axis('x', false); + } + + if (yAxis) { + chart.axis('y', yAxis); + } else { + chart.axis('y', false); + } + + chart.source(data, { + x: { + type: 'cat', + range: [0, 1], + ...xAxis, + }, + y: { + min: 0, + ...yAxis, + }, + }); + + chart.tooltip({ + title: null, + crosshairs: false, + map: { + name: 'x', + }, + }); + chart.area().position('x*y').color(color).shape('smooth'); + if (line) { + chart.line().position('x*y').color(color).shape('smooth'); + } + chart.render(); + } + + render() { + const { height } = this.props; + + return ( +
    +
    +
    +
    +
    + ); + } +} + +export default MiniArea; diff --git a/src/components/Charts/MiniBar/index.js b/src/components/Charts/MiniBar/index.js new file mode 100644 index 00000000..d0553a7f --- /dev/null +++ b/src/components/Charts/MiniBar/index.js @@ -0,0 +1,78 @@ +import React, { PureComponent } from 'react'; +import G2 from 'g2'; +import styles from '../index.less'; + +class MiniBar extends PureComponent { + componentDidMount() { + this.renderChart(this.props.data); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.data !== this.props.data) { + this.renderChart(nextProps.data); + } + } + + handleRef = (n) => { + this.node = n; + } + + renderChart(data) { + const { height = 0, fit = true, color = '#33ABFB' } = this.props; + + if (!data || (data && data.length < 1)) { + return; + } + + // clean + this.node.innerHTML = ''; + + const Frame = G2.Frame; + const frame = new Frame(data); + + const chart = new G2.Chart({ + container: this.node, + forceFit: fit, + height: height + 54, + plotCfg: { + margin: [36, 0, 30, 0], + }, + legend: null, + }); + + chart.axis(false); + + chart.source(frame, { + x: { + type: 'cat', + }, + y: { + min: 0, + }, + }); + + chart.tooltip({ + title: null, + crosshairs: false, + map: { + name: 'x', + }, + }); + chart.interval().position('x*y').color(color); + chart.render(); + } + + render() { + const { height } = this.props; + + return ( +
    +
    +
    +
    +
    + ); + } +} + +export default MiniBar; diff --git a/src/components/Charts/MiniProgress/index.js b/src/components/Charts/MiniProgress/index.js new file mode 100644 index 00000000..63037a1c --- /dev/null +++ b/src/components/Charts/MiniProgress/index.js @@ -0,0 +1,27 @@ +import React from 'react'; + +import styles from './index.less'; + +const MiniProgress = ({ target, color, 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 00000000..94ed47a0 --- /dev/null +++ b/src/components/Charts/MiniProgress/index.less @@ -0,0 +1,37 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../../utils/utils.less"; + +.miniProgress { + padding: 5px 0; + position: relative; + width: 100%; + .progressWrap { + background-color: @background-color-base; + position: relative; + } + .progress { + transition: all .4s cubic-bezier(.08, .82, .17, 1) 0s; + border-radius: 1px 0 0 1px; + background-color: @primary-color; + width: 0; + height: 100%; + } + .target { + position: absolute; + top: 0; + bottom: 0; + span { + border-radius: 100px; + position: absolute; + top: 0; + left: 0; + height: 4px; + width: 2px; + } + span:last-child { + top: auto; + bottom: 0; + } + } +} + diff --git a/src/components/Charts/NumberInfo/index.js b/src/components/Charts/NumberInfo/index.js new file mode 100644 index 00000000..4971f998 --- /dev/null +++ b/src/components/Charts/NumberInfo/index.js @@ -0,0 +1,32 @@ +import React from 'react'; +import { Icon } from 'antd'; +import classNames from 'classnames'; + +import styles from './index.less'; + +export default ({ theme, title, subTitle, total, subTotal, status, ...rest }) => ( +
    + { + title &&

    {title}

    + } +
    {subTitle}
    +
    + {total} + { + (status || subTotal) && + { + status && + } + {subTotal} + + } +
    +
    +); diff --git a/src/components/Charts/NumberInfo/index.less b/src/components/Charts/NumberInfo/index.less new file mode 100644 index 00000000..c1dfcba8 --- /dev/null +++ b/src/components/Charts/NumberInfo/index.less @@ -0,0 +1,46 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../../utils/utils.less"; + +.numberInfo { + h4 { + color: @heading-color; + margin-bottom: 16px; + } + h6 { + color: @text-color-secondary; + font-size: @font-size-base; + height: 22px; + line-height: 22px; + .textOverflow(); + } + & > div { + margin-top: 8px; + font-size: 0; + .textOverflow(); + & > span { + color: @heading-color; + display: inline-block; + line-height: 32px; + height: 32px; + font-size: 24px; + margin-right: 32px; + } + .subTotal { + color: @text-color-secondary; + font-size: @font-size-base; + vertical-align: top; + i { + font-size: 12px; + transform: scale(0.82); + margin-right: 4px; + } + } + } +} +.numberInfolight { + & > div { + & > span { + color: @text-color; + } + } +} diff --git a/src/components/Charts/Pie/index.js b/src/components/Charts/Pie/index.js new file mode 100644 index 00000000..7228268a --- /dev/null +++ b/src/components/Charts/Pie/index.js @@ -0,0 +1,224 @@ +import React, { Component } from 'react'; +import G2 from 'g2'; +import styles from './index.less'; + +/* eslint react/no-danger:0 */ +class Pie extends Component { + state = { + legendData: [], + left: undefined, + } + + componentDidMount() { + this.renderChart(this.props.data); + } + + componentWillReceiveProps(nextProps) { + this.renderChart(nextProps.data); + } + + handleRef = (n) => { + this.node = n; + } + handleTotalRef = (n) => { + this.totalNode = n; + } + + + handleLegendClick = (item, i) => { + const newItem = item; + newItem.checked = !newItem.checked; + + const legendData = this.state.legendData; + legendData[i] = newItem; + + if (this.chart) { + const filterItem = legendData.filter(l => l.checked).map(l => l.x); + this.chart.filter('x', filterItem); + this.chart.repaint(); + } + + this.setState({ + legendData, + }); + } + + renderChart(data) { + const { + title, height = 0, + hasLegend, fit = true, + margin, percent, color, + inner = 0.75, + animate = true, + } = this.props; + + let selected = this.props.selected || true; + let tooltip = this.props.tooltips || true; + + let formatColor; + if (percent) { + selected = false; + tooltip = false; + formatColor = (value) => { + if (value === '占比') { + return color || '#0096fa'; + } else { + return '#e9e9e9'; + } + }; + + /* eslint no-param-reassign: */ + data = [ + { + x: '占比', + y: parseFloat(percent), + }, + { + x: '反比', + y: 100 - parseFloat(percent), + }, + ]; + } + + if (!data || (data && data.length < 1)) { + return; + } + + let m = margin; + if (!margin) { + if (hasLegend) { + m = [24, 240, 24, 0]; + } else if (percent) { + m = [0, 0, 0, 0]; + } else { + m = [24, 0, 24, 0]; + } + } + + const h = title ? (height + m[0] + m[2] + (-46)) : (height + m[0] + m[2]); + + // clean + this.node.innerHTML = ''; + + const Stat = G2.Stat; + + const chart = new G2.Chart({ + container: this.node, + forceFit: fit, + height: h, + plotCfg: { + margin: m, + }, + animate, + }); + + if (!tooltip) { + chart.tooltip(false); + } else { + chart.tooltip({ + title: null, + }); + } + + chart.axis(false); + chart.legend(false); + + chart.source(data, { + x: { + type: 'cat', + range: [0, 1], + }, + y: { + min: 0, + }, + }); + + chart.coord('theta', { + inner, + }); + + chart.intervalStack().position(Stat.summary.percent('y')).color('x', formatColor).selected(selected); + chart.render(); + + this.chart = chart; + + let legendData = []; + if (hasLegend) { + const geom = chart.getGeoms()[0]; // 获取所有的图形 + const items = geom.getData(); // 获取图形对应的数据 + legendData = items.map((item) => { + /* eslint no-underscore-dangle:0 */ + const origin = item._origin; + origin.color = item.color; + origin.checked = true; + return origin; + }); + } + + this.setState({ + legendData, + }, () => { + let left = 0; + if (this.totalNode) { + left = -((this.totalNode.offsetWidth / 2) + ((margin || m)[1] / 2)); + } + this.setState({ + left, + }); + }); + } + + render() { + const { height, title, valueFormat, subTitle, total, hasLegend } = this.props; + const { legendData, left } = this.state; + const mt = -(((legendData.length * 38) - 16) / 2); + + return ( +
    +
    + { title &&

    {title}

    } +
    +
    + { + (subTitle || total) &&
    + { + subTitle &&

    {subTitle}

    + } + { + // eslint-disable-next-line + total &&

    + } +

    + } + { + hasLegend &&
      + { + legendData.map((item, i) => ( +
    • this.handleLegendClick(item, i)}> + + {item.x} + + {`${(item['..percent'] * 100).toFixed(2)}%`} + +
    • + )) + } +
    + } +
    +
    +
    + ); + } +} + +export default Pie; diff --git a/src/components/Charts/Pie/index.less b/src/components/Charts/Pie/index.less new file mode 100644 index 00000000..37392d19 --- /dev/null +++ b/src/components/Charts/Pie/index.less @@ -0,0 +1,70 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../../utils/utils.less"; + +.pie { + .content { + position: relative; + } + .legend { + position: absolute; + top: 50%; + right: 0; + min-width: 200px; + li { + cursor: pointer; + margin-bottom: 16px; + height: 22px; + line-height: 22px; + } + } + .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; + margin-right: 8px; + } + .percent { + color: @text-color-secondary; + } + .value { + position: absolute; + right: 0; + } + .total { + opacity: 0; + position: absolute; + left: 50%; + top: 50%; + margin-top: -34px; + text-align: center; + height: 62px; + & > h4 { + color: @text-color-secondary; + font-size: 14px; + line-height: 22px; + height: 22px; + margin-bottom: 8px; + } + & > p { + color: @heading-color; + display: block; + font-size: 24px; + height: 32px; + line-height: 32px; + } + } +} diff --git a/src/components/Charts/Radar/index.js b/src/components/Charts/Radar/index.js new file mode 100644 index 00000000..1ac14375 --- /dev/null +++ b/src/components/Charts/Radar/index.js @@ -0,0 +1,155 @@ +import React, { PureComponent } from 'react'; +import G2 from 'g2'; +import { Row, Col } from 'antd'; +import styles from './index.less'; + +/* eslint react/no-danger:0 */ +class Radar extends PureComponent { + state = { + legendData: [], + } + + componentDidMount() { + this.renderChart(this.props.data); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.data !== this.props.data) { + this.renderChart(nextProps.data); + } + } + + handleRef = (n) => { + this.node = n; + } + + handleLegendClick = (item, i) => { + const newItem = item; + newItem.checked = !newItem.checked; + + const legendData = this.state.legendData; + legendData[i] = newItem; + + if (this.chart) { + const filterItem = legendData.filter(l => l.checked).map(l => l.name); + this.chart.filter('name', filterItem); + this.chart.repaint(); + } + + this.setState({ + legendData, + }); + } + + renderChart(data) { + const { height = 0, + hasLegend = true, + fit = true, + tickCount = 4, + margin = [16, 0, 16, 0] } = this.props; + + if (!data || (data && data.length < 1)) { + return; + } + + // clean + this.node.innerHTML = ''; + + const chart = new G2.Chart({ + container: this.node, + forceFit: fit, + height: height - 22, + plotCfg: { + margin, + }, + }); + + this.chart = chart; + + chart.source(data, { + value: { + min: 0, + tickCount, + }, + }); + + chart.coord('polar'); + chart.legend(false); + + chart.axis('label', { + line: null, + }); + + chart.axis('value', { + grid: { + type: 'polygon', + }, + }); + + chart.line().position('label*value').color('name'); + chart.point().position('label*value').color('name').shape('circle'); + + chart.render(); + + if (hasLegend) { + const geom = chart.getGeoms()[0]; // 获取所有的图形 + const items = geom.getData(); // 获取图形对应的数据 + const legendData = items.map((item) => { + /* eslint no-underscore-dangle:0 */ + const origin = item._origin; + const result = { + name: origin[0].name, + color: item.color, + checked: true, + value: origin.reduce((p, n) => p + n.value, 0), + }; + + return result; + }); + + this.setState({ + legendData, + }); + } + } + + render() { + const { height, title, hasLegend } = this.props; + const { legendData } = this.state; + + return ( +
    +
    + { title &&

    {title}

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

    + + {item.name} +

    +
    {item.value}
    + { + i !== (legendData.length - 1) &&
    + } +
    + + )) + } + + } +
    +
    + ); + } +} + +export default Radar; diff --git a/src/components/Charts/Radar/index.less b/src/components/Charts/Radar/index.less new file mode 100644 index 00000000..5e334680 --- /dev/null +++ b/src/components/Charts/Radar/index.less @@ -0,0 +1,38 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../../utils/utils.less"; + +.radar { + .legend { + margin-top: 16px; + .legendItem { + position: relative; + text-align: center; + p { + cursor: pointer; + } + h6 { + color: @heading-color; + font-size: 24px; + line-height: 32px; + margin-top: 2px; + } + .split { + background-color: @border-color-split; + position: absolute; + top: 8px; + right: 0; + height: 40px; + width: 1px; + } + } + .dot { + border-radius: 8px; + display: inline-block; + margin-right: 8px; + position: relative; + top: -1px; + height: 8px; + width: 8px; + } + } +} diff --git a/src/components/Charts/Trend/index.js b/src/components/Charts/Trend/index.js new file mode 100644 index 00000000..91c318b8 --- /dev/null +++ b/src/components/Charts/Trend/index.js @@ -0,0 +1,22 @@ +import React from 'react'; +import { Icon } from 'antd'; + +import styles from './index.less'; + +const Item = ({ title, flag, children, ...rest }) => ( +
    + {title} + { flag && } + {children} +
    +); + +const Trend = ({ colorType, children, ...rest }) => ( +
    + {children} +
    +); + +Trend.Item = Item; + +export default Trend; diff --git a/src/components/Charts/Trend/index.less b/src/components/Charts/Trend/index.less new file mode 100644 index 00000000..818fa9e6 --- /dev/null +++ b/src/components/Charts/Trend/index.less @@ -0,0 +1,49 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../../utils/utils.less"; + +.trend { + font-size: 0; + height: 22px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + .trendItem { + display: inline-block; + margin-right: 16px; + color: @text-color; + font-size: @font-size-base; + line-height: 22px; + height: 22px; + .title { + margin-right: 4px; + } + .value { + color: @text-color; + font-weight: 600; + } + .up, .down { + color: #00a854; + margin-right: 4px; + position: relative; + top: 1px; + i { + font-size: 12px; + transform: scale(0.83); + } + } + .down { + color: #f04134; + top: -1px; + } + } + .trendItem:last-child { + margin-right: 0; + } +} + +.trendgray { + .trend(); + .trendItem { + color: @text-color-secondary; + } +} diff --git a/src/components/Charts/WaterWave/index.js b/src/components/Charts/WaterWave/index.js new file mode 100644 index 00000000..f2dab658 --- /dev/null +++ b/src/components/Charts/WaterWave/index.js @@ -0,0 +1,189 @@ +import React, { PureComponent } from 'react'; +import styles from './index.less'; + +/* eslint no-return-assign: 0 */ +// riddle: https://riddle.alibaba-inc.com/riddles/2d9a4b90 + +class WaterWave extends PureComponent { + static defaultProps = { + height: 160, + } + state = { + radio: 1, + } + + componentDidMount() { + this.renderChart(); + this.resize(); + + window.addEventListener('resize', () => { + this.resize(); + }); + } + + resize() { + const { height } = this.props; + const realWidth = this.root.parentNode.offsetWidth; + if (realWidth < this.props.height) { + const radio = realWidth / height; + this.setState({ + radio, + }); + } else { + this.setState({ + radio: 1, + }); + } + } + + renderChart() { + const { percent, color = '#19AFFA' } = this.props; + const data = percent / 100; + + 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; + + 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]); + ctx.fillStyle = color; + 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 = '#108ee9'; + } + } 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(); + } + 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} height={height} /> +
    + { + title && {title} + } +

    {percent}%

    +
    +
    + ); + } +} + +export default WaterWave; diff --git a/src/components/Charts/WaterWave/index.less b/src/components/Charts/WaterWave/index.less new file mode 100644 index 00000000..5248ed07 --- /dev/null +++ b/src/components/Charts/WaterWave/index.less @@ -0,0 +1,25 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../../utils/utils.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; + } + } +} diff --git a/src/components/Charts/index.js b/src/components/Charts/index.js new file mode 100644 index 00000000..5967d6eb --- /dev/null +++ b/src/components/Charts/index.js @@ -0,0 +1,34 @@ +import numeral from 'numeral'; +import ChartCard from './ChartCard'; +import Bar from './Bar'; +import Pie from './Pie'; +import Radar from './Radar'; +import Gauge from './Gauge'; +import MiniArea from './MiniArea'; +import MiniBar from './MiniBar'; +import MiniProgress from './MiniProgress'; +import Trend from './Trend'; +import Field from './Field'; +import NumberInfo from './NumberInfo'; +import WaterWave from './WaterWave'; +import { IconUp, IconDown } from './Icon'; + +const yuan = val => `¥ ${numeral(val).format('0,0')}`; + +export default { + IconUp, + IconDown, + yuan, + Bar, + Pie, + Gauge, + Radar, + MiniBar, + MiniArea, + MiniProgress, + ChartCard, + Trend, + Field, + NumberInfo, + WaterWave, +}; diff --git a/src/components/Charts/index.less b/src/components/Charts/index.less new file mode 100644 index 00000000..cc4387d0 --- /dev/null +++ b/src/components/Charts/index.less @@ -0,0 +1,9 @@ +.miniChart { + position: relative; + width: 100%; + & > div { + position: absolute; + bottom: -34px; + width: 100%; + } +} diff --git a/src/components/Countdown/index.js b/src/components/Countdown/index.js new file mode 100644 index 00000000..6b364a75 --- /dev/null +++ b/src/components/Countdown/index.js @@ -0,0 +1,108 @@ +import React, { Component } from 'react'; + +function fixedZero(val) { + return val * 1 < 10 ? `0${val}` : val; +} + +class Countdown extends Component { + constructor(props) { + super(props); + + const { targetTime, lastTime } = this.initTime(props); + + this.state = { + targetTime, + lastTime, + }; + } + + componentDidMount() { + this.tick(); + } + + componentWillReceiveProps(nextProps) { + if (this.props.target !== nextProps.target) { + const { targetTime, lastTime } = this.initTime(nextProps); + this.setState({ + lastTime, + targetTime, + }); + } + } + + 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, + targetTime, + }; + } + // defaultFormat = time => ( + // {moment(time).format('hh:mm:ss')} + // ); + defaultFormat = (time) => { + const hours = 60 * 60 * 1000; + const minutes = 60 * 1000; + + const h = fixedZero(Math.floor(time / hours)); + const m = fixedZero(Math.floor((time - (h * hours)) / minutes)); + const s = fixedZero(Math.floor((time - (h * hours) - (m * minutes)) / 1000)); + return ( + {h}:{m}:{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 } = this.props; + const { lastTime } = this.state; + + const result = format(lastTime); + + return result; + } +} + +export default Countdown; diff --git a/src/components/DescriptionList/Description.js b/src/components/DescriptionList/Description.js new file mode 100644 index 00000000..2aae62b2 --- /dev/null +++ b/src/components/DescriptionList/Description.js @@ -0,0 +1,17 @@ +import React from 'react'; +import classNames from 'classnames'; +import { Col } from 'antd'; +import styles from './index.less'; +import responsive from './responsive'; + +const Description = ({ term, column, className, children, ...restProps }) => { + const clsString = classNames(styles.description, className); + return ( + + {term &&
    {term}
    } + {children &&
    {children}
    } + + ); +}; + +export default Description; diff --git a/src/components/DescriptionList/DescriptionList.js b/src/components/DescriptionList/DescriptionList.js new file mode 100644 index 00000000..25a5faf3 --- /dev/null +++ b/src/components/DescriptionList/DescriptionList.js @@ -0,0 +1,18 @@ +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, ...restProps }) => { + const clsString = classNames(styles.descriptionList, styles[layout], className); + 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 00000000..9e59c015 --- /dev/null +++ b/src/components/DescriptionList/demo/basic.md @@ -0,0 +1,35 @@ +--- +order: 0 +title: Basic +--- + +基本描述列表。 + +````jsx +import { DescriptionList } from 'ant-design-pro'; + +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 00000000..c45c9f87 --- /dev/null +++ b/src/components/DescriptionList/demo/vertical.md @@ -0,0 +1,35 @@ +--- +order: 1 +title: Vertical +--- + +垂直布局。 + +````jsx +import { DescriptionList } from 'ant-design-pro'; + +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.js b/src/components/DescriptionList/index.js new file mode 100644 index 00000000..357f479f --- /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 00000000..f7dc3203 --- /dev/null +++ b/src/components/DescriptionList/index.less @@ -0,0 +1,50 @@ +@import "~antd/lib/style/themes/default.less"; + +.descriptionList { + // offset the padding-bottom of last row + :global { + .ant-row { + margin-bottom: -16px; + overflow: hidden; + } + } + + .title { + color: @heading-color; + font-weight: 600; + margin-bottom: 16px; + } + + .term { + padding-bottom: 16px; + margin-right: 8px; + color: @heading-color; + white-space: nowrap; + display: table-cell; + + &:after { + content: ":"; + margin: 0 8px 0 2px; + position: relative; + top: -.5px; + } + } + + .detail { + padding-bottom: 16px; + color: @text-color; + display: table-cell; + } + + &.vertical { + + .term { + padding-bottom: 8px; + display: block; + } + + .detail { + display: block; + } + } +} diff --git a/src/components/DescriptionList/index.md b/src/components/DescriptionList/index.md new file mode 100644 index 00000000..7d63e2ad --- /dev/null +++ b/src/components/DescriptionList/index.md @@ -0,0 +1,29 @@ +--- +category: Components +type: General +title: DescriptionList +subtitle: 描述列表 +cols: 1 +--- + +描述列表用来展示一系列文本信息。 + +## API + +### DescriptionList + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| layout | 布局方式 | Enum{'horizontal', 'vertical'} | 'horizontal' | +| col | 指定信息分几列展示 | number(0 < col <= 4) | 3 | +| title | 列表标题 | ReactNode | - | +| gutter | 列表项间距,单位为 `px` | number | 32 | + +### DescriptionList.Description + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| term | 列表项标题 | ReactNode | - | + + + diff --git a/src/components/DescriptionList/responsive.js b/src/components/DescriptionList/responsive.js new file mode 100644 index 00000000..a5aa73f7 --- /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/EditableLinkGroup/index.js b/src/components/EditableLinkGroup/index.js new file mode 100644 index 00000000..68c0c42f --- /dev/null +++ b/src/components/EditableLinkGroup/index.js @@ -0,0 +1,46 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { Link } from 'dva/router'; +import { Button, Icon } from 'antd'; +import styles from './index.less'; + +// TODO: 添加逻辑 + +class EditableLinkGroup extends PureComponent { + static defaultProps = { + links: [], + onAdd: () => { + }, + } + state = { + links: this.props.links, + }; + + handleOnClick() { + const { onAdd } = this.props; + onAdd(); + } + + render() { + const { links } = this.state; + return ( +
    + { + links.map(link => {link.title}) + } + { + + } +
    + ); + } +} + +EditableLinkGroup.propTypes = { + links: PropTypes.array, + onAdd: PropTypes.func, +}; + +export default EditableLinkGroup; diff --git a/src/components/EditableLinkGroup/index.less b/src/components/EditableLinkGroup/index.less new file mode 100644 index 00000000..16a3e18a --- /dev/null +++ b/src/components/EditableLinkGroup/index.less @@ -0,0 +1,29 @@ +@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; + margin-right: 32px; + &:hover { + color: @primary-color; + } + } + & > button { + border-color: @primary-color; + color: @primary-color; + i { + position: relative; + top: -1px; + } + span { + margin-left: 0 !important; + position: relative; + top: -1px; + } + } +} diff --git a/src/components/Exception/demo/403.md b/src/components/Exception/demo/403.md new file mode 100644 index 00000000..9cafa41c --- /dev/null +++ b/src/components/Exception/demo/403.md @@ -0,0 +1,21 @@ +--- +order: 2 +title: 403 +--- + +403 页面,配合自定义操作。 + +````jsx +import { Exception } from 'ant-design-pro'; +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 00000000..cd693592 --- /dev/null +++ b/src/components/Exception/demo/404.md @@ -0,0 +1,14 @@ +--- +order: 0 +title: 404 +--- + +404 页面。 + +````jsx +import { Exception } from 'ant-design-pro'; + +ReactDOM.render( + +, mountNode); +```` diff --git a/src/components/Exception/demo/500.md b/src/components/Exception/demo/500.md new file mode 100644 index 00000000..b94e10ef --- /dev/null +++ b/src/components/Exception/demo/500.md @@ -0,0 +1,14 @@ +--- +order: 1 +title: 500 +--- + +500 页面。 + +````jsx +import { Exception } from 'ant-design-pro'; + +ReactDOM.render( + +, mountNode); +```` diff --git a/src/components/Exception/index.js b/src/components/Exception/index.js new file mode 100644 index 00000000..39f903f8 --- /dev/null +++ b/src/components/Exception/index.js @@ -0,0 +1,26 @@ +import React from 'react'; +import classNames from 'classnames'; +import { Button } from 'antd'; +import { Link } from 'react-router'; +import config from './typeConfig'; +import styles from './index.less'; + + +export default ({ className, type, title, desc, img, actions }) => { + const pageType = type in config ? type : '404'; + const clsString = classNames(styles.exception, className); + return ( +
    +
    + +
    +
    +

    {title || config[pageType].title}

    +
    {desc || config[pageType].desc}
    +
    + {actions || } +
    +
    +
    + ); +}; diff --git a/src/components/Exception/index.less b/src/components/Exception/index.less new file mode 100644 index 00000000..37b639f9 --- /dev/null +++ b/src/components/Exception/index.less @@ -0,0 +1,37 @@ +@import "~antd/lib/style/themes/default.less"; + +.exception { + display: flex; + align-items: center; + height: 100%; + + .imgBlock { + flex: 0 0 62.5%; + width: 62.5%; + text-align: right; + padding-right: 152px; + } + + .content { + flex: auto; + + h1 { + color: @text-color; + font-size: 68px; + line-height: 68px; + margin-bottom: 16px; + } + + .desc { + color: @text-color-secondary; + font-size: 20px; + margin-bottom: 16px; + } + + .actions { + button:not(:last-child) { + margin-right: 8px; + } + } + } +} diff --git a/src/components/Exception/index.md b/src/components/Exception/index.md new file mode 100644 index 00000000..9ac52653 --- /dev/null +++ b/src/components/Exception/index.md @@ -0,0 +1,19 @@ +--- +category: Components +type: General +title: Exception +subtitle: 异常 +cols: 1 +--- + +异常页用于对页面特定的异常状态进行反馈。通常,它包含对错误状态的阐述,并向用户提供建议或操作,避免用户感到迷失和困惑。 + +## API + +| 参数 | 说明 | 类型 | 默认值 | +|-------------|------------------------------------------|-------------|-------| +| type | 页面类型,若配置,则自带对应类型默认的 `title`,`desc`,`img`,此默认设置可以被 `title`,`desc`,`img` 覆盖 | Enum {'403', '404', '500'} | - | +| title | 标题 | ReactNode | - | +| desc | 补充描述 | ReactNode | - | +| img | 背景图片地址 | string | - | +| actions | 建议操作,配置此属性时默认的『返回首页』按钮不生效 | ReactNode | - | diff --git a/src/components/Exception/typeConfig.js b/src/components/Exception/typeConfig.js new file mode 100644 index 00000000..a1f018c8 --- /dev/null +++ b/src/components/Exception/typeConfig.js @@ -0,0 +1,19 @@ +const config = { + 403: { + img: 'https://gw.alipayobjects.com/zos/rmsportal/byTGXmzwJVwgotvxHQsU.svg', + title: '403', + desc: '对不起,你没有权限', + }, + 404: { + img: 'https://gw.alipayobjects.com/zos/rmsportal/GdXXOjtMMzaPfCziUVYt.svg', + title: '404', + desc: '你要找的页面不存在', + }, + 500: { + img: 'https://gw.alipayobjects.com/zos/rmsportal/OpTUNDbQGfEWLubSrJap.svg', + title: '500', + desc: '服务器错误,我们正在维修', + }, +}; + +export default config; diff --git a/src/components/FooterToolbar/index.js b/src/components/FooterToolbar/index.js new file mode 100644 index 00000000..81a8257f --- /dev/null +++ b/src/components/FooterToolbar/index.js @@ -0,0 +1,44 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import styles from './index.less'; + +export default class FooterToolbar extends Component { + static contextTypes = { + layoutCollapsed: PropTypes.bool, + }; + state = { + width: '', + }; + componentDidMount() { + this.syncWidth(); + } + componentWillReceiveProps() { + this.syncWidth(); + } + syncWidth() { + const sider = document.querySelectorAll('.ant-layout-sider')[0]; + if (sider) { + this.setState({ + width: `calc(100% - ${sider.style.width})`, + }); + } + } + render() { + const { children, style, 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 00000000..b544c1a2 --- /dev/null +++ b/src/components/FooterToolbar/index.less @@ -0,0 +1,32 @@ +@import "~antd/lib/style/themes/default.less"; + +.toolbar { + position: fixed; + width: 100%; + bottom: 0; + right: 0; + height: 56px; + line-height: 56px; + box-shadow: @shadow-1-up; + background: #fff; + padding: 0 28px; + transition: all .3s; + + &:after { + content: ""; + display: block; + clear: both; + } + + .left { + float: left; + } + + .right { + float: right; + } + + button + button { + margin-left: 8px; + } +} diff --git a/src/components/FooterToolbar/index.md b/src/components/FooterToolbar/index.md new file mode 100644 index 00000000..b202bd1e --- /dev/null +++ b/src/components/FooterToolbar/index.md @@ -0,0 +1,9 @@ +--- +category: Components +type: General +title: FooterToolbar +subtitle: 底部固定工具栏 +cols: 1 +--- + +## API diff --git a/src/components/GlobalFooter/demo/basic.md b/src/components/GlobalFooter/demo/basic.md new file mode 100644 index 00000000..2f0d82fe --- /dev/null +++ b/src/components/GlobalFooter/demo/basic.md @@ -0,0 +1,29 @@ +--- +order: 0 +title: Basic +--- + +基本页脚。 + +````jsx +import { GlobalFooter } from 'ant-design-pro'; +import { Icon } from 'antd'; + +const links = [{ + title: '帮助', + href: '', +}, { + title: '隐私', + href: '', +}, { + title: '条款', + href: '', + blankTarget: true, +}]; + +const copyright =
    Copyright 2017 蚂蚁金服体验技术部出品
    ; + +ReactDOM.render( + +, mountNode); +```` diff --git a/src/components/GlobalFooter/index.js b/src/components/GlobalFooter/index.js new file mode 100644 index 00000000..ed668319 --- /dev/null +++ b/src/components/GlobalFooter/index.js @@ -0,0 +1,18 @@ +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 00000000..90461690 --- /dev/null +++ b/src/components/GlobalFooter/index.less @@ -0,0 +1,23 @@ +@import "~antd/lib/style/themes/default.less"; + +.globalFooter { + padding: 32px 28px 16px; + text-align: center; + + .links { + margin-bottom: 8px; + + a { + color: @text-color-secondary; + + &:not(:last-child) { + margin-right: 40px; + } + } + } + + .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 00000000..6f3e43a1 --- /dev/null +++ b/src/components/GlobalFooter/index.md @@ -0,0 +1,16 @@ +--- +category: Components +type: General +title: GlobalFooter +subtitle: 全局页脚 +cols: 1 +--- + +页脚属于全局导航的一部分,作为对顶部导航的补充,通过传递数据控制展示内容。 + +## API + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| links | 链接数据 | array<{ title: ReactNode, href: string, blankTarget?: boolean }> | - | +| copyright | 版权信息 | ReactNode | - | diff --git a/src/components/HeaderSearch/index.js b/src/components/HeaderSearch/index.js new file mode 100644 index 00000000..de6c44fd --- /dev/null +++ b/src/components/HeaderSearch/index.js @@ -0,0 +1,65 @@ +import React, { PureComponent } from 'react'; +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, + }; + state = { + searchMode: false, + value: '', + }; + componentWillUnmount() { + clearTimeout(this.timeout); + } + onKeyDown = (e) => { + if (e.key === 'Enter') { + this.timeout = setTimeout(() => { + this.props.onPressEnter(this.state.value); // Fix duplicate onPressEnter + }, 0); + } + } + onChange = (value) => { + this.setState({ value }); + } + enterSearchMode = () => { + this.setState({ searchMode: true }, () => { + if (this.state.searchMode) { + this.input.refs.input.focus(); + } + }); + } + leaveSearchMode = () => { + this.setState({ + searchMode: false, + value: '', + }); + } + render() { + const { className, placeholder, ...restProps } = this.props; + const inputClass = classNames(styles.input, { + [styles.show]: this.state.searchMode, + }); + return ( + + + + { 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 00000000..11538746 --- /dev/null +++ b/src/components/HeaderSearch/index.less @@ -0,0 +1,27 @@ +.input { + transition: all .3s; + width: 0; + background: transparent; + border-radius: 0; + :global(.ant-select-selection) { + background: transparent; + } + input { + border: 0; + padding-left: 0; + padding-right: 0; + color: #fff; + &::placeholder { + color: rgba(255, 255, 255, .5); + } + } + &, + &:hover, + &:focus { + border-bottom: 1px solid #fff; + } + &.show { + width: 210px; + margin-left: 8px; + } +} diff --git a/src/components/MapChart/index.js b/src/components/MapChart/index.js new file mode 100644 index 00000000..190724ec --- /dev/null +++ b/src/components/MapChart/index.js @@ -0,0 +1,32 @@ +import React, { Component } from 'react'; +import { Tooltip } from 'antd'; + +import styles from './index.less'; + +/* eslint no-return-assign: 0 */ +class MapChart extends Component { + getRect() { + // 0.4657 = 708 / 1520 (img origin size) + const width = this.root.offsetWidth; + const height = width * 0.4657; + return { + width, + height, + }; + } + + render() { + return ( +
    (this.root = n)}> + +
    (this.root = n)}> + map +
    (this.node = n)} /> +
    + +
    + ); + } +} + +export default MapChart; diff --git a/src/components/MapChart/index.less b/src/components/MapChart/index.less new file mode 100644 index 00000000..11bb7cb5 --- /dev/null +++ b/src/components/MapChart/index.less @@ -0,0 +1,10 @@ +.mapChart { + background-color: #fff; + position: relative; + .canvas { + width: 100%; + & > img { + width: 100%; + } + } +} diff --git a/src/components/NoticeIcon/NoticeList.js b/src/components/NoticeIcon/NoticeList.js new file mode 100644 index 00000000..1d09b3ed --- /dev/null +++ b/src/components/NoticeIcon/NoticeList.js @@ -0,0 +1,44 @@ +import React from 'react'; +import { Avatar, Icon } from 'antd'; +import classNames from 'classnames'; +import styles from './NoticeList.less'; + +export default function NoticeList({ data = [], onClick, onClear, title, locale }) { + if (data.length === 0) { + return ( +
    + + {locale.emptyText} +
    + ); + } + return ( +
    +
      + {data.map((item, i) => { + const itemCls = classNames(styles.item, { + [styles.read]: item.read, + }); + return ( +
    • onClick(item)}> +
      + {item.avatar ? : null} +
      +

      {item.title}

      +
      + {item.description} +
      +
      {item.datetime}
      +
      {item.extra}
      +
      +
      +
    • + ); + })} +
    +
    + {locale.clear}{title} +
    +
    + ); +} diff --git a/src/components/NoticeIcon/NoticeList.less b/src/components/NoticeIcon/NoticeList.less new file mode 100644 index 00000000..1a7b0452 --- /dev/null +++ b/src/components/NoticeIcon/NoticeList.less @@ -0,0 +1,89 @@ +@import "~antd/lib/style/themes/default.less"; + +.list { + max-height: 400px; + overflow: auto; + .item { + transition: all .3s; + overflow: hidden; + cursor: pointer; + + .wrapper { + margin: 0 32px; + padding: 12px 0; + border-bottom: 1px solid @border-color-split; + } + &.read { + opacity: .4; + } + &:last-child .wrapper { + border-bottom: 0; + } + &:hover { + background: @primary-1; + } + .content { + position: relative; + overflow: hidden; + } + .avatar { + margin-right: 16px; + float: left; + margin-top: 4px; + background: #fff; + } + .title { + font-weight: normal; + color: @text-color; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .description { + color: @text-color-secondary; + font-size: 12px; + margin-top: 8px; + } + .datetime { + color: @text-color-secondary; + font-size: 12px; + margin-top: 4px; + } + .extra { + position: absolute; + right: 0; + top: 0; + color: @text-color-secondary; + font-size: 12px; + } + } +} + +.notFound { + text-align: center; + height: 120px; + line-height: 120px; + font-size: 14px; + color: @text-color-secondary; + > i { + font-size: 16px; + margin-right: 8px; + vertical-align: middle; + margin-top: -1px; + } +} + +.clear { + height: 46px; + line-height: 46px; + text-align: center; + color: @text-color-secondary; + border-radius: 0 0 @border-radius-base @border-radius-base; + border-top: 1px solid @border-color-split; + transition: all .3s; + cursor: pointer; + + &:hover { + color: @text-color; + } +} diff --git a/src/components/NoticeIcon/demo/basic.md b/src/components/NoticeIcon/demo/basic.md new file mode 100644 index 00000000..c61a91f7 --- /dev/null +++ b/src/components/NoticeIcon/demo/basic.md @@ -0,0 +1,12 @@ +--- +order: 1 +title: 通知图标 +--- + +通常用在全局导航上。 + +````jsx +import { NoticeIcon } from 'ant-design-pro'; + +ReactDOM.render(, mountNode); +```` diff --git a/src/components/NoticeIcon/demo/popover.md b/src/components/NoticeIcon/demo/popover.md new file mode 100644 index 00000000..071b2eac --- /dev/null +++ b/src/components/NoticeIcon/demo/popover.md @@ -0,0 +1,41 @@ +--- +order: 2 +title: 带浮层卡片 +--- + +点击展开通知卡片,展现多种类型的通知。 + +````jsx +import { NoticeIcon } from 'ant-design-pro'; +import moment from 'moment'; + +const data = [{ + key: '1', + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', + title: '曲丽丽 评论了你', + description: '描述信息描述信息描述信息', + datetime: moment('2017-08-07').fromNow(), +}, { + key: '2', + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', + title: '朱偏右 回复了你', + description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像', + datetime: moment('2017-08-07').fromNow(), +}, { + key: '3', + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', + title: '标题', + description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像', + datetime: moment('2017-08-07').fromNow(), +}]; + +ReactDOM.render( +
    + + + + + +
    +, mountNode); +```` diff --git a/src/components/NoticeIcon/index.js b/src/components/NoticeIcon/index.js new file mode 100644 index 00000000..63b2f8d5 --- /dev/null +++ b/src/components/NoticeIcon/index.js @@ -0,0 +1,93 @@ +import React, { PureComponent } from 'react'; +import { Popover, Icon, Tabs, Badge, Spin } from 'antd'; +import classNames from 'classnames'; +import List from './NoticeList'; +import styles from './index.less'; + +const { TabPane } = Tabs; + +export default class NoticeIcon extends PureComponent { + static defaultProps = { + onItemClick: () => {}, + onPopupVisibleChange: () => {}, + onTabChange: () => {}, + onClear: () => {}, + loading: false, + locale: { + emptyText: '暂无数据', + clear: '清空', + }, + }; + static Tab = TabPane; + constructor(props) { + super(props); + this.state = {}; + if (props.children && props.children[0]) { + this.state.tabType = props.children[0].props.title; + } + } + onItemClick = (item, tabProps) => { + const { onItemClick } = this.props; + onItemClick(item, tabProps); + } + onTabChange = (tabType) => { + this.setState({ tabType }); + this.props.onTabChange(tabType); + } + getNotificationBox() { + const { children, loading, locale } = this.props; + if (!children) { + return null; + } + const panes = children.map((child) => { + const title = child.props.list && child.props.list.length > 0 + ? `${child.props.title} (${child.props.list.length})` : child.props.title; + return ( + + this.onItemClick(item, child.props)} + onClear={() => this.props.onClear(child.props.title)} + title={child.props.title} + locale={locale} + /> + + ); + }); + return ( + + + {panes} + + + ); + } + render() { + const { className, count, popupAlign } = this.props; + const noticeButtonClass = classNames(className, styles.noticeButton); + const notificationBox = this.getNotificationBox(); + const trigger = ( + + + + + + ); + if (!notificationBox) { + return trigger; + } + return ( + + {trigger} + + ); + } +} diff --git a/src/components/NoticeIcon/index.less b/src/components/NoticeIcon/index.less new file mode 100644 index 00000000..5e2ff1b3 --- /dev/null +++ b/src/components/NoticeIcon/index.less @@ -0,0 +1,36 @@ +@import "~antd/lib/style/themes/default.less"; + +.popover { + width: 336px; + :global(.ant-popover-inner-content) { + padding: 0; + } +} + +.noticeButton { + cursor: pointer; + display: inline-block; + transition: all .3s; +} + +.icon { + font-size: 20px; +} + +.tabs { + :global { + .ant-tabs-nav-container { + font-size: 14px; + } + .ant-tabs-nav-scroll { + text-align: center; + } + .ant-tabs-bar { + margin-bottom: 0; + } + .ant-tabs-nav .ant-tabs-tab { + padding-top: 16px; + padding-bottom: 16px; + } + } +} diff --git a/src/components/NoticeIcon/index.md b/src/components/NoticeIcon/index.md new file mode 100644 index 00000000..f0b1b120 --- /dev/null +++ b/src/components/NoticeIcon/index.md @@ -0,0 +1,39 @@ +--- +category: Components +type: General +title: NoticeIcon +subtitle: 通知菜单 +cols: 1 +--- + +用在顶部导航上,作为整个产品统一的通知中心。 + +## API + +参数 | 说明 | 类型 | 默认值 +----|------|-----|------ +count | 图标上的消息总数 | number | - +loading | 弹出卡片加载状态 | boolean | false +onClear | 点击清空按钮的回调 | function(tabTitle) | - +onItemClick | 点击列表项的回调 | function(item, tabProps) | - +onTabChange | 切换页签的回调 | function(tabTitle) | - +popupAlign | 弹出卡片的位置配置 | Object [alignConfig](https://github.com/yiminghe/dom-align#alignconfig-object-details) | - +onPopupVisibleChange | 弹出卡片显隐的回调 | function(visible) | - +locale | 默认文案 | Object | `{ emptyText: '暂无数据', clear: '清空' }` + +### NoticeIcon.Tab + +参数 | 说明 | 类型 | 默认值 +----|------|-----|------ +title | 消息分类的页签标题 | string | - +data | 列表数据,格式参照下表 | Array | `[]` + +### Tab data + +参数 | 说明 | 类型 | 默认值 +----|------|-----|------ +avatar | 头像图片链接 | string | - +title | 标题 | ReactNode | - +description | 描述信息 | ReactNode | - +datetime | 时间戳 | ReactNode | - +extra | 额外信息,在列表项右上角 | ReactNode | - diff --git a/src/components/PageHeader/demo/image.md b/src/components/PageHeader/demo/image.md new file mode 100644 index 00000000..b5d0dc95 --- /dev/null +++ b/src/components/PageHeader/demo/image.md @@ -0,0 +1,71 @@ +--- +order: 2 +title: With Image +--- + +带图片的页头。 + +````jsx +import { PageHeader } from 'ant-design-pro'; + +const content = ( +
    +

    段落示意:蚂蚁金服务设计平台-design.alipay.com,用最小的工作量,无缝接入蚂蚁金服生态, +提供跨越设计与开发的体验解决方案。

    + +
    +); + +const extra = ( +
    + +
    +); + +const breadcrumbList = [{ + title: '一级菜单', + href: '/', +}, { + title: '二级菜单', + href: '/', +}, { + title: '三级菜单', +}]; + +ReactDOM.render( +
    + +
    +, mountNode); +```` + + diff --git a/src/components/PageHeader/demo/simple.md b/src/components/PageHeader/demo/simple.md new file mode 100644 index 00000000..bff44117 --- /dev/null +++ b/src/components/PageHeader/demo/simple.md @@ -0,0 +1,26 @@ +--- +order: 3 +title: Simple +--- + +简单的页头。 + +````jsx +import { PageHeader } from 'ant-design-pro'; + +const breadcrumbList = [{ + title: '一级菜单', + href: '/', +}, { + title: '二级菜单', + href: '/', +}, { + title: '三级菜单', +}]; + +ReactDOM.render( +
    + +
    +, mountNode); +```` diff --git a/src/components/PageHeader/demo/standard.md b/src/components/PageHeader/demo/standard.md new file mode 100644 index 00000000..e870e986 --- /dev/null +++ b/src/components/PageHeader/demo/standard.md @@ -0,0 +1,81 @@ +--- +order: 1 +title: Standard +--- + +标准页头。 + +````jsx +import { PageHeader } from 'ant-design-pro'; +import { Button, Menu, Dropdown, Icon, Row, Col } from 'antd'; + +const menu = ( + + 选项一 + 选项二 + 选项三 + +); + +const action = ( +
    + + + + + +
    +); + +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="DescriptionList 占位" + extraContent={extra} + breadcrumbList={breadcrumbList} + tabList={tabList} + onTabChange={onTabChange} + /> +
    +, mountNode); +```` diff --git a/src/components/PageHeader/demo/structure.md b/src/components/PageHeader/demo/structure.md new file mode 100644 index 00000000..0c7f81e6 --- /dev/null +++ b/src/components/PageHeader/demo/structure.md @@ -0,0 +1,67 @@ +--- +order: 0 +title: Structure +--- + +基本结构,可以形成多种组合。 + +````jsx +import { PageHeader } from 'ant-design-pro'; + +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} + /> +
    +, mountNode); +```` + + diff --git a/src/components/PageHeader/index.js b/src/components/PageHeader/index.js new file mode 100644 index 00000000..364b5562 --- /dev/null +++ b/src/components/PageHeader/index.js @@ -0,0 +1,98 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { Breadcrumb, Tabs } from 'antd'; +import { Link } from 'dva/router'; +import classNames from 'classnames'; +import styles from './index.less'; + +const TabPane = Tabs.TabPane; + +function itemRender(route, params, routes, paths) { + const last = routes.indexOf(route) === routes.length - 1; + return (last || !route.component) + ? {route.breadcrumbName} + : {route.breadcrumbName}; +} + +export default class PageHeader extends PureComponent { + static contextTypes = { + routes: PropTypes.array, + params: 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, + }; + }; + render() { + const { routes, params } = this.getBreadcrumbProps(); + const { title, logo, action, content, extraContent, + breadcrumbList, tabList, className } = this.props; + const clsString = classNames(styles.pageHeader, className); + let breadcrumb; + if (routes && params) { + breadcrumb = ( + route.breadcrumbName)} + params={params} + itemRender={itemRender} + /> + ); + } else if (breadcrumbList && breadcrumbList.length) { + breadcrumb = ( + + { + breadcrumbList.map(item => ( + + {item.href ? {item.title} : item.title} + ) + ) + } + + ); + } else { + breadcrumb = null; + } + + const tabDefaultValue = tabList && tabList.filter(item => item.default)[0]; + + 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 00000000..962479e5 --- /dev/null +++ b/src/components/PageHeader/index.less @@ -0,0 +1,95 @@ +@import "~antd/lib/style/themes/default.less"; + +.pageHeader { + background: @component-background; + padding: 18px 28px 0 36px; + border-bottom: @border-width-base @border-style-base @border-color-split; + + .detail { + display: flex; + } + + .row { + display: flex; + } + + .breadcrumb { + margin-bottom: 18px; + } + + .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; + } + + .title { + font-size: 20px; + font-weight: 500; + color: @heading-color; + } + + .action { + margin-left: 56px; + min-width: 266px; + + button:not(:last-child) { + margin-right: 8px; + } + } + + .title, .action, .content, .extraContent, .main { + flex: auto; + } + + .title, .action { + margin-bottom: 16px; + } + + .logo, .content, .extraContent { + margin-bottom: 12px; + } + + .action, .extraContent { + text-align: right; + } + + .extraContent { + margin-left: 88px; + min-width: 242px; + } +} + +@media screen and (max-width: @screen-md) { + .pageHeader { + .extraContent { + margin-left: 44px; + } + } +} + +@media screen and (max-width: @screen-sm) { + .pageHeader { + .extraContent { + margin-left: 24px; + } + } +} + +@media screen and (max-width: @screen-xs) { + .pageHeader { + .extraContent { + margin-left: 8px; + } + } +} diff --git a/src/components/PageHeader/index.md b/src/components/PageHeader/index.md new file mode 100644 index 00000000..4a42c68f --- /dev/null +++ b/src/components/PageHeader/index.md @@ -0,0 +1,26 @@ +--- +category: Components +type: General +title: PageHeader +subtitle: 页头 +cols: 1 +--- + +页头用来声明页面的主题,包含了用户所关注的最重要的信息,使用户可以快速理解当前页面是什么以及它的功能。 + +## API + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| title | title 区域 | ReactNode | - | +| logo | logo区域 | ReactNode | - | +| action | 操作区,位于 title 行的行尾 | ReactNode | - | +| content | 内容区 | ReactNode | - | +| extraContent | 额外内容区,位于content的右侧 | ReactNode | - | +| routes | 面包屑相关属性,router 的路由栈信息 | object[] | - | +| params | 面包屑相关属性,路由的参数 | object | - | +| breadcrumbList | 面包屑数据,配置了 `routes` `params` 时此属性无效 | array<{title: ReactNode, href?: string}> | - | +| tabList | tab 标题列表 | array<{key: string, tab: ReactNode}> | - | +| onTabChange | 切换面板的回调 | (key) => void | - | + +> 面包屑的配置方式有两种,一是结合 `react-router`,通过配置 `routes` 及 `params` 实现,类似 [面包屑 Demo](https://ant.design/components/breadcrumb-cn/#components-breadcrumb-demo-router);二是直接配置 `breadcrumbList`。 你也可以将 `routes` 及 `params` 放到 context 中,`PageHeader` 组件会自动获取。 diff --git a/src/components/RadioText/index.js b/src/components/RadioText/index.js new file mode 100644 index 00000000..6d62dcdc --- /dev/null +++ b/src/components/RadioText/index.js @@ -0,0 +1,10 @@ +import React from 'react'; +import { Radio } from 'antd'; + +import styles from './index.less'; + +const RadioButton = Radio.Button; + +export default props => (
    + +
    ); diff --git a/src/components/RadioText/index.less b/src/components/RadioText/index.less new file mode 100644 index 00000000..333cd16f --- /dev/null +++ b/src/components/RadioText/index.less @@ -0,0 +1,12 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../utils/utils.less"; + +.radioText { + display: inline; + :global { + .ant-radio-button-wrapper { + border: none; + padding: 0 12px; + } + } +} diff --git a/src/components/Result/demo/classic.md b/src/components/Result/demo/classic.md new file mode 100644 index 00000000..8fbcf185 --- /dev/null +++ b/src/components/Result/demo/classic.md @@ -0,0 +1,64 @@ +--- +order: 1 +title: Classic +--- + +典型结果页面。 + +````jsx +import { Result } from 'ant-design-pro'; +import { Button, Row, Col, Icon, Steps } from 'antd'; + +const Step = Steps.Step; + +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 00000000..1ab7ad8e --- /dev/null +++ b/src/components/Result/demo/error.md @@ -0,0 +1,39 @@ +--- +order: 2 +title: Failed +--- + +提交失败。 + +````jsx +import { Result } from 'ant-design-pro'; +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 00000000..13f2dab7 --- /dev/null +++ b/src/components/Result/demo/structure.md @@ -0,0 +1,20 @@ +--- +order: 0 +title: Structure +--- + +结构包含 `处理结果`,`补充信息` 以及 `操作建议` 三个部分,其中 `处理结果` 由 `提示图标`,`标题` 和 `结果描述` 组成。 + +````jsx +import { Result } from 'ant-design-pro'; + +ReactDOM.render( + 标题
    } + description={
    结果描述
    } + extra="其他补充信息,自带灰底效果" + actions={
    操作建议,一般放置按钮组
    } + /> +, mountNode); +```` diff --git a/src/components/Result/index.js b/src/components/Result/index.js new file mode 100644 index 00000000..4af5310f --- /dev/null +++ b/src/components/Result/index.js @@ -0,0 +1,21 @@ +import React from 'react'; +import classNames from 'classnames'; +import { Icon } from 'antd'; +import styles from './index.less'; + +export default ({ 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 00000000..4af83fa1 --- /dev/null +++ b/src/components/Result/index.less @@ -0,0 +1,45 @@ +@import "~antd/lib/style/themes/default.less"; + +.result { + text-align: center; + + .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; + color: @text-color-secondary; + margin-bottom: 24px; + } + + .extra { + background: rgba(245, 245, 245, 0.5); + padding: 24px 40px; + margin-bottom: 32px; + border-radius: @border-radius-sm; + text-align: left; + } + + .actions 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 00000000..8e01c7f8 --- /dev/null +++ b/src/components/Result/index.md @@ -0,0 +1,19 @@ +--- +category: Components +type: General +title: Result +subtitle: 处理结果 +cols: 1 +--- + +结果页用于对用户进行的一系列任务处理结果进行反馈。 + +## API + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| type | 类型,不同类型自带对应的图标 | Enum {'success', 'error'} | - | +| title | 标题 | ReactNode | - | +| description | 结果描述 | ReactNode | - | +| extra | 补充信息,有默认的灰色背景 | ReactNode | - | +| actions | 操作建议,推荐放置跳转链接,按钮组等 | ReactNode | - | diff --git a/src/components/SearchInput/index.js b/src/components/SearchInput/index.js new file mode 100644 index 00000000..e5e4403c --- /dev/null +++ b/src/components/SearchInput/index.js @@ -0,0 +1,15 @@ +import React from 'react'; +import { Button, Input } from 'antd'; + +import styles from './index.less'; + +export default ({ onSearch = () => ({}), text = '搜索', ...reset }) => ( +
    + {text}} + /> +
    +); diff --git a/src/components/SearchInput/index.less b/src/components/SearchInput/index.less new file mode 100644 index 00000000..7d00f448 --- /dev/null +++ b/src/components/SearchInput/index.less @@ -0,0 +1,45 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../utils/utils.less"; + +.search { + display: inline-block; + :global { + .ant-input-group-addon { + border: none; + padding: 0; + } + .ant-input-group .ant-input { + width: 522px; + } + } + input { + border-right: none; + height: 40px; + line-height: 40px; + } + button { + border-radius: 0 @border-radius-base @border-radius-base 0; + width: 86px; + height: 40px; + } +} + +@media screen and (max-width: @screen-sm) { + .search { + :global { + .ant-input-group .ant-input { + width: 300px; + } + } + } +} + +@media screen and (max-width: @screen-xs) { + .search { + :global { + .ant-input-group .ant-input { + width: 200px; + } + } + } +} diff --git a/src/components/StandardFormRow/index.js b/src/components/StandardFormRow/index.js new file mode 100644 index 00000000..27ecd79b --- /dev/null +++ b/src/components/StandardFormRow/index.js @@ -0,0 +1,24 @@ +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 00000000..9f11453d --- /dev/null +++ b/src/components/StandardFormRow/index.less @@ -0,0 +1,68 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../utils/utils.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: 16px; + } + } + .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 00000000..4d6be6e8 --- /dev/null +++ b/src/components/StandardTable/index.js @@ -0,0 +1,149 @@ +import React, { PureComponent } from 'react'; +import moment from 'moment'; +import { Table, Alert, Badge } from 'antd'; +import styles from './index.less'; + +class StandardTable extends PureComponent { + state = { + selectedRowKeys: [], + selectedRows: [], + totalCallNo: 0, + loading: false, + }; + + componentWillReceiveProps(nextProps) { + // clean state + if (nextProps.selectedRows.length === 0) { + this.setState({ + selectedRows: [], + selectedRowKeys: [], + totalCallNo: 0, + }); + } + } + + handleRowSelectChange = (selectedRowKeys, selectedRows) => { + const totalCallNo = selectedRows.reduce((sum, val) => { + return sum + parseFloat(val.callNo, 10); + }, 0); + + if (this.props.onSelectRow) { + this.props.onSelectRow(selectedRows); + } + + this.setState({ selectedRowKeys, selectedRows, totalCallNo }); + } + + handleTableChange = (pagination, filters, sorter) => { + this.props.onChange(pagination, filters, sorter); + } + + cleanSelectedKeys = () => { + this.handleRowSelectChange([], []); + } + + render() { + const { selectedRowKeys, totalCallNo } = this.state; + const { data: { list, pagination }, loading } = this.props; + + const status = ['关闭', '运行中']; + + const columns = [ + { + title: '规则编号', + dataIndex: 'no', + }, + { + title: '描述', + dataIndex: 'description', + }, + { + title: '服务调用次数', + dataIndex: 'callNo', + sorter: true, + render: val => ( +

    + {val} 万 +

    + ), + }, + { + title: '状态', + dataIndex: 'status', + filters: [ + { + text: status[0], + value: 0, + }, + { + text: status[1], + value: 1, + }, + ], + render(val) { + if (val === 0) { + return ; + } else { + return ; + } + }, + }, + { + title: '更新时间', + dataIndex: 'updatedAt', + sorter: true, + render: val => {moment(val).format('YYYY-MM-DD HH:mm:ss')}, + }, + { + title: '操作', + render: () => ( +

    + 配置 + + 订阅警报 +

    + ), + }, + ]; + + const paginationProps = { + showSizeChanger: true, + showQuickJumper: true, + ...pagination, + }; + + const rowSelection = { + selectedRowKeys, + onChange: this.handleRowSelectChange, + }; + + return ( +
    +
    + + 已选择 {selectedRowKeys.length} 项   + 服务调用总计 {totalCallNo} 万 + 清空 +

    + )} + type="info" + showIcon + /> +
    + record.key} + rowSelection={rowSelection} + dataSource={list} + columns={columns} + pagination={paginationProps} + onChange={this.handleTableChange} + /> + + ); + } +} + +export default StandardTable; diff --git a/src/components/StandardTable/index.less b/src/components/StandardTable/index.less new file mode 100644 index 00000000..34ec184c --- /dev/null +++ b/src/components/StandardTable/index.less @@ -0,0 +1,22 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../utils/utils.less"; + +.standardTable { + :global { + .ant-table-pagination { + margin-bottom: 0; + } + } + + .tableAlert { + margin-bottom: 16px; + } + + .splitLine { + background: @border-color-split; + display: inline-block; + margin: 0 8px; + width: 1px; + height: 12px; + } +} diff --git a/src/components/TagCloud/index.js b/src/components/TagCloud/index.js new file mode 100644 index 00000000..0ade0e71 --- /dev/null +++ b/src/components/TagCloud/index.js @@ -0,0 +1,134 @@ +import React, { PureComponent } from 'react'; +import G2 from 'g2'; +import Cloud from 'g-cloud'; + +/* eslint no-underscore-dangle: 0 */ +/* eslint no-param-reassign: 0 */ +/* eslint no-return-assign: 0 */ + +class TagCloud extends PureComponent { + componentDidMount() { + this.initTagCloud(); + this.renderChart(this.props.data); + } + + componentWillReceiveProps(nextProps) { + if (this.props.data !== nextProps.data) { + this.renderChart(nextProps.data); + } + } + + initTagCloud = () => { + const Util = G2.Util; + const Shape = G2.Shape; + + function getTextAttrs(cfg) { + const textAttrs = Util.mix(true, {}, { + fillOpacity: cfg.opacity, + fontSize: cfg.size, + rotate: 0, // cfg.origin._origin.rotate, + text: cfg.origin._origin.text, + textAlign: 'center', + fill: cfg.color, + textBaseline: 'Alphabetic', + }, cfg.style); + return textAttrs; + } + + // 给point注册一个词云的shape + Shape.registShape('point', 'cloud', { + drawShape(cfg, container) { + cfg.points = this.parsePoints(cfg.points); + const attrs = getTextAttrs(cfg); + const shape = container.addShape('text', { + attrs: Util.mix(attrs, { + x: cfg.points[0].x, + y: cfg.points[0].y, + }), + }); + return shape; + }, + }); + } + + renderChart(data) { + if (!data || data.length < 1) { + return; + } + + const { height } = this.props; + let width = 0; + if (this.root) { + width = this.root.offsetWidth; + } + + // clean + if (this.node) { + this.node.innerHTML = ''; + } + + data.sort((a, b) => b.value - a.value); + + const max = data[0].value; + const min = data[data.length - 1].value; + + // 构造一个词云布局对象 + const layout = new Cloud({ + words: data, + width, + height, + + // 设定文字大小配置函数(默认为12-40px的随机大小) + size: words => (((words.value - min) / (max - min)) * 10) + 12, + + // 设定文字内容 + text: words => words.name, + }); + + // 执行词云布局函数,并在回调函数中调用G2对结果进行绘制 + layout.exec((texts) => { + const chart = new G2.Chart({ + container: this.node, + width, + height, + plotCfg: { + margin: 0, + }, + }); + + chart.legend(false); + chart.axis(false); + chart.tooltip(false); + + chart.source(texts); + + // 将词云坐标系调整为G2的坐标系 + chart.coord().reflect(); + + chart + .point() + .position('x*y') + .color('text') + .size('size', size => size) + .shape('cloud') + .style({ + fontStyle: texts[0].style, + fontFamily: texts[0].font, + fontWeight: texts[0].weight, + }); + + chart.render(); + }); + } + + render() { + return ( +
    (this.root = n)} style={{ width: '100%' }}> +
    (this.node = n)} /> +
    + ); + } +} + +export default TagCloud; + diff --git a/src/components/TagSelect/index.js b/src/components/TagSelect/index.js new file mode 100644 index 00000000..e0969eba --- /dev/null +++ b/src/components/TagSelect/index.js @@ -0,0 +1,164 @@ +import React, { PureComponent } from 'react'; +import classNames from 'classnames'; +import { Tag, Icon } from 'antd'; + +import styles from './index.less'; + +const CheckableTag = Tag.CheckableTag; + +const TagSelectOption = ({ children, checked, onChange, value }) => ( + onChange(value, state)} + > + {children} + +); +TagSelectOption.defaultProps = { + displayName: 'TagSelectOption', +}; + +const TagSelectExpand = ({ children }) => ( +
    {children}
    +); +TagSelectExpand.defaultProps = { + displayName: 'TagSelectExpand', +}; + +class TagSelect extends PureComponent { + static defaultProps = { + initialValue: [], + }; + + state = { + checkedAll: false, + expand: false, + checkedTags: this.props.initialValue || [], + }; + + onSelectAll = (checked) => { + const { onChange } = this.props; + let checkedTags = []; + let expand = this.state.expand; + + if (checked) { + const tags = this.getAllTags(); + checkedTags = tags.list; + expand = tags.expand; + } + + this.setState({ + checkedAll: checked, + checkedTags, + expand, + }); + + if (onChange) { + onChange(checkedTags); + } + } + + getAllTags() { + let expand = this.state.expand; + const { children } = this.props; + + let checkedTags = children.filter(child => child.props.displayName === 'TagSelectOption').map(child => child.props.value); + const expandChild = children.filter(child => child.props.displayName === 'TagSelectExpand')[0]; + if (expandChild) { + checkedTags = checkedTags.concat( + expandChild.props.children.map(child => child.props.value) + ); + expand = true; + } + return { + list: checkedTags, + expand, + }; + } + + handleTagChange = (value, checked) => { + const { onChange } = this.props; + const { checkedTags } = this.state; + + const index = checkedTags.indexOf(value); + if (checked && index === -1) { + checkedTags.push(value); + } else if (!checked && index > -1) { + checkedTags.splice(index, 1); + } + + const tags = this.getAllTags(); + + let checkedAll = false; + if (tags.list.length === checkedTags.length) { + checkedAll = true; + } + + this.setState({ + checkedAll, + checkedTags, + }); + + if (onChange) { + onChange(checkedTags); + } + } + + handleExpand = () => { + this.setState({ + expand: !this.state.expand, + }); + } + + render() { + const { checkedTags, checkedAll, expand } = this.state; + const { children } = this.props; + + const expandNode = children.filter(child => child.props.displayName === 'TagSelectExpand')[0]; + + const cls = classNames(styles.tagSelect, { + [styles.expandTag]: expandNode, + }); + + return ( +
    + + 全部 + + { + children.filter(child => child.props.displayName === 'TagSelectOption').map(child => React.cloneElement(child, { + key: `tag-select-${child.props.value}`, + checked: checkedTags.indexOf(child.props.value) > -1, + onChange: this.handleTagChange, + })) + } + { + expandNode && + { expand ? '收起' : '展开'} + + } + { + expandNode &&
    + { + expandNode.props.children.map(child => React.cloneElement(child, { + key: `tag-select-${child.props.value}`, + checked: checkedTags.indexOf(child.props.value) > -1, + onChange: this.handleTagChange, + })) + } +
    + } +
    + ); + } +} + +TagSelect.Option = TagSelectOption; +TagSelect.Expand = TagSelectExpand; + +export default TagSelect; diff --git a/src/components/TagSelect/index.less b/src/components/TagSelect/index.less new file mode 100644 index 00000000..c669684b --- /dev/null +++ b/src/components/TagSelect/index.less @@ -0,0 +1,26 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../utils/utils.less"; + +.tagSelect { + user-select: none; + margin-left: -8px; + position: relative; + .expand { + transition: all 0.32s ease; + overflow: hidden; + max-height: 100px; + } + .fold { + .expand(); + max-height: 0; + } + .trigger { + position: absolute; + top: 0; + right: 0; + } +} +.expandTag { + padding-right: 50px; +} + diff --git a/src/components/TimelineChart/index.js b/src/components/TimelineChart/index.js new file mode 100644 index 00000000..c5b185cd --- /dev/null +++ b/src/components/TimelineChart/index.js @@ -0,0 +1,104 @@ +import React, { Component } from 'react'; +import G2 from 'g2'; +import Slider from 'g2-plugin-slider'; +import styles from './index.less'; + +class TimelineChart extends Component { + componentDidMount() { + this.renderChart(this.props.data); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.data !== this.props.data) { + this.renderChart(nextProps.data); + } + } + + sliderId = `timeline-chart-slider-${Math.random() * 1000}` + + handleRef = (n) => { + this.node = n; + } + + renderChart(data) { + const { height = 400, margin = [60, 40, 40, 40], titleMap } = this.props; + + if (!data || (data && data.length < 1)) { + return; + } + + // clean + if (this.sliderId) { + document.getElementById(this.sliderId).innerHTML = ''; + } + this.node.innerHTML = ''; + + const chart = new G2.Chart({ + container: this.node, + forceFit: true, + height, + plotCfg: { + margin, + }, + }); + + chart.axis('x', { + title: false, + }); + chart.axis('y1', { + title: false, + }); + chart.axis('y2', false); + + chart.legend({ + mode: false, + position: 'top', + }); + + chart.source(data, { + x: { + type: 'timeCat', + tickCount: 16, + mask: 'HH:MM', + range: [0, 1], + }, + y1: { + alias: titleMap.y1, + min: 0, + }, + y2: { + alias: titleMap.y2, + min: 0, + }, + }); + + chart.line().position('x*y1').color('#4FAAEB'); + chart.line().position('x*y2').color('#9AD681'); + + /* eslint new-cap:0 */ + const slider = new Slider({ + domId: this.sliderId, + height: 26, + xDim: 'x', + yDim: 'y1', + charts: [chart], + }); + slider.render(); + } + + render() { + const { height, title } = this.props; + + return ( +
    +
    + { title &&

    {title}

    } +
    +
    +
    +
    + ); + } +} + +export default TimelineChart; diff --git a/src/components/TimelineChart/index.less b/src/components/TimelineChart/index.less new file mode 100644 index 00000000..17519756 --- /dev/null +++ b/src/components/TimelineChart/index.less @@ -0,0 +1,3 @@ +.timelineChart { + background: #fff; +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 00000000..1a5f9a5b --- /dev/null +++ b/src/index.js @@ -0,0 +1,25 @@ +import dva from 'dva'; +// import { browserHistory } from 'dva/router'; +import 'moment/locale/zh-cn'; +import models from './models'; + +import './index.less'; + +// 1. Initialize +const app = dva({ + // history: browserHistory, +}); + +// 2. Plugins +// app.use({}); + +// 3. Model +models.forEach((m) => { + app.model(m); +}); + +// 4. Router +app.router(require('./router')); + +// 5. Start +app.start('#root'); diff --git a/src/index.less b/src/index.less new file mode 100644 index 00000000..06d5f3e5 --- /dev/null +++ b/src/index.less @@ -0,0 +1,14 @@ +html, body, :global(#root) { + height: 100%; +} + +body { + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +// temporary font size patch +:global(.ant-tag) { + font-size: 12px; +} diff --git a/src/layouts/BasicLayout.js b/src/layouts/BasicLayout.js new file mode 100644 index 00000000..bd3ffa8a --- /dev/null +++ b/src/layouts/BasicLayout.js @@ -0,0 +1,260 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Layout, Menu, Icon, Avatar, Dropdown, Tag, message } from 'antd'; +import DocumentTitle from 'react-document-title'; +import { connect } from 'dva'; +import { Link, routerRedux } from 'dva/router'; +import moment from 'moment'; +import groupBy from 'lodash/groupBy'; +import styles from './BasicLayout.less'; +import HeaderSearch from '../components/HeaderSearch'; +import NoticeIcon from '../components/NoticeIcon'; +import GlobalFooter from '../components/GlobalFooter'; +import { menus } from '../common/nav'; + +const { Header, Sider, Content } = Layout; +const { SubMenu } = Menu; + +class BasicLayout extends React.PureComponent { + static childContextTypes = { + routes: PropTypes.array, + params: PropTypes.object, + } + state = { + mode: 'inline', + }; + getChildContext() { + const { routes, params } = this.props; + return { routes, params }; + } + componentDidMount() { + this.props.dispatch({ + type: 'user/fetchCurrent', + }); + } + onCollapse = (collapsed) => { + this.props.dispatch({ + type: 'global/changeLayoutCollapsed', + payload: collapsed, + }); + } + onMenuClick = ({ key }) => { + if (key === 'logout') { + this.props.dispatch(routerRedux.push('/user/login')); + } + } + getDefaultCollapsedSubMenus() { + const currentMenuSelectedKeys = [...this.getCurrentMenuSelectedKeys()]; + currentMenuSelectedKeys.splice(-1, 1); + return currentMenuSelectedKeys; + } + getCurrentMenuSelectedKeys() { + const { location: { pathname } } = this.props; + const keys = pathname.split('/').slice(1); + if (keys.length === 1 && keys[0] === '') { + return [menus[0].key]; + } + return keys; + } + getNavMenuItems(menusData, parentPath = '') { + return menusData.map((item) => { + if (!item.name) { + return null; + } + const itemPath = `${parentPath}/${item.path || ''}`.replace(/\/+/g, '/'); + if (item.children && item.children.some(child => child.name)) { + return ( + + + {item.name} + + } + key={item.key || item.path} + > + {this.getNavMenuItems(item.children, itemPath)} + + ); + } + return ( + + + + {item.name} + + + ); + }); + } + getPageTitle() { + const { routes } = this.props; + for (let i = routes.length - 1; i >= 0; i -= 1) { + if (routes[i].breadcrumbName) { + return `${routes[i].breadcrumbName} - Ant Design Pro`; + } + } + return 'Ant Design Pro'; + } + 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 = ({ + processing: 'blue', + urgent: 'red', + doing: 'yellow', + })[newNotice.status]; + newNotice.extra = {newNotice.extra}; + } + return newNotice; + }); + return groupBy(newNotices, 'type'); + } + toggle = () => { + const { collapsed } = this.props; + this.props.dispatch({ + type: 'global/changeLayoutCollapsed', + payload: !collapsed, + }); + } + handleNoticeClear = (type) => { + message.success(`清空了${type}`); + this.props.dispatch({ + type: 'global/clearNotices', + payload: type, + }); + } + handleNoticeVisibleChange = (visible) => { + if (visible) { + this.props.dispatch({ + type: 'global/fetchNotices', + }); + } + } + render() { + const { children, currentUser, collapsed, fetchingNotices } = this.props; + + const menu = ( + + 个人中心 + 设置 + + 退出登录 + + ); + + const noticeData = this.getNoticeData(); + + return ( + + + +
    + + logo +

    Ant Design Pro

    + +
    + + {this.getNavMenuItems(menus)} + +
    + +
    + +
    + { + console.log('input', value); // eslint-disable-line + }} + onPressEnter={(value) => { + console.log('enter', value); // eslint-disable-line + }} + /> + { + console.log(item, tabProps); // eslint-disable-line + }} + onClear={this.handleNoticeClear} + onPopupVisibleChange={this.handleNoticeVisibleChange} + loading={fetchingNotices} + popupAlign={{ offset: [20, -16] }} + > + + + + + + + + {currentUser.name} + + +
    +
    + + {children} + Copyright 2017 蚂蚁金服体验技术部出品
    } + /> + + + + + ); + } +} + +export default connect(state => ({ + currentUser: state.user.currentUser, + collapsed: state.global.collapsed, + fetchingNotices: state.global.fetchingNotices, + notices: state.global.notices, +}))(BasicLayout); diff --git a/src/layouts/BasicLayout.less b/src/layouts/BasicLayout.less new file mode 100644 index 00000000..eccda762 --- /dev/null +++ b/src/layouts/BasicLayout.less @@ -0,0 +1,95 @@ +@import "~antd/lib/style/themes/default.less"; + +.header { + background: @primary-color; + padding: 0 16px 0 0; + color: #fff; +} + +.logo { + height: 64px; + position: relative; + line-height: 64px; + padding: 0 24px; + background: @primary-color; + overflow: hidden; + img { + display: inline-block; + vertical-align: middle; + height: 32px; + } + h1 { + color: #fff; + display: inline-block; + vertical-align: middle; + font-size: 22px; + margin-left: 12px; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + margin-top: -3px; + } +} + +:global(.ant-layout-sider-collapsed) .logo > a { + width: 32px; +} + +.trigger { + font-size: 20px; + line-height: 64px; + cursor: pointer; + transition: all .3s; + color: #fff; + padding: 0 28px; + vertical-align: middle; + &:hover { + background: @primary-7; + } +} + +.right { + float: right; + height: 100%; + .action { + cursor: pointer; + padding: 0 12px; + display: inline-block; + transition: all .3s; + height: 100%; + > i { + font-size: 20px; + vertical-align: middle; + } + &:global(.ant-popover-open), + &:hover { + background: @primary-7; + } + } + .search:hover { + background: transparent; + } + .account { + .avatar { + margin: 20px 8px 20px 0; + color: @primary-color; + background: rgba(255, 255, 255, .85); + vertical-align: middle; + } + } +} + +.menu { + :global(.anticon) { + margin-right: 8px; + } + :global(.ant-dropdown-menu-item) { + padding-left: 16px; + padding-right: 16px; + width: 190px; + } +} + +:global { + .ant-layout { + overflow-x: hidden; + } +} diff --git a/src/layouts/BlankLayout.js b/src/layouts/BlankLayout.js new file mode 100644 index 00000000..505270f8 --- /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 00000000..ef1f0f6d --- /dev/null +++ b/src/layouts/PageHeaderLayout.js @@ -0,0 +1,9 @@ +import React from 'react'; +import PageHeader from '../components/PageHeader'; + +export default ({ children, ...restProps }) => ( +
    + + {children ?
    {children}
    : null} +
    +); diff --git a/src/layouts/UserLayout.js b/src/layouts/UserLayout.js new file mode 100644 index 00000000..4ae73853 --- /dev/null +++ b/src/layouts/UserLayout.js @@ -0,0 +1,60 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import DocumentTitle from 'react-document-title'; +import { Icon } from 'antd'; +import GlobalFooter from '../components/GlobalFooter'; +import styles from './UserLayout.less'; + +const links = [{ + title: '帮助', + href: '', +}, { + title: '隐私', + href: '', +}, { + title: '条款', + href: '', +}]; + +const copyright =
    Copyright 2017 蚂蚁金服体验技术部出品
    ; + +class UserLayout extends React.PureComponent { + static childContextTypes = { + routes: PropTypes.array, + params: PropTypes.object, + } + getChildContext() { + const { routes, params } = this.props; + return { routes, params }; + } + getPageTitle() { + const { routes } = this.props; + for (let i = routes.length - 1; i >= 0; i -= 1) { + if (routes[i].breadcrumbName) { + return `${routes[i].breadcrumbName} - Ant Design Pro`; + } + } + return 'Ant Design Pro'; + } + render() { + const { children } = this.props; + + return ( + +
    +
    +
    + + Ant Design +
    +

    Ant Design 是东半球最具影响力的 Web 设计规范

    +
    + {children} + +
    +
    + ); + } +} + +export default UserLayout; diff --git a/src/layouts/UserLayout.less b/src/layouts/UserLayout.less new file mode 100644 index 00000000..7da9a074 --- /dev/null +++ b/src/layouts/UserLayout.less @@ -0,0 +1,46 @@ +@import "~antd/lib/style/themes/default.less"; + +.container { + background: @background-color-base; + background-image: url('https://gw.alipayobjects.com/zos/rmsportal/bOjjckIwLKuWCswKAghg.svg'); + width: 100%; + min-height: 100%; + background-repeat: no-repeat; + background-position: center; + background-size: 85%; + padding: 110px 0 144px 0; + position: relative; +} + +.top { + text-align: center; +} + +.header { + height: 44px; + line-height: 44px; +} + +.logo { + height: 44px; + vertical-align: top; + margin-right: 12px; +} + +.title { + font-size: 33px; + color: @heading-color; +} + +.desc { + font-size: @font-size-lg; + color: @text-color-secondary; + margin-top: 12px; + margin-bottom: 40px; +} + +.footer { + position: absolute; + width: 100%; + bottom: 0; +} diff --git a/src/models/activities.js b/src/models/activities.js new file mode 100644 index 00000000..c12584eb --- /dev/null +++ b/src/models/activities.js @@ -0,0 +1,43 @@ +import { queryActivities } from '../services/api'; + +export default { + namespace: 'activities', + + state: { + list: [], + loading: true, + }, + + effects: { + *fetchList({ payload }, { call, put }) { + yield put({ + type: 'changeLoading', + payload: true, + }); + const response = yield call(queryActivities); + yield put({ + type: 'saveList', + payload: response, + }); + yield put({ + type: 'changeLoading', + payload: false, + }); + }, + }, + + reducers: { + saveList(state, action) { + return { + ...state, + list: action.payload, + }; + }, + changeLoading(state, action) { + return { + ...state, + loading: action.payload, + }; + }, + }, +}; diff --git a/src/models/chart.js b/src/models/chart.js new file mode 100644 index 00000000..43e83e8f --- /dev/null +++ b/src/models/chart.js @@ -0,0 +1,64 @@ +import { fakeChartData } from '../services/api'; + +export default { + namespace: 'chart', + + state: { + visitData: [], + salesData: [], + searchData: [], + offlineData: [], + offlineChartData: [], + salesTypeData: [], + salesTypeDataOnline: [], + salesTypeDataOffline: [], + radarData: [], + }, + + effects: { + *fetch({ payload }, { call, put }) { + const response = yield call(fakeChartData); + yield put({ + type: 'save', + payload: response, + }); + }, + *fetchSalesData({ payload }, { call, put }) { + const response = yield call(fakeChartData); + yield put({ + type: 'save', + payload: { + salesData: response.salesData, + }, + }); + }, + }, + + reducers: { + save(state, { payload }) { + return { + ...state, + ...payload, + }; + }, + setter(state, { payload }) { + return { + ...state, + ...payload, + }; + }, + clear() { + return { + visitData: [], + salesData: [], + searchData: [], + offlineData: [], + offlineChartData: [], + salesTypeData: [], + salesTypeDataOnline: [], + salesTypeDataOffline: [], + radarData: [], + }; + }, + }, +}; diff --git a/src/models/form.js b/src/models/form.js new file mode 100644 index 00000000..a3a144a2 --- /dev/null +++ b/src/models/form.js @@ -0,0 +1,88 @@ +import { routerRedux } from 'dva/router'; +import { message } from 'antd'; +import { fakeSubmitForm } from '../services/api'; + +export default { + namespace: 'form', + + state: { + step: { + }, + regularFormSubmitting: false, + stepFormSubmitting: false, + advancedFormSubmitting: false, + }, + + effects: { + *submitRegularForm({ payload }, { call, put }) { + yield put({ + type: 'changeRegularFormSubmitting', + payload: true, + }); + yield call(fakeSubmitForm, payload); + yield put({ + type: 'changeRegularFormSubmitting', + payload: false, + }); + message.success('提交成功'); + }, + *submitStepForm({ payload }, { call, put }) { + yield put({ + type: 'changeStepFormSubmitting', + payload: true, + }); + yield call(fakeSubmitForm, payload); + yield put({ + type: 'saveStepFormData', + payload, + }); + yield put({ + type: 'changeStepFormSubmitting', + payload: false, + }); + yield put(routerRedux.push('/form/step-form/result')); + }, + *submitAdvancedForm({ payload }, { call, put }) { + yield put({ + type: 'changeAdvancedFormSubmitting', + payload: true, + }); + yield call(fakeSubmitForm, payload); + yield put({ + type: 'changeAdvancedFormSubmitting', + payload: false, + }); + message.success('提交成功'); + }, + }, + + reducers: { + saveStepFormData(state, { payload }) { + return { + ...state, + step: { + ...state.step, + ...payload, + }, + }; + }, + changeRegularFormSubmitting(state, { payload }) { + return { + ...state, + regularFormSubmitting: payload, + }; + }, + changeStepFormSubmitting(state, { payload }) { + return { + ...state, + stepFormSubmitting: payload, + }; + }, + changeAdvancedFormSubmitting(state, { payload }) { + return { + ...state, + advancedFormSubmitting: payload, + }; + }, + }, +}; diff --git a/src/models/global.js b/src/models/global.js new file mode 100644 index 00000000..6628c59b --- /dev/null +++ b/src/models/global.js @@ -0,0 +1,53 @@ +import { queryNotices } from '../services/api'; + +export default { + namespace: 'global', + + state: { + collapsed: false, + notices: [], + fetchingNotices: false, + }, + + effects: { + *fetchNotices({ payload }, { call, put }) { + yield put({ + type: 'changeNoticeLoading', + payload: true, + }); + const data = yield call(queryNotices); + yield put({ + type: 'saveNotices', + payload: data, + }); + }, + }, + + reducers: { + changeLayoutCollapsed(state, { payload }) { + return { + ...state, + collapsed: payload, + }; + }, + saveNotices(state, { payload }) { + return { + ...state, + notices: payload, + fetchingNotices: false, + }; + }, + clearNotices(state, { payload }) { + return { + ...state, + notices: state.notices.filter(item => item.type !== payload), + }; + }, + changeNoticeLoading(state, { payload }) { + return { + ...state, + fetchingNotices: payload, + }; + }, + }, +}; diff --git a/src/models/index.js b/src/models/index.js new file mode 100644 index 00000000..36666147 --- /dev/null +++ b/src/models/index.js @@ -0,0 +1,11 @@ +// Use require.context to require reducers automatically +// Ref: https://webpack.github.io/docs/context.html +const context = require.context('./', false, /\.js$/); +const keys = context.keys().filter(item => item !== './index.js'); + +const models = []; +for (let i = 0; i < keys.length; i += 1) { + models.push(context(keys[i])); +} + +export default models; diff --git a/src/models/list.js b/src/models/list.js new file mode 100644 index 00000000..8d74663f --- /dev/null +++ b/src/models/list.js @@ -0,0 +1,46 @@ +import { queryFakeList } from '../services/api'; + +export default { + namespace: 'list', + + state: { + list: [], + loading: true, + }, + + effects: { + *fetch({ payload, callback }, { call, put }) { + yield put({ + type: 'changeLoading', + payload: true, + }); + const response = yield call(queryFakeList, payload); + yield put({ + type: 'save', + payload: response, + }); + yield put({ + type: 'changeLoading', + payload: false, + }); + if (callback) { + callback(); + } + }, + }, + + reducers: { + save(state, action) { + return { + ...state, + list: action.payload, + }; + }, + changeLoading(state, action) { + return { + ...state, + loading: action.payload, + }; + }, + }, +}; diff --git a/src/models/login.js b/src/models/login.js new file mode 100644 index 00000000..f815f46b --- /dev/null +++ b/src/models/login.js @@ -0,0 +1,58 @@ +import { fakeAccountLogin, fakeMobileLogin } from '../services/api'; + +export default { + namespace: 'login', + + state: { + status: undefined, + }, + + effects: { + *accountSubmit({ payload }, { call, put }) { + yield put({ + type: 'changeSubmitting', + payload: true, + }); + const response = yield call(fakeAccountLogin); + yield put({ + type: 'loginHandle', + payload: response, + }); + yield put({ + type: 'changeSubmitting', + payload: false, + }); + }, + *mobileSubmit({ payload }, { call, put }) { + yield put({ + type: 'changeSubmitting', + payload: true, + }); + const response = yield call(fakeMobileLogin); + yield put({ + type: 'loginHandle', + payload: response, + }); + yield put({ + type: 'changeSubmitting', + payload: false, + }); + }, + }, + + reducers: { + loginHandle(state, { payload }) { + return { + ...state, + status: payload.status, + type: payload.type, + }; + }, + changeSubmitting(state, { payload }) { + return { + ...state, + submitting: payload, + }; + }, + }, +}; diff --git a/src/models/monitor.js b/src/models/monitor.js new file mode 100644 index 00000000..3a7503e2 --- /dev/null +++ b/src/models/monitor.js @@ -0,0 +1,28 @@ +import { queryTags } from '../services/api'; + +export default { + namespace: 'monitor', + + state: { + tags: [], + }, + + effects: { + *fetchTags({ payload }, { 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 00000000..7c7a9a6d --- /dev/null +++ b/src/models/profile.js @@ -0,0 +1,45 @@ +import { queryProfile } from '../services/api'; + +export default { + namespace: 'profile', + + state: { + operation1: [], + operation2: [], + operation3: [], + loading: true, + }, + + effects: { + *fetch({ payload }, { call, put }) { + yield put({ + type: 'changeLoading', + payload: true, + }); + const response = yield call(queryProfile); + yield put({ + type: 'show', + payload: response, + }); + yield put({ + type: 'changeLoading', + payload: false, + }); + }, + }, + + reducers: { + show(state, { payload }) { + return { + ...state, + ...payload, + }; + }, + changeLoading(state, { payload }) { + return { + ...state, + loading: payload, + }; + }, + }, +}; diff --git a/src/models/project.js b/src/models/project.js new file mode 100644 index 00000000..f6243dcf --- /dev/null +++ b/src/models/project.js @@ -0,0 +1,43 @@ +import { queryProjectNotice } from '../services/api'; + +export default { + namespace: 'project', + + state: { + notice: [], + loading: true, + }, + + effects: { + *fetchNotice({ payload }, { call, put }) { + yield put({ + type: 'changeLoading', + payload: true, + }); + const response = yield call(queryProjectNotice); + yield put({ + type: 'saveNotice', + payload: response, + }); + yield put({ + type: 'changeLoading', + payload: false, + }); + }, + }, + + reducers: { + saveNotice(state, action) { + return { + ...state, + notice: action.payload, + }; + }, + changeLoading(state, action) { + return { + ...state, + loading: action.payload, + }; + }, + }, +}; diff --git a/src/models/register.js b/src/models/register.js new file mode 100644 index 00000000..c878692f --- /dev/null +++ b/src/models/register.js @@ -0,0 +1,42 @@ +import { fakeRegister } from '../services/api'; + +export default { + namespace: 'register', + + state: { + status: undefined, + }, + + effects: { + *submit({ payload }, { call, put }) { + yield put({ + type: 'changeSubmitting', + payload: true, + }); + const response = yield call(fakeRegister); + yield put({ + type: 'registerHandle', + payload: response, + }); + yield put({ + type: 'changeSubmitting', + payload: false, + }); + }, + }, + + reducers: { + registerHandle(state, { payload }) { + return { + ...state, + status: payload.status, + }; + }, + changeSubmitting(state, { payload }) { + return { + ...state, + submitting: payload, + }; + }, + }, +}; diff --git a/src/models/rule.js b/src/models/rule.js new file mode 100644 index 00000000..8b36ba3f --- /dev/null +++ b/src/models/rule.js @@ -0,0 +1,80 @@ +import { queryRule, removeRule, addRule } from '../services/api'; + +export default { + namespace: 'rule', + + state: { + data: { + list: [], + pagination: {}, + }, + loading: true, + }, + + effects: { + *fetch({ payload }, { call, put }) { + yield put({ + type: 'changeLoading', + payload: true, + }); + const response = yield call(queryRule, payload); + yield put({ + type: 'save', + payload: response, + }); + yield put({ + type: 'changeLoading', + payload: false, + }); + }, + *add({ payload, callback }, { call, put }) { + yield put({ + type: 'changeLoading', + payload: true, + }); + const response = yield call(addRule, payload); + yield put({ + type: 'save', + payload: response, + }); + yield put({ + type: 'changeLoading', + payload: false, + }); + + if (callback) callback(); + }, + *remove({ payload, callback }, { call, put }) { + yield put({ + type: 'changeLoading', + payload: true, + }); + const response = yield call(removeRule, payload); + yield put({ + type: 'save', + payload: response, + }); + yield put({ + type: 'changeLoading', + payload: false, + }); + + if (callback) callback(); + }, + }, + + reducers: { + save(state, action) { + return { + ...state, + data: action.payload, + }; + }, + changeLoading(state, action) { + return { + ...state, + loading: action.payload, + }; + }, + }, +}; diff --git a/src/models/user.js b/src/models/user.js new file mode 100644 index 00000000..570be681 --- /dev/null +++ b/src/models/user.js @@ -0,0 +1,57 @@ +import { query as queryUsers, queryCurrent } from '../services/user'; + +export default { + namespace: 'user', + + state: { + list: [], + loading: false, + currentUser: {}, + }, + + effects: { + *fetch({ payload }, { call, put }) { + yield put({ + type: 'changeLoading', + payload: true, + }); + const response = yield call(queryUsers); + yield put({ + type: 'save', + payload: response, + }); + yield put({ + type: 'changeLoading', + payload: false, + }); + }, + *fetchCurrent({ payload }, { call, put }) { + const response = yield call(queryCurrent); + yield put({ + type: 'saveCurrentUser', + payload: response, + }); + }, + }, + + reducers: { + save(state, action) { + return { + ...state, + list: action.payload, + }; + }, + changeLoading(state, action) { + return { + ...state, + loading: action.payload, + }; + }, + saveCurrentUser(state, action) { + return { + ...state, + currentUser: action.payload, + }; + }, + }, +}; diff --git a/src/router.js b/src/router.js new file mode 100644 index 00000000..a31ba0ca --- /dev/null +++ b/src/router.js @@ -0,0 +1,47 @@ +import React from 'react'; +import { Router, Route, Redirect } from 'dva/router'; +import navData from './common/nav'; + +function getRoutes(data, level = 0) { + return data.map((item, i) => { + let children; + if (item.children) { + children = getRoutes(item.children, level + 1); + } + let homePageRedirect; + if (level === 1 && i === 0) { + let indexPath; + // First children router + if (item.children && item.children[0]) { + indexPath = `/${item.path}/${item.children[0].path}`; + } else { + indexPath = item.path; + } + homePageRedirect = ; + } + if (item.noRoute) { + return null; + } + return ( + + {homePageRedirect} + {children} + + ); + }); +} + +function RouterConfig({ history }) { + return ( + + {getRoutes(navData)} + + ); +} + +export default RouterConfig; diff --git a/src/routes/Dashboard.css b/src/routes/Dashboard.css new file mode 100644 index 00000000..3f7685d1 --- /dev/null +++ b/src/routes/Dashboard.css @@ -0,0 +1,5 @@ +.normal { + font-family: Georgia, sans-serif; + margin-top: 3em; + text-align: center; +} diff --git a/src/routes/Dashboard.js b/src/routes/Dashboard.js new file mode 100644 index 00000000..0bb12890 --- /dev/null +++ b/src/routes/Dashboard.js @@ -0,0 +1,100 @@ +import React, { PureComponent } from 'react'; +import { connect } from 'dva'; +import { Row, Col, Card, Table, Icon } from 'antd'; + +const columns = [{ + title: 'Name', + dataIndex: 'name', + key: 'name', +}, { + title: 'Age', + dataIndex: 'age', + key: 'age', +}, { + title: 'Address', + dataIndex: 'address', + key: 'address', +}, { + title: 'Action', + key: 'action', + render: (text, record) => ( + + Action 一 {record.name} + + Delete + + + More actions + + + ), +}]; + +class Dashboard extends PureComponent { + componentDidMount() { + this.props.dispatch({ + type: 'user/fetch', + }); + } + render() { + const { user: { list, loading } } = this.props; + return ( +
    + +
    + +

    卡片内容

    +

    卡片内容

    +

    卡片内容

    +
    + + + +

    卡片内容

    +

    卡片内容

    +

    卡片内容

    +
    + + + +

    卡片内容

    +

    卡片内容

    +

    卡片内容

    +
    + + + + + +

    卡片内容

    +

    卡片内容

    +

    卡片内容

    +
    + + + +

    卡片内容

    +

    卡片内容

    +

    卡片内容

    +
    + + + + + } + > +
    + + + + + ); + } +} + +export default connect(state => ({ + user: state.user, +}))(Dashboard); diff --git a/src/routes/Dashboard/Analysis.js b/src/routes/Dashboard/Analysis.js new file mode 100644 index 00000000..9ea97b18 --- /dev/null +++ b/src/routes/Dashboard/Analysis.js @@ -0,0 +1,388 @@ +import React, { Component } from 'react'; +import { connect } from 'dva'; +import { Row, Col, Icon, Card, Tabs, Table, Radio, DatePicker, Tooltip } from 'antd'; +import numeral from 'numeral'; + +import { ChartCard, Trend, yuan, MiniArea, MiniBar, MiniProgress, Field, Bar, Pie, NumberInfo, IconUp, IconDown } from '../../components/Charts'; + +import TimelineChart from '../../components/TimelineChart'; +import { getTimeDistance } from '../../utils/utils'; + +import styles from './Analysis.less'; + +const TabPane = Tabs.TabPane; +const { RangePicker } = DatePicker; + +const rankingListData = []; +for (let i = 0; i < 7; i += 1) { + rankingListData.push({ + title: `工专路 ${i} 号店`, + total: 323234, + }); +} + +@connect(state => ({ + chart: state.chart, +})) +export default class Analysis extends Component { + state = { + salesType: 'all', + currentTabKey: '', + rangePickerValue: [], + } + + 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, + }); + } + + selectDate = (type) => { + this.setState({ + rangePickerValue: getTimeDistance(type), + }); + + this.props.dispatch({ + type: 'chart/fetchSalesData', + }); + } + + render() { + const { rangePickerValue, salesType, currentTabKey } = this.state; + const { chart } = this.props; + const { + visitData, + salesData, + searchData, + offlineData, + offlineChartData, + salesTypeData, + salesTypeDataOnline, + salesTypeDataOffline, + } = chart; + + const salesPieData = salesType === 'all' ? + salesTypeData + : + (salesType === 'online' ? salesTypeDataOnline : salesTypeDataOffline); + + 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, + }, + { + title: '周涨幅', + dataIndex: 'range', + key: 'range', + sorter: (a, b) => a.range - b.range, + render: (text, record) => ( + {text}% {record.status === 1 ? : } + ), + }, + ]; + + const CustomTab = ({ data, currentTabKey: currentKey }) => ( + + + + + + + + + ); + + const topColResponsiveProps = { + xs: 24, + sm: 12, + md: 6, + style: { marginBottom: 24 }, + }; + + return ( +
    + +
    + } + total={yuan(126560)} + footer={} + contentHeight={46} + > + + 12.3% + 11% + + + + + } + total={numeral(8846).format('0,0')} + footer={} + contentHeight={46} + > + + + + + } + total={numeral(6560).format('0,0')} + footer={} + contentHeight={46} + > + + + + + } + total="78%" + footer={ + 12.3% + 11% + } + contentHeight={46} + > + + + + + + +
    + + + +
    + + + +

    门店销售额排名

    +
      + { + rankingListData.map((item, i) => ( +
    • + {i + 1} + {item.title} + {numeral(item.total).format('0,0')} +
    • + )) + } +
    + + + + + 访问量没有, 因为偷懒了 + + + + + + + + + + + 搜索用户数量 } + total={numeral(12321).format('0,0')} + status="up" + subTotal={17.1} + /> + + + + + + + +
    record.index} + size="middle" + columns={columns} + dataSource={searchData} + pagination={{ + style: { marginBottom: 0 }, + showSizeChanger: true, + showQuickJumper: true, + pageSize: 5, + }} + /> + + + + + + 全部渠道 + 线上 + 门店 + +
    + now.y + pre, 0))} + data={salesPieData} + valueFormat={val => yuan(val)} + height={294} + /> +
    +
    + + + + + + { + 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 00000000..390941ae --- /dev/null +++ b/src/routes/Dashboard/Analysis.less @@ -0,0 +1,100 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../utils/utils.less"; + +.iconGroup { + i { + cursor: pointer; + margin-left: 16px; + } +} +.rankingList { + margin-top: 25px; + 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; + 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; + } + } +} + +.salesCard { + :global { + .ant-tabs-content { + padding: 0 24px 24px 24px; + } + .ant-tabs-bar { + padding-left: 24px; + .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; + } + } +} + +@media screen and (max-width: @screen-lg) { + .rankingList { + li { + span:first-child { + margin-right: 8px; + } + } + } +} + +@media screen and (max-width: @screen-sm) { + .salesExtra { + display: none; + } + .salesExtraWrap { + position: absolute; + top: 50px; + left: 24px; + } + .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 00000000..b8b20568 --- /dev/null +++ b/src/routes/Dashboard/Monitor.js @@ -0,0 +1,191 @@ +import React, { PureComponent } from 'react'; +import { connect } from 'dva'; +import { Row, Col, Card } from 'antd'; +import numeral from 'numeral'; + +import PageHeaderLayout from '../../layouts/PageHeaderLayout'; +import { NumberInfo, MiniArea, Pie, WaterWave, Gauge } from '../../components/Charts'; +import MapChart from '../../components/MapChart'; +import TagCloud from '../../components/TagCloud'; +import Countdown from '../../components/Countdown'; +import { fixedZero } from '../../utils/utils'; + +import styles from './Monitor.less'; + +const activeData = []; +for (let i = 0; i < 24; i += 1) { + activeData.push({ + x: `${fixedZero(i)}:00`, + y: (i * 50) + (Math.floor(Math.random() * 200)), + }); +} + +const MapData = []; +for (let i = 0; i < 50; i += 1) { + MapData.push({ + x: Math.floor(Math.random() * 600), + y: Math.floor(Math.random() * 400), + value: Math.floor(Math.random() * 1000) + 500, + }); +} +const targetTime = new Date().getTime() + 3900000; + +@connect(state => ({ + monitor: state.monitor, +})) +export default class Monitor extends PureComponent { + componentDidMount() { + this.props.dispatch({ + type: 'monitor/fetchTags', + }); + } + + render() { + const { monitor } = this.props; + const { tags } = monitor; + + 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/routes/Dashboard/Monitor.less b/src/routes/Dashboard/Monitor.less new file mode 100644 index 00000000..59a8357e --- /dev/null +++ b/src/routes/Dashboard/Monitor.less @@ -0,0 +1,45 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../utils/utils.less"; + +.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; + } +} + +.mapChart { + padding-top: 46px; + height: 436px; +} + +@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 00000000..c1928a48 --- /dev/null +++ b/src/routes/Dashboard/Workplace.js @@ -0,0 +1,263 @@ +import React, { PureComponent } from 'react'; +import moment from 'moment'; +import { connect } from 'dva'; +import { Link } from 'dva/router'; +import { Row, Col, Card, List, Avatar, Alert, Icon } from 'antd'; + +import PageHeaderLayout from '../../layouts/PageHeaderLayout'; +import EditableLinkGroup from '../../components/EditableLinkGroup'; +import { Radar } from '../../components/Charts'; + +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/CRxBvUggxBYzWBTGmkxF.png', + link: '', + }, + { + id: 'members-2', + title: 'Ant Design', + logo: 'https://gw.alipayobjects.com/zos/rmsportal/RBytOnluTcyeyDazAbvs.png', + link: '', + }, + { + id: 'members-3', + title: 'DesignLab', + logo: 'https://gw.alipayobjects.com/zos/rmsportal/HQVJYAXtWHEJvLxQjmPa.png', + link: '', + }, + { + id: 'members-4', + title: 'Basement', + logo: 'https://gw.alipayobjects.com/zos/rmsportal/HQVJYAXtWHEJvLxQjmPa.png', + link: '', + }, + { + id: 'members-5', + title: 'Github', + logo: 'https://gw.alipayobjects.com/zos/rmsportal/RBytOnluTcyeyDazAbvs.png', + link: '', + }, +]; + +@connect(state => ({ + project: state.project, + activities: state.activities, + chart: state.chart, +})) +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', + }); + } + + render() { + const { + project: { loading: projectLoading, notice }, + activities: { loading: activitiesLoading, list: activitiesList }, + chart: { radarData }, + } = this.props; + + const pageHeaderContent = ( + + ); + + const pageHeaderTitle = ( +
    +
    + +
    +
    +

    早安, 曲丽丽, 祝你开心每一天

    +

    交互专家 | 蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED

    +
    +
    + ); + + const pageHeaderAction = ( +
    +
    +

    项目数

    +

    56

    + +
    +
    +

    团队内排名

    +

    8 / 24

    + +
    +
    +

    项目访问

    +

    2,223

    +
    +
    + ); + + return ( + + +
    + 全部项目} + loading={projectLoading} + bodyStyle={{ padding: 0 }} + > + { + !projectLoading && notice.length > 0 && notice.map(item => ( + + + } + title={{item.title}} + description={item.description} + /> +
    + {item.member || ''} + { + item.updatedAt && {moment(item.updatedAt).fromNow()} + } +
    +
    +
    + )) + } +
    + + +
    + { + activitiesList.map(item => ( + + } + title={

    {item.user.name} 在 xx 新建了项目 xxxx

    } + description={moment(item.updatedAt).fromNow()} + /> +
    + )) + } +
    +
    +
    + +
    + + {}} + links={links} + /> + + +
    + { + + } +
    +
    + +
    + + { + members.map(item => ( +
    + + {item.title} + {item.title} + + + )) + } + + + + + + + ); + } +} diff --git a/src/routes/Dashboard/Workplace.less b/src/routes/Dashboard/Workplace.less new file mode 100644 index 00000000..11990695 --- /dev/null +++ b/src/routes/Dashboard/Workplace.less @@ -0,0 +1,191 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../utils/utils.less"; + +.activitiesList { + padding: 0 24px 24px 24px; + :global { + .ant-list-item-meta-title:hover { + color: @text-color; + } + } +} + +.pageHeaderTitle { + display: flex; + .titleAvatar { + flex: 0 1 80px; + & > span { + border-radius: 80px; + display: block; + width: 80px; + height: 80px; + } + } + .titleContent { + position: relative; + top: 8px; + margin-left: 32px; + flex: 1 1 auto; + p { + font-weight: normal; + } + & > p:last-child { + color: @text-color-secondary; + font-size: @font-size-base; + line-height: 22px; + margin-top: 12px; + } + } +} + +.pageHeaderAction { + float: right; + .clearfix(); + & > div { + text-align: right; + padding: 0 24px; + position: relative; + float: left; + & > p:first-child { + color: @text-color-secondary; + font-size: @font-size-base; + line-height: 22px; + margin-bottom: 2px; + } + & > p { + color: @text-color; + font-size: 30px; + line-height: 38px; + & > span { + color: @text-color-secondary; + font-size: 20px; + } + } + & > em { + background-color: @border-color-split; + position: absolute; + top: 8px; + right: 0; + width: 1px; + height: 40px; + } + } + & > div:last-child { + padding-right: 0; + } +} + +.members { + a { + display: block; + margin-bottom: 24px; + line-height: 24px; + height: 24px; + .textOverflow(); + img { + border-radius: 24px; + display: inline; + position: relative; + top: -2px; + width: 24px; + height: 24px; + margin-right: 12px; + vertical-align: middle; + } + span { + font-size: @font-size-base; + color: @text-color; + line-height: 24px; + max-width: 100px; + .textOverflow(); + } + &:hover { + span { + color: @primary-color; + } + } + } +} + +.projectList { + :global { + .ant-card-meta-title { + font-size: 14px; + a { + color: @heading-color; + &:hover { + color: @primary-color; + } + } + } + .ant-card-meta-description { + font-size: 12px; + min-height: 36px; + } + } + .projectGrid { + width: 33.33%; + } + .projectItemContent { + display: flex; + padding-left: 48px; + margin-top: 12px; + 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; + } + } + span { + color: @text-color-secondary; + flex: 0 0 auto; + float: right; + } + } +} + +@media screen and (max-width: @screen-xl) and (min-width: @screen-lg) { + .pageHeaderAction { + margin-left: -44px; + & > div { + padding: 0 16px; + } + } +} + +@media screen and (max-width: @screen-lg) { + .pageHeaderAction { + margin-left: -64px; + & > div { + padding: 0 16px; + text-align: left; + & > em { + display: none; + } + } + } +} + +@media screen and (max-width: @screen-md) { + .projectList { + .projectGrid { + width: 50%; + } + } +} + +@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 00000000..ea0d2307 --- /dev/null +++ b/src/routes/Exception/403.js @@ -0,0 +1,4 @@ +import React from 'react'; +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 00000000..e114e04c --- /dev/null +++ b/src/routes/Exception/404.js @@ -0,0 +1,4 @@ +import React from 'react'; +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 00000000..7fa97a08 --- /dev/null +++ b/src/routes/Exception/500.js @@ -0,0 +1,4 @@ +import React from 'react'; +import Exception from '../../components/Exception'; + +export default () => ; diff --git a/src/routes/Forms/AdvancedForm.js b/src/routes/Forms/AdvancedForm.js new file mode 100644 index 00000000..a83f7bb3 --- /dev/null +++ b/src/routes/Forms/AdvancedForm.js @@ -0,0 +1,267 @@ +import React from 'react'; +import { Card, Button, Form, Icon, Col, Row, DatePicker, TimePicker, Input, Select, Popover } from 'antd'; +import { connect } from 'dva'; +import PageHeaderLayout from '../../layouts/PageHeaderLayout'; +import FooterToolbar from '../../components/FooterToolbar'; +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', +}]; + +function AdvancedForm({ form, dispatch, submitting }) { + 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: '请输入' }], + })( + + )} + + + + + {getFieldDecorator('type2', { + rules: [{ required: true, message: '请选择仓库类型' }], + })( + + )} + + + + + + + {getFieldDecorator('members', { + initialValue: tableData, + })()} + + + {getErrorInfo()} + + + + + ); +} + +export default connect(state => ({ + collapsed: state.global.collapsed, + submitting: state.form.advancedFormSubmitting, +}))(Form.create()(AdvancedForm)); diff --git a/src/routes/Forms/BasicForm.js b/src/routes/Forms/BasicForm.js new file mode 100644 index 00000000..c6ed6df5 --- /dev/null +++ b/src/routes/Forms/BasicForm.js @@ -0,0 +1,145 @@ +import React, { PureComponent } from 'react'; +import { connect } from 'dva'; +import { Form, Input, DatePicker, Select, Button, Card } from 'antd'; +import PageHeaderLayout from '../../layouts/PageHeaderLayout'; + +const FormItem = Form.Item; +const Option = Select.Option; +const { RangePicker } = DatePicker; + +@connect(state => ({ + submitting: state.form.regularFormSubmitting, +})) +@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 } = this.props.form; + + const formItemLayout = { + labelCol: { + xs: { span: 24 }, + sm: { span: 3 }, + }, + wrapperCol: { + xs: { span: 24 }, + sm: { span: 12 }, + md: { span: 10 }, + }, + }; + + const submitFormLayout = { + wrapperCol: { + xs: { span: 24, offset: 0 }, + sm: { span: 10, offset: 3 }, + }, + }; + + return ( + + +
    + + {getFieldDecorator('appType', { + rules: [{ + required: true, message: '应用类型', + }], + })( + + )} + + + {getFieldDecorator('productName', { + rules: [{ + required: true, message: '请输入产品名', + }], + })( + + )} + + + {getFieldDecorator('appName', { + rules: [ + { required: true, message: '请输入应用名' }, + { pattern: /^[a-zA-Z0-9-]+$/, message: '只能输入英文、数字、中划线' }, + ], + })( + + )} + + + {getFieldDecorator('appChineseName', { + rules: [ + { required: true, message: '请输入应用中文名' }, + { pattern: /^[\u4e00-\u9fa5]+$/, message: '请输入中文' }, + ], + })( + + )} + + + {getFieldDecorator('dateRange', { + rules: [{ type: 'array', required: true, message: '请选择生效日期' }], + })( + + )} + + + {getFieldDecorator('domain', { + rules: [{ required: true, message: '请输入域名' }], + })( + + )} + + + + + +
    +
    + ); + } +} diff --git a/src/routes/Forms/StepForm/Step1.js b/src/routes/Forms/StepForm/Step1.js new file mode 100644 index 00000000..d4982d6b --- /dev/null +++ b/src/routes/Forms/StepForm/Step1.js @@ -0,0 +1,101 @@ +import React from 'react'; +import { Form, Input, Button, Select, Divider } from 'antd'; +import { routerRedux } from 'dva/router'; +import styles from './style.less'; + +const Option = Select.Option; + +export default ({ formItemLayout, form, dispatch }) => { + const { getFieldDecorator, validateFields } = form; + const onValidateForm = () => { + validateFields((err, values) => { + if (!err) { + dispatch({ + type: 'form/saveStepFormData', + payload: values, + }); + dispatch(routerRedux.push('/form/step-form/confirm')); + } + }); + }; + return ( +
    +
    + + {getFieldDecorator('payAccount', { + initialValue: 'ant-design@alipay.com', + rules: [{ required: true, message: '请选择付款账户' }], + })( + + )} + + + + + {getFieldDecorator('receiverAccount', { + initialValue: 'test@example.com', + rules: [ + { required: true, message: '请输入收款人账户' }, + { type: 'email', message: '账户名应为邮箱格式' }, + ], + })( + + )} + + + + {getFieldDecorator('receiverName', { + initialValue: 'Alex', + rules: [{ required: true, message: '请输入收款人姓名' }], + })( + + )} + + + {getFieldDecorator('amount', { + initialValue: '500', + rules: [ + { required: true, message: '请输入转账金额' }, + { pattern: /^(\d+)((?:\.\d+)?)$/, message: '请输入合法金额数字' }, + ], + })( + + )} + + + + + + +
    +

    说明

    +

    转账到支付宝账户

    +

    如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。

    +

    转账到银行卡

    +

    如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。

    +
    +
    + ); +}; diff --git a/src/routes/Forms/StepForm/Step2.js b/src/routes/Forms/StepForm/Step2.js new file mode 100644 index 00000000..4a43d0b2 --- /dev/null +++ b/src/routes/Forms/StepForm/Step2.js @@ -0,0 +1,89 @@ +import React from 'react'; +import { Form, Input, Button, Alert, Divider } from 'antd'; +import { routerRedux } from 'dva/router'; +import styles from './style.less'; + +export default ({ formItemLayout, form, data, dispatch, submitting }) => { + const { getFieldDecorator, validateFields } = form; + const onPrev = () => { + dispatch(routerRedux.push('/form/step-form')); + }; + const onValidateForm = (e) => { + e.preventDefault(); + validateFields((err, values) => { + if (!err) { + dispatch({ + type: 'form/submitStepForm', + payload: { + ...data, + ...values, + }, + }); + } + }); + }; + return ( +
    + + + {data.payAccount} + + + {data.receiverAccount} + + + {data.receiverName} + + + {data.amount} 元 + + + + {getFieldDecorator('password', { + initialValue: '123456', + rules: [{ + required: true, message: '需要支付密码才能进行支付', + }], + })( + + )} + + + + + + + + ); +}; diff --git a/src/routes/Forms/StepForm/Step3.js b/src/routes/Forms/StepForm/Step3.js new file mode 100644 index 00000000..4e8f55dc --- /dev/null +++ b/src/routes/Forms/StepForm/Step3.js @@ -0,0 +1,51 @@ +import React from 'react'; +import { Button, Row, Col } from 'antd'; +import { routerRedux } from 'dva/router'; +import Result from '../../../components/Result'; +import styles from './style.less'; + +export default ({ dispatch, data }) => { + const onFinish = () => { + dispatch(routerRedux.push('/form/step-form')); + }; + const information = ( +
    + +
    付款账户: + {data.payAccount} + + + 收款账户: + {data.receiverAccount} + + + 收款人姓名: + {data.receiverName} + + + 转账金额: + {data.amount} 元 + + + ); + const actions = ( +
    + + +
    + ); + return ( + + ); +}; diff --git a/src/routes/Forms/StepForm/index.js b/src/routes/Forms/StepForm/index.js new file mode 100644 index 00000000..85a0a20c --- /dev/null +++ b/src/routes/Forms/StepForm/index.js @@ -0,0 +1,63 @@ +import React, { cloneElement, PureComponent } from 'react'; +import { connect } from 'dva'; +import { Card, Steps, Form } from 'antd'; +import PageHeaderLayout from '../../../layouts/PageHeaderLayout'; +import Step1 from './Step1'; +import styles from '../style.less'; + +const Step = Steps.Step; + +@Form.create() +class StepForm extends PureComponent { + getCurrentStep() { + const { routes } = this.props; + switch (routes[routes.length - 1].path) { + case 'step-form': return 0; + case 'confirm': return 1; + case 'result': return 2; + default: return 0; + } + } + render() { + const { form, stepFormData, submitting, dispatch, children } = this.props; + const formItemLayout = { + labelCol: { + span: 5, + }, + wrapperCol: { + span: 19, + }, + }; + return ( + + +
    + + + + + + {children ? cloneElement(children, { + form, + formItemLayout, + data: stepFormData, + submitting, + dispatch, + }) : ( + + )} +
    +
    +
    + ); + } +} + +export default connect(state => ({ + stepFormData: state.form.step, + submitting: state.form.stepFormSubmitting, +}))(StepForm); diff --git a/src/routes/Forms/StepForm/style.less b/src/routes/Forms/StepForm/style.less new file mode 100644 index 00000000..16850fa5 --- /dev/null +++ b/src/routes/Forms/StepForm/style.less @@ -0,0 +1,59 @@ +@import "~antd/lib/style/themes/default.less"; + +.stepForm { + margin: 40px auto; + max-width: 500px; +} + +.stepFormText { + :global { + .ant-form-item-label, + .ant-form-item-control { + line-height: 22px; + } + } +} + +.result { + margin: 0 auto; + max-width: 520px; + padding: 32px 0; +} + +.desc { + h3 { + font-size: 14px; + margin: 8px 0; + color: @text-color-secondary; + } + h4 { + margin: 2px 0; + color: @text-color-secondary; + } + p { + margin-bottom: 16px; + } + padding: 0 34px; + color: @text-color-secondary; + font-size: 12px; +} + +.information { + line-height: 22px; + :global { + .ant-row:not(:last-child) { + margin-bottom: 24px; + } + } + .label { + font-weight: 500; + text-align: right; + padding-right: 8px; + } +} + +.money { + font-weight: 500; + font-size: 20px; + line-height: 22px; +} diff --git a/src/routes/Forms/TableForm.js b/src/routes/Forms/TableForm.js new file mode 100644 index 00000000..5be2baae --- /dev/null +++ b/src/routes/Forms/TableForm.js @@ -0,0 +1,200 @@ +import React, { PureComponent } from 'react'; +import { Table, Button, Input, message } from 'antd'; +import styles from './style.less'; + +export default class TableForm extends PureComponent { + constructor(props) { + super(props); + + this.state = { + data: props.value, + }; + } + componentWillReceiveProps(nextProps) { + if ('value' in nextProps) { + this.setState({ + data: nextProps.value, + }); + } + } + getRowByKey(key) { + return this.state.data.filter(item => item.key === key)[0]; + } + index = 0; + cacheOriginData = {}; + handleSubmit = (e) => { + e.preventDefault(); + this.props.form.validateFieldsAndScroll((err, values) => { + if (!err) { + this.props.dispatch({ + type: 'form/submit', + payload: values, + }); + } + }); + } + toggleEditable(e, key) { + e.preventDefault(); + const target = this.getRowByKey(key); + if (target) { + // 进入编辑状态时保存原始数据 + if (!target.editable) { + this.cacheOriginData[key] = { ...target }; + } + target.editable = !target.editable; + this.setState({ data: [...this.state.data] }); + } + } + remove(e, key) { + e.preventDefault(); + const newData = this.state.data.filter(item => item.key !== key); + this.setState({ data: newData }); + this.props.onChange(newData); + } + newMember = () => { + const newData = [...this.state.data]; + newData.push({ + key: `NEW_TEMP_ID_${this.index}`, + workId: '', + name: '', + department: '', + editable: true, + }); + this.index += 1; + this.setState({ data: newData }); + } + handleFieldChange(e, fieldName, key) { + const newData = [...this.state.data]; + const target = this.getRowByKey(key); + if (target) { + target[fieldName] = e.target.value; + this.setState({ data: newData }); + } + } + saveRow(e, key) { + const target = this.getRowByKey(key); + if (!target.workId || !target.name || !target.department) { + message.error('请填写完整成员信息。'); + return; + } + this.toggleEditable(e, key); + this.props.onChange(this.state.data); + } + cancel(e, key) { + e.preventDefault(); + const target = this.getRowByKey(key); + if (this.cacheOriginData[key]) { + Object.assign(target, this.cacheOriginData[key]); + target.editable = false; + delete this.cacheOriginData[key]; + } + this.setState({ data: [...this.state.data] }); + } + render() { + const columns = [{ + title: '成员姓名', + dataIndex: 'name', + key: 'name', + width: '20%', + render: (text, record) => { + if (record.editable) { + return ( + this.handleFieldChange(e, 'name', record.key)} + placeholder="成员姓名" + /> + ); + } + return text; + }, + }, { + title: '工号', + dataIndex: 'workId', + key: 'workId', + width: '20%', + render: (text, record) => { + if (record.editable) { + return ( + this.handleFieldChange(e, 'workId', record.key)} + placeholder="工号" + /> + ); + } + return text; + }, + }, { + title: '所属部门', + dataIndex: 'department', + key: 'department', + width: '40%', + render: (text, record) => { + if (record.editable) { + return ( + this.handleFieldChange(e, 'department', record.key)} + placeholder="所属部门" + /> + ); + } + return text; + }, + }, { + title: '操作', + key: 'action', + render: (text, record) => { + if (record.editable) { + if (record.key.indexOf('NEW_TEMP_ID_') >= 0) { + return ( + + this.saveRow(e, record.key)}>保存 + + this.remove(e, record.key)}>删除 + + ); + } + return ( + + this.saveRow(e, record.key)}>保存 + + this.cancel(e, record.key)}>取消 + + ); + } + return ( + + this.toggleEditable(e, record.key)}>编辑 + + this.remove(e, record.key)}>删除 + + ); + }, + }]; + + return ( +
    +
    { + return record.editable ? styles.editable : ''; + }} + /> + + + ); + } +} diff --git a/src/routes/Forms/style.less b/src/routes/Forms/style.less new file mode 100644 index 00000000..18f256b2 --- /dev/null +++ b/src/routes/Forms/style.less @@ -0,0 +1,78 @@ +@import "~antd/lib/style/themes/default.less"; + +.card { + margin-bottom: 24px; +} + +.heading { + font-size: 14px; + line-height: 22px; + margin: 0 0 16px 0; +} + +.steps { + max-width: 750px; + margin: 16px auto; +} + +.divider { + border: 0; + border-top: 1px solid @border-color-split; + height: 1px; + margin: 0 0 24px 0; +} + +.errorIcon { + cursor: pointer; + color: @error-color; + margin-right: 24px; + i { + margin-right: 4px; + } +} + +.errorPopover { + :global { + .ant-popover-inner-content { + padding: 0; + max-height: 400px; + overflow: auto; + min-width: 240px; + } + } +} + +.errorListItem { + list-style: none; + border-bottom: 1px solid @border-color-split; + padding: 8px 24px; + cursor: pointer; + transition: all .3s; + &:hover { + background: @primary-1; + } + &:last-child { + border: 0; + } + .errorIcon { + color: @error-color; + float: left; + margin-top: 4px; + margin-right: 8px; + padding-bottom: 22px; + } + .errorField { + font-size: 12px; + color: @text-color-secondary; + margin-top: 4px; + } +} + +// 避免表格编辑模式切换时抖动 +.editable { + td { + transition: none !important; + padding-top: 12.5px !important; + padding-bottom: 12.5px !important; + } +} diff --git a/src/routes/List/BasicList.js b/src/routes/List/BasicList.js new file mode 100644 index 00000000..90063e51 --- /dev/null +++ b/src/routes/List/BasicList.js @@ -0,0 +1,145 @@ +import React, { PureComponent } from 'react'; +import moment from 'moment'; +import { connect } from 'dva'; +import { List, Card, Row, Col, Radio, Input, Progress, Button, Icon, Dropdown, Menu, Avatar } from 'antd'; + +import PageHeaderLayout from '../../layouts/PageHeaderLayout'; + +import styles from './BasicList.less'; + +const RadioButton = Radio.Button; +const RadioGroup = Radio.Group; +const Search = Input.Search; + +@connect(state => ({ + list: state.list, +})) +export default class BasicList extends PureComponent { + componentDidMount() { + this.props.dispatch({ + type: 'list/fetch', + payload: { + count: 5, + }, + }); + } + + render() { + const { list: { list, loading } } = this.props; + + const Info = ({ title, value, bordered }) => ( +
    + {title} +

    {value}

    + {bordered && } +
    + ); + + const extraContent = ( +
    + + 全部 + 进行中 + 等待中 + + ({})} + /> +
    + ); + + const paginationProps = { + showSizeChanger: true, + showQuickJumper: true, + pageSize: 5, + total: 50, + }; + + const ListContent = ({ data: { owner, createdAt, percent, status } }) => ( +
    +
    + Owner +

    {owner}

    +
    +
    + 开始时间 +

    {moment(createdAt).format('YYYY-MM-DD hh:mm')}

    +
    +
    + +
    +
    + ); + + const menu = ( + + + 编辑 + + + 删除 + + + ); + + const MoreBtn = () => ( + + + 更多 + + + ); + + return ( + +
    + + +
    + + + + + + + + + + + + + + + { + list && list.map(item => ( + 编辑, ]} + > + } + title={{item.title}} + description={item.subDescription} + /> + + + )) + } + + + + + ); + } +} diff --git a/src/routes/List/BasicList.less b/src/routes/List/BasicList.less new file mode 100644 index 00000000..e530c2ff --- /dev/null +++ b/src/routes/List/BasicList.less @@ -0,0 +1,103 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../utils/utils.less"; + +.standardList { + :global { + .ant-list-pagination { + text-align: right; + } + } + .headerInfo { + position: relative; + text-align: center; + & > span { + color: @text-color-secondary; + display: inline-block; + font-size: @font-size-base; + line-height: 22px; + margin-bottom: 4px; + } + & > p { + color: @heading-color; + font-size: 24px; + line-height: 32px; + } + & > em { + background-color: @border-color-split; + position: absolute; + height: 56px; + width: 1px; + top: 0; + right: 0; + } + } + .listContent { + margin-left: 24px; + font-size: 0; + & > div { + color: @text-color-secondary; + display: inline-block; + font-size: @font-size-base; + margin-left: 32px; + & > span { + line-height: 20px; + } + & > p { + margin-top: 4px; + line-height: 22px; + } + } + & > div:last-child { + position: relative; + top: -16px; + width: 188px; + } + } + .extraContentSearch { + margin-left: 16px; + width: 272px; + } +} + +@media screen and (max-width: @screen-sm) { + .standardList { + .extraContentSearch { + margin-left: 0; + width: 100%; + } + .headerInfo { + margin-bottom: 16px; + & > em { + display: none; + } + } + } +} + +@media screen and (max-width: @screen-md) { + .standardList { + .listContent { + & > div { + display: block; + } + & > div:last-child { + top: 0; + width: 100%; + } + } + } +} + +@media screen and (max-width: @screen-lg) and (min-width: @screen-md) { + .standardList { + .listContent { + & > div { + display: block; + } + & > div:last-child { + top: 0; + width: 100%; + } + } + } +} diff --git a/src/routes/List/CardList.js b/src/routes/List/CardList.js new file mode 100644 index 00000000..f406fca4 --- /dev/null +++ b/src/routes/List/CardList.js @@ -0,0 +1,91 @@ +import React, { PureComponent } from 'react'; +import { connect } from 'dva'; +import { Row, Col, Card, Avatar, Spin, Button, Icon } from 'antd'; + +import PageHeaderLayout from '../../layouts/PageHeaderLayout'; + +import styles from './CardList.less'; + +@connect(state => ({ + list: state.list, +})) +export default class CardList extends PureComponent { + componentDidMount() { + this.props.dispatch({ + type: 'list/fetch', + payload: { + count: 8, + }, + }); + } + + render() { + const { list: { list, loading } } = this.props; + + const content = ( +
    +

    段落示意:蚂蚁金服务设计平台-design.alipay.com,用最小的工作量,无缝接入蚂蚁金服生态, + 提供跨越设计与开发的体验解决方案。

    + +
    + ); + + const extraContent = ( +
    + 这是一个标题 +
    + ); + + return ( + +
    + { + loading ? + + : + +
    + + + { + list && list.map(item => ( + + 操作一, 操作二]} + > + } + title={item.title} + description={( +

    + {item.description} +

    + )} + /> +
    + + )) + } + + } + + + ); + } +} diff --git a/src/routes/List/CardList.less b/src/routes/List/CardList.less new file mode 100644 index 00000000..664da006 --- /dev/null +++ b/src/routes/List/CardList.less @@ -0,0 +1,71 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../utils/utils.less"; + +.cardList { + :global { + .ant-card-meta-content { + margin-top: 0; + } + } +} + +.extraImg { + margin-top: -60px; + text-align: center; +} + +.newButton { + background-color: transparent; + border-color: @border-color-base; + color: @text-color-secondary; + width: 100%; + height: 178px; + &:hover { + background-color: transparent; + } +} + +.cardDescription { + .textOverflowMulti(); +} + +.pageHeaderContent { + position: relative; +} + +.contentLink { + margin-top: 16px; + a { + margin-right: 32px; + } + img { + vertical-align: middle; + margin-right: 8px; + } +} + +@media screen and (max-width: @screen-lg) { + .contentLink { + a { + margin-right: 16px; + } + } +} + +@media screen and (max-width: @screen-sm) { + .pageHeaderContent { + padding-bottom: 30px; + } + .contentLink { + position: absolute; + left: 0; + bottom: -4px; + width: 1000px; + a { + margin-right: 16px; + } + img { + margin-right: 4px; + } + } +} diff --git a/src/routes/List/CoverCardList.js b/src/routes/List/CoverCardList.js new file mode 100644 index 00000000..00cb75f0 --- /dev/null +++ b/src/routes/List/CoverCardList.js @@ -0,0 +1,206 @@ +import React, { PureComponent } from 'react'; +import moment from 'moment'; +import { connect } from 'dva'; +import { Row, Col, Form, Card, Select, Spin } from 'antd'; + +import PageHeaderLayout from '../../layouts/PageHeaderLayout'; +import StandardFormRow from '../../components/StandardFormRow'; +import TagSelect from '../../components/TagSelect'; +import AvatarList from '../../components/AvatarList'; +import SearchInput from '../../components/SearchInput'; + +import styles from './CoverCardList.less'; + +const Option = Select.Option; +const FormItem = Form.Item; +const TagOption = TagSelect.Option; +const TagExpand = TagSelect.Expand; + +/* eslint react/no-array-index-key: 0 */ +@Form.create() +@connect(state => ({ + list: state.list, +})) +export default class CoverCardList extends PureComponent { + componentDidMount() { + this.props.dispatch({ + type: 'list/fetch', + payload: { + count: 8, + }, + }); + } + + handleFormSubmit = () => { + const { form, dispatch } = this.props; + // setTimeout 用于保证获取表单值是在所有表单字段更新完毕的时候 + setTimeout(() => { + form.validateFields((err) => { + if (!err) { + // eslint-disable-next-line + dispatch({ + type: 'list/fetch', + payload: { + count: 8, + }, + }); + } + }); + }, 0); + } + + render() { + const { list: { list = [], loading }, form } = this.props; + const { getFieldDecorator } = form; + + const cardList = list ? ( + + { + list.map(item => ( +
    + } + > + +
    + {moment(item.updatedAt).fromNow()} +
    + + { + item.members.map((member, i) => ( + + )) + } + +
    +
    +
    + + )) + } + + ) : null; + + const tabList = [ + { + key: 'docs', + tab: '文章', + }, + { + key: 'app', + tab: '应用', + }, + { + key: 'project', + tab: '项目', + }, + ]; + + const pageHeaderContent = ( +
    + +
    + ); + + const formItemLayout = { + wrapperCol: { + xs: { span: 24 }, + sm: { span: 16 }, + }, + }; + + return ( + +
    + +
    + + + {getFieldDecorator('category')( + + 类目一 + 类目二 + 类目三 + 类目四 + + 类目五 + 类目六 + + + )} + + + + +
    + + {getFieldDecorator('author', {})( + + )} + + + + + {getFieldDecorator('rate', {})( + + )} + + + + + + + { + loading && (list.length > 0) && + {cardList} + + } + { + loading && (list.length < 1) &&
    + } + { + !loading && cardList + } + + + ); + } +} diff --git a/src/routes/List/CoverCardList.less b/src/routes/List/CoverCardList.less new file mode 100644 index 00000000..d4c7eade --- /dev/null +++ b/src/routes/List/CoverCardList.less @@ -0,0 +1,24 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../utils/utils.less"; + +.coverCardList { + :global { + .ant-card-meta-description { + font-size: 12px; + } + } + .cardItemContent { + display: flex; + margin-top: 8px; + line-height: 20px; + height: 20px; + span { + flex: 1; + font-size: 12px; + color: @disabled-color; + } + .avatarList { + flex: 0 1 auto; + } + } +} diff --git a/src/routes/List/FilterCardList.js b/src/routes/List/FilterCardList.js new file mode 100644 index 00000000..6f26beb7 --- /dev/null +++ b/src/routes/List/FilterCardList.js @@ -0,0 +1,211 @@ +import React, { PureComponent } from 'react'; +import numeral from 'numeral'; +import { connect } from 'dva'; +import { Row, Col, Form, Card, Select, Spin, Icon, Avatar } from 'antd'; + +import PageHeaderLayout from '../../layouts/PageHeaderLayout'; +import StandardFormRow from '../../components/StandardFormRow'; +import TagSelect from '../../components/TagSelect'; +import SearchInput from '../../components/SearchInput'; + +import styles from './FilterCardList.less'; + +const Option = Select.Option; +const FormItem = Form.Item; +const TagOption = TagSelect.Option; +const TagExpand = TagSelect.Expand; + +const formatWan = (val) => { + const v = val * 1; + if (!v || isNaN(v)) return ''; + + let result = val; + if (val > 10000) { + result = Math.floor(val / 10000); + result = {result}; + } + return result; +}; + +/* eslint react/no-array-index-key: 0 */ +@Form.create() +@connect(state => ({ + list: state.list, +})) +export default class FilterCardList extends PureComponent { + componentDidMount() { + this.props.dispatch({ + type: 'list/fetch', + payload: { + count: 8, + }, + }); + } + + handleFormSubmit = () => { + const { form, dispatch } = this.props; + // setTimeout 用于保证获取表单值是在所有表单字段更新完毕的时候 + setTimeout(() => { + form.validateFields((err) => { + if (!err) { + // eslint-disable-next-line + dispatch({ + type: 'list/fetch', + payload: { + count: 8, + }, + }); + } + }); + }, 0); + } + + render() { + const { list: { list, loading }, form } = this.props; + const { getFieldDecorator } = form; + + const tabList = [ + { + key: 'docs', + tab: '文章', + }, + { + key: 'apps', + tab: '应用', + default: true, + }, + { + key: 'projects', + tab: '项目', + }, + ]; + + const CardInfo = ({ activeUser, newUser }) => ( +
    +
    +

    活跃用户

    +

    {activeUser}

    + +
    +
    +

    新增用户

    +

    {newUser}

    +
    +
    + ); + + const pageHeaderContent = ( +
    + +
    + ); + + const formItemLayout = { + wrapperCol: { + xs: { span: 24 }, + sm: { span: 16 }, + }, + }; + + return ( + +
    + +
    + + + {getFieldDecorator('category')( + + 类目一 + 类目二 + 类目三 + 类目四 + + 类目五 + 类目六 + + + )} + + + + +
    + + {getFieldDecorator('author', {})( + + )} + + + + + {getFieldDecorator('rate', {})( + + )} + + + + + + + + { + loading && + } + { + !loading && list && list.map(item => ( + + , , , ]} + > + } + title={item.title} + /> +
    + +
    +
    + + )) + } + + + + ); + } +} diff --git a/src/routes/List/FilterCardList.less b/src/routes/List/FilterCardList.less new file mode 100644 index 00000000..679cec45 --- /dev/null +++ b/src/routes/List/FilterCardList.less @@ -0,0 +1,54 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../utils/utils.less"; + +.filterCardList { + :global { + .ant-card-meta-title { + position: relative; + top: 8px; + } + .ant-card-meta-content { + margin-top: 0; + } + } + .cardInfo { + .clearfix(); + border: 1px solid @border-color-base; + border-radius: @border-radius-base; + padding: 8px 0; + margin-top: 16px; + width: 100%; + & > div { + position: relative; + text-align: center; + float: left; + width: 50%; + & > span { + background-color: @border-color-split; + position: absolute; + top: 0; + right: 0; + width: 1px; + height: 44px; + } + p { + color: @text-color-secondary; + line-height: 32px; + font-size: 24px; + } + p:first-child { + font-size: 12px; + line-height: 20px; + } + } + } +} + +.wan { + position: relative; + top: -2px; + font-size: @font-size-base; + font-style: normal; + line-height: 20px; + margin-left: 2px; +} diff --git a/src/routes/List/SearchList.js b/src/routes/List/SearchList.js new file mode 100644 index 00000000..682c1573 --- /dev/null +++ b/src/routes/List/SearchList.js @@ -0,0 +1,274 @@ +import React, { Component } from 'react'; +import moment from 'moment'; +import { connect } from 'dva'; +import { Form, Card, Select, List, Tag, Icon, Avatar, Row, Col } from 'antd'; + +import PageHeaderLayout from '../../layouts/PageHeaderLayout'; +import StandardFormRow from '../../components/StandardFormRow'; +import TagSelect from '../../components/TagSelect'; +import SearchInput from '../../components/SearchInput'; +import styles from './SearchList.less'; + +const Option = Select.Option; +const FormItem = Form.Item; +const TagOption = TagSelect.Option; +const TagExpand = TagSelect.Expand; + +@Form.create() +@connect(state => ({ + list: state.list, +})) +export default class SearchList extends Component { + state = { + count: 3, + showLoadMore: true, + loadingMore: false, + } + + componentDidMount() { + const { count } = this.state; + this.props.dispatch({ + type: 'list/fetch', + payload: { + count, + }, + }); + } + + setOwner = () => { + const { form } = this.props; + form.setFieldsValue({ + owner: ['wzj'], + }); + } + + handleLoadMore = () => { + const { count } = this.state; + const nextCount = count + 5; + + this.setState({ + count: nextCount, + loadingMore: true, + }); + this.props.dispatch({ + type: 'list/fetch', + payload: { + count: nextCount, + }, + callback: () => { + this.setState({ + loadingMore: false, + }); + + // fack count + if (nextCount < 10) { + this.setState({ + showLoadMore: false, + }); + } + }, + }); + } + + render() { + const { showLoadMore, loadingMore } = this.state; + const { form, list: { list } } = this.props; + const { getFieldDecorator } = form; + + const owners = [ + { + id: 'wzj', + name: '我自己', + }, + { + id: 'wjh', + name: '吴家豪', + }, + { + id: 'zxx', + name: '周星星', + }, + { + id: 'zly', + name: '赵丽颖', + }, + { + id: 'ym', + name: '姚明', + }, + ]; + + const tabList = [ + { + key: 'docs', + tab: '文章', + }, + { + key: 'app', + tab: '应用', + }, + { + key: 'project', + tab: '项目', + }, + ]; + + const IconText = ({ type, text }) => ( + + + {text} + + ); + + const ListContent = ({ data: { content, updatedAt, avatar, owner, href } }) => ( +
    +

    {content}

    +
    + {owner} 发布在 {href} + {moment(updatedAt).format('YYYY-MM-DD hh:mm')} +
    +
    + ); + + const pageHeaderContent = ( +
    + +
    + ); + + const formItemLayout = { + wrapperCol: { + xs: { span: 24 }, + sm: { span: 16 }, + }, + }; + + return ( + +
    + +
    + + + {getFieldDecorator('category')( + + 类目一 + 类目二 + 类目三 + 类目四 + + 类目五 + 类目六 + + + )} + + + + +
    + + {getFieldDecorator('owner', { + initialValue: ['wjh', 'zxx'], + })( + + )} + 只看自己的 + + + + + + + + + {getFieldDecorator('user', {})( + + )} + + + + + {getFieldDecorator('rate', {})( + + {getFieldDecorator('rate', {})( + + )} + + )} + + + + + + + + 0) && showLoadMore} + onLoadMore={this.handleLoadMore} + itemLayout="vertical" + > + { + list && list.map(item => ( + , , ]} + extra={
    } + > + {item.title}} + description={Ant Design设计语言蚂蚁金服} + /> + + + )) + } + + +
    + + ); + } +} diff --git a/src/routes/List/SearchList.less b/src/routes/List/SearchList.less new file mode 100644 index 00000000..87ef3d77 --- /dev/null +++ b/src/routes/List/SearchList.less @@ -0,0 +1,45 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../utils/utils.less"; + +.listContent { + p { + line-height: 24px; + } + & > div { + color: @text-color-secondary; + margin-top: 16px; + height: 22px; + line-height: 22px; + img { + margin-right: 16px; + } + & > span { + vertical-align: top; + margin-right: 16px; + width: 20px; + height: 20px; + } + & > em { + color: @disabled-color; + font-style: normal; + margin-left: 24px; + } + a { + color: @text-color-secondary; + &:hover { + color: @primary-color; + } + } + } +} +.listItemExtra { + width: 272px; + height: 1px; +} + +@media screen and (max-width: @screen-lg) { + .listItemExtra { + width: 0; + height: 1px; + } +} diff --git a/src/routes/List/TableList.js b/src/routes/List/TableList.js new file mode 100644 index 00000000..a0d9f4f8 --- /dev/null +++ b/src/routes/List/TableList.js @@ -0,0 +1,265 @@ +import React, { PureComponent } from 'react'; +import { connect } from 'dva'; +import { Card, Row, Col, Form, Input, Select, Icon, Button, Dropdown, Menu, InputNumber, DatePicker, Modal, message } from 'antd'; +import StandardTable from '../../components/StandardTable'; +import PageHeaderLayout from '../../layouts/PageHeaderLayout'; + +import styles from './TableList.less'; + +const FormItem = Form.Item; +const Option = Select.Option; +const getValue = obj => Object.keys(obj).map(key => obj[key]).join(','); + +@connect(state => ({ + rule: state.rule, +})) +@Form.create() +export default class TableList extends PureComponent { + state = { + addInputValue: '', + modalVisible: false, + expandForm: false, + selectedRows: [], + formValues: {}, + }; + + componentDidMount() { + const { dispatch } = this.props; + dispatch({ + type: 'rule/fetch', + }); + } + + handleStandardTableChange = (pagination, filtersArg, sorter) => { + const { dispatch } = this.props; + const { formValues } = this.state; + + const filters = Object.keys(filtersArg).reduce((obj, key) => { + const newObj = { ...obj }; + newObj[key] = getValue(filtersArg[key]); + return newObj; + }, {}); + + const params = { + currentPage: pagination.current, + pageSize: pagination.pageSize, + ...formValues, + ...filters, + }; + if (sorter.field) { + params.sorter = `${sorter.field}_${sorter.order}`; + } + + dispatch({ + type: 'rule/fetch', + payload: params, + }); + } + + handleFormReset = () => { + const { form, dispatch } = this.props; + form.resetFields(); + dispatch({ + type: 'rule/fetch', + payload: {}, + }); + } + + toggleForm = () => { + this.setState({ + expandForm: !this.state.expandForm, + }); + } + + handleMenuClick = (e) => { + const { dispatch } = this.props; + const { selectedRows } = this.state; + + if (!selectedRows) return; + + switch (e.key) { + case 'remove': + dispatch({ + type: 'rule/remove', + payload: { + no: selectedRows.map(row => row.no).join(','), + }, + callback: () => { + this.setState({ + selectedRows: [], + }); + }, + }); + break; + default: + break; + } + } + + handleSelectRows = (rows) => { + this.setState({ + selectedRows: rows, + }); + } + + handleSearch = (e) => { + e.preventDefault(); + + const { dispatch, form } = this.props; + + form.validateFields((err, fieldsValue) => { + if (err) return; + + const values = { + ...fieldsValue, + updatedAt: fieldsValue.updatedAt && fieldsValue.updatedAt.valueOf(), + }; + + this.setState({ + formValues: values, + }); + + dispatch({ + type: 'rule/fetch', + payload: values, + }); + }); + } + + handleModalVisible = (flag) => { + this.setState({ + modalVisible: !!flag, + }); + } + + handleAddInput = (e) => { + this.setState({ + addInputValue: e.target.value, + }); + } + + handleAdd = () => { + this.props.dispatch({ + type: 'rule/add', + payload: { + description: this.state.addInputValue, + }, + }); + + message.success('添加成功'); + this.setState({ + modalVisible: false, + }); + } + + render() { + const { rule: { loading: ruleLoading, data }, form: { getFieldDecorator } } = this.props; + const { selectedRows, modalVisible, addInputValue } = this.state; + + const formItemLayout = { + labelCol: { span: 5 }, + wrapperCol: { span: 19 }, + }; + + const menu = ( + + 删除 + { + selectedRows.length > 1 && 批量审批 + } + + ); + + return ( + + +
    +
    +
    + +
    + + {getFieldDecorator('no')( + + )} + + + + + {getFieldDecorator('status')( + + )} + + + + + + + { + this.state.expandForm && + + + {getFieldDecorator('updatedAt')( + + )} + + + + + {getFieldDecorator('callNo')( + } + placeholder="请输入" + /> + )} + + + + } + + +
    + + + + + +
    + + + + this.handleModalVisible()} + > + + + + + + ); + } +} diff --git a/src/routes/List/TableList.less b/src/routes/List/TableList.less new file mode 100644 index 00000000..0af787a6 --- /dev/null +++ b/src/routes/List/TableList.less @@ -0,0 +1,23 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../utils/utils.less"; + +.tableList { + .tableListOperator { + margin-bottom: 16px; + button { + margin-right: 8px; + } + } +} + +.formButton { + margin-left: 40px; + position: relative; + top: 2px; +} + +@media screen and (max-width: @screen-md) { + .formButton { + margin-left: 0; + } +} diff --git a/src/routes/Profile.js b/src/routes/Profile.js new file mode 100644 index 00000000..bf0a354b --- /dev/null +++ b/src/routes/Profile.js @@ -0,0 +1,255 @@ +import React, { Component } from 'react'; +import { connect } from 'dva'; +import { Button, Menu, Dropdown, Icon, Row, Col, Steps, Card, Popover, Badge, Table, Tooltip } from 'antd'; +import PageHeaderLayout from '../layouts/PageHeaderLayout'; +import DescriptionList from '../components/DescriptionList'; +import styles from './Profile.less'; + +const { Step } = Steps; +const { Description } = DescriptionList; + +const menu = ( + + 选项一 + 选项二 + 选项三 + +); + +const action = ( +
    + + + + + +
    +); + +const extra = ( + +
    +
    状态
    +
    待审批
    + +
    +
    订单金额
    +
    ¥ 568.08
    + + +); + +const description = ( + + 曲丽丽 + 12421 + 2017-07-07 + 2017-07-07 ~ 2017-08-08 + 修改公司地址:浙江省杭州市西湖区工专路 + +); + +const tabList = [{ + key: 'detail', + tab: '详情', +}, { + key: 'rule', + tab: '规则', +}]; + +const desc1 = ( +
    +
    + 曲丽丽 +
    +
    2016-12-12 12:32
    +
    +); + +const desc2 = ( +
    +
    + 周毛毛 +
    + +
    +); + +const popoverContent = ( +
    + 吴加号 + + + +

    耗时:2小时25分钟

    +
    +); + +const customDot = (dot, { status }) => (status === 'process' ? + + {dot} + + : dot +); + +const operationTabList = [{ + key: 'tab1', + tab: '操作日志一', +}, { + key: 'tab2', + tab: '操作日志二', +}, { + key: 'tab3', + tab: '操作日志三', +}]; + +const columns = [{ + title: '操作类型', + dataIndex: 'type', + key: 'type', +}, { + title: '操作人', + dataIndex: 'name', + key: 'name', +}, { + title: '执行结果', + dataIndex: 'status', + key: 'status', + render: text => ( + text === 'agree' ? : + ), +}, { + title: '操作时间', + dataIndex: 'updatedAt', + key: 'updatedAt', +}, { + title: '备注', + dataIndex: 'memo', + key: 'memo', +}]; + +@connect(state => ({ + profile: state.profile, +})) +export default class Profile extends Component { + state = { + operationkey: 'tab1', + } + + componentDidMount() { + const { dispatch } = this.props; + dispatch({ + type: 'profile/fetch', + }); + } + + onOperationTabChange = (key) => { + this.setState({ operationkey: key }); + } + + render() { + const { profile } = this.props; + const { loading, operation1, operation2, operation3 } = profile; + const contentList = { + tab1:
    , + tab2:
    , + tab3:
    , + }; + + return ( + } + action={action} + content={description} + extraContent={extra} + tabList={tabList} + > + + + + + + + + + + + 付小小 + 32943898021309809423 + 3321944288191034921 + 18322193472 + 曲丽丽 18100000000 浙江省杭州市西湖区黄姑山路工专路交叉路口 + + + 725 + 2017-08-08 + + 某某数据 + + + + + } + > + 725 + + 2017-08-08 + + + + 林东东 + 1234567 + XX公司 - YY部 + 2017-08-08 + 这段描述很长很长很长很长很长很长很长很长很长很长很长很长很长很长... + +
    + + + Citrullus lanatus (Thunb.) Matsum. et Nakai一年生蔓生藤本;茎、枝粗壮,具明显的棱。卷须较粗.. + + +
    + + 付小小 + 1234568 + + + + +
    + 暂无数据 +
    +
    + + {contentList[this.state.operationkey]} + + + ); + } +} diff --git a/src/routes/Profile.less b/src/routes/Profile.less new file mode 100644 index 00000000..f33e342f --- /dev/null +++ b/src/routes/Profile.less @@ -0,0 +1,27 @@ +@import "~antd/lib/style/themes/default.less"; + +.tabsCard { + margin-bottom: 24px; +} + +.noData { + color: @disabled-color; + text-align: center; + line-height: 64px; +} + +.heading { + color: @heading-color; + font-size: 20px; +} + +.textSecondary { + color: @text-color-secondary; +} + +.divider { + border: 0; + border-top: 1px solid @border-color-split; + height: 1px; + margin: 0 0 24px 0; +} diff --git a/src/routes/Result/Error.js b/src/routes/Result/Error.js new file mode 100644 index 00000000..a047205d --- /dev/null +++ b/src/routes/Result/Error.js @@ -0,0 +1,37 @@ +import React from 'react'; +import { Button, Icon, Card } from 'antd'; +import Result from '../../components/Result'; +import PageHeaderLayout from '../../layouts/PageHeaderLayout'; + +const extra = ( +
    +
    + 您提交的内容有如下错误: +
    +
    + 您的账户已被冻结 + 立即解冻 +
    +
    + 您的账户还不具备申请资格 + 立即升级 +
    +
    +); + +const actions = ; + +export default () => ( + + + + + +); diff --git a/src/routes/Result/Success.js b/src/routes/Result/Success.js new file mode 100644 index 00000000..a1fb6e51 --- /dev/null +++ b/src/routes/Result/Success.js @@ -0,0 +1,69 @@ +import React from 'react'; +import { Button, Row, Col, Icon, Steps, Card } from 'antd'; +import Result from '../../components/Result'; +import PageHeaderLayout from '../../layouts/PageHeaderLayout'; + +const Step = Steps.Step; + +const desc1 = ( +
    +
    + 曲丽丽 +
    +
    2016-12-12 12:32
    +
    +); + +const desc2 = ( +
    +
    + 周毛毛 +
    + +
    +); + +const extra = ( +
    +
    + 项目名称 +
    + +
    项目 ID:23421 + 负责人:曲丽丽 + 生效时间:2016-12-12 ~ 2017-12-12 + + + + + + + + +); + +const actions = ( +
    + + + +
    +); + +export default () => ( + + + + + +); diff --git a/src/routes/User/Login.js b/src/routes/User/Login.js new file mode 100644 index 00000000..7e424f44 --- /dev/null +++ b/src/routes/User/Login.js @@ -0,0 +1,172 @@ +import React, { Component } from 'react'; +import { connect } from 'dva'; +import { routerRedux, Link } from 'dva/router'; +import { Form, Input, Tabs, Button, Icon, Checkbox, Row, Col, Alert } from 'antd'; +import styles from './Login.less'; + +const FormItem = Form.Item; +const TabPane = Tabs.TabPane; + +@connect(state => ({ + login: state.login, +})) +@Form.create() +export default class Login extends Component { + state = { + count: 0, + type: 'account', + } + + componentWillReceiveProps(nextProps) { + if (nextProps.login.status === 'ok') { + this.props.dispatch(routerRedux.push('/')); + } + } + + componentWillUnmount() { + clearInterval(this.interval); + } + + onSwitch = (key) => { + this.setState({ + type: key, + }); + } + + onGetCaptcha = () => { + let count = 59; + this.setState({ count }); + this.interval = setInterval(() => { + count -= 1; + this.setState({ count }); + if (count === 0) { + clearInterval(this.interval); + } + }, 1000); + } + + handleSubmit = (e) => { + e.preventDefault(); + const { type } = this.state; + this.props.form.validateFields({ force: true }, + (err, values) => { + if (!err) { + this.props.dispatch({ + type: `login/${type}Submit`, + payload: values, + }); + } + } + ); + } + + msg = (message) => { + return ; + } + + render() { + const { form, login } = this.props; + const { getFieldDecorator } = form; + const { count, type } = this.state; + return ( +
    +
    + + + {login.status === 'error' && login.type === 'account' && this.msg('账户或密码错误')} + + {getFieldDecorator('userName', { + rules: [{ + required: type === 'account', message: '请输入账户名!', + }], + })( + } + placeholder="账户" + /> + )} + + + {getFieldDecorator('password', { + rules: [{ + required: type === 'account', message: '请输入密码!', + }], + })( + } + type="password" + placeholder="密码" + /> + )} + + + + {login.status === 'error' && login.type === 'mobile' && this.msg('验证码错误')} + + {getFieldDecorator('mobile', { + rules: [{ + required: type === 'mobile', message: '请输入手机号!', + }, { + pattern: /^1\d{10}$/, message: '手机号格式错误!', + }], + })( + } + placeholder="手机号" + /> + )} + + + +
    + {getFieldDecorator('captcha', { + rules: [{ + required: type === 'mobile', message: '请输入验证码!', + }], + })( + } + placeholder="验证码" + /> + )} + + + + + + + + + + {getFieldDecorator('remember', { + valuePropName: 'checked', + initialValue: true, + })( + 自动登录 + )} + 忘记密码 + + + +
    + 其他登录方式 + {/* 需要加到 Icon 中 */} + + + + 注册账户 +
    + + ); + } +} diff --git a/src/routes/User/Login.less b/src/routes/User/Login.less new file mode 100644 index 00000000..1ebb1b29 --- /dev/null +++ b/src/routes/User/Login.less @@ -0,0 +1,79 @@ +@import "~antd/lib/style/themes/default.less"; + +.main { + width: 368px; + margin: 0 auto; + + :global { + .ant-tabs .ant-tabs-bar { + border-bottom: 0; + margin-bottom: 24px; + text-align: center; + } + + .ant-form-item { + margin-bottom: 16px; + } + } + + .getCaptcha { + display: block; + width: 100%; + } + + .additional { + text-align: left; + + .forgot { + float: right; + } + + .submit { + width: 100%; + margin-top: 16px; + } + } + + .iconAlipay, .iconTaobao, .iconWeibo { + display: inline-block; + width: 24px; + height: 24px; + background: url('https://gw.alipayobjects.com/zos/rmsportal/itDzjUnkelhQNsycranf.svg'); + margin-left: 16px; + vertical-align: middle; + cursor: pointer; + } + + .iconAlipay { + background-position: -24px 0; + + &:hover { + background-position: 0 0; + } + } + + .iconTaobao { + background-position: -24px -24px; + + &:hover { + background-position: 0 -24px; + } + } + + .iconWeibo { + background-position: -24px -48px; + + &:hover { + background-position: 0 -48px; + } + } + + .other { + text-align: left; + margin-top: 32px; + + .register { + float: right; + } + } +} diff --git a/src/routes/User/Register.js b/src/routes/User/Register.js new file mode 100644 index 00000000..93fa8e6a --- /dev/null +++ b/src/routes/User/Register.js @@ -0,0 +1,261 @@ +import React, { Component } from 'react'; +import { connect } from 'dva'; +import { routerRedux, Link } from 'dva/router'; +import { Form, Input, Button, Select, Row, Col, Popover, Progress } from 'antd'; +import styles from './Register.less'; + +const FormItem = Form.Item; +const Option = Select.Option; +const InputGroup = Input.Group; + +const passwordStatusMap = { + ok:

    强度:强

    , + pass:

    强度:中

    , + pool:

    强度:太短

    , +}; + +const passwordProgressMap = { + ok: 'success', + pass: 'normal', + pool: 'exception', +}; + +@connect(state => ({ + register: state.register, +})) +@Form.create() +export default class Register extends Component { + state = { + count: 0, + confirmDirty: false, + visible: false, + help: '', + } + + componentWillReceiveProps(nextProps) { + if (nextProps.register.status === 'ok') { + this.props.dispatch(routerRedux.push('/')); + } + } + + componentWillUnmount() { + clearInterval(this.interval); + } + + onGetCaptcha = () => { + let count = 59; + this.setState({ count }); + this.interval = setInterval(() => { + count -= 1; + this.setState({ count }); + if (count === 0) { + clearInterval(this.interval); + } + }, 1000); + } + + getPasswordStatus = () => { + const form = this.props.form; + const value = form.getFieldValue('password'); + if (value && value.length > 9) { + return 'ok'; + } + if (value && value.length > 5) { + return 'pass'; + } + return 'pool'; + } + + handleSubmit = (e) => { + e.preventDefault(); + this.props.form.validateFields({ force: true }, + (err, values) => { + if (!err) { + this.props.dispatch({ + type: 'register/submit', + payload: values, + }); + } + } + ); + } + + handleConfirmBlur = (e) => { + const value = e.target.value; + this.setState({ confirmDirty: this.state.confirmDirty || !!value }); + } + + checkConfirm = (rule, value, callback) => { + const form = this.props.form; + if (value && value !== form.getFieldValue('password')) { + callback('两次输入的密码不匹配!'); + } else { + callback(); + } + } + + checkPassword = (rule, value, callback) => { + if (!value) { + this.setState({ + help: '请输入密码!', + visible: !!value, + }); + callback('error'); + } else { + this.setState({ + help: '', + }); + if (!this.state.visible) { + this.setState({ + visible: !!value, + }); + } + if (value.length < 6) { + callback('error'); + } else { + const form = this.props.form; + if (value && this.state.confirmDirty) { + form.validateFields(['confirm'], { force: true }); + } + callback(); + } + } + } + + renderPasswordProgress = () => { + const form = this.props.form; + const value = form.getFieldValue('password'); + const passwordStatus = this.getPasswordStatus(); + return value && value.length ? +
    + 100 ? 100 : value.length * 10} + showInfo={false} + /> +
    : null; + } + + render() { + const { form, register } = this.props; + const { getFieldDecorator } = form; + const { count } = this.state; + return ( +
    +

    注册

    +
    + + {getFieldDecorator('mail', { + rules: [{ + required: true, message: '请输入邮箱地址!', + }, { + type: 'email', message: '邮箱地址格式错误!', + }], + })( + + )} + + + + {passwordStatusMap[this.getPasswordStatus()]} + {this.renderPasswordProgress()} +

    请至少输入 6 个字符。请不要使用容易被猜到的密码。

    +
    + } + overlayStyle={{ width: 240 }} + placement="right" + visible={this.state.visible} + > + {getFieldDecorator('password', { + rules: [{ + validator: this.checkPassword, + }], + })( + + )} + + + + {getFieldDecorator('confirm', { + rules: [{ + required: true, message: '请确认密码!', + }, { + validator: this.checkConfirm, + }], + })( + + )} + + + + + {getFieldDecorator('prefix', { + initialValue: '86', + })( + + )} + + + {getFieldDecorator('mobile', { + rules: [{ + required: true, message: '请输入手机号!', + }, { + pattern: /^1\d{10}$/, message: '手机号格式错误!', + }], + })( + + )} + + + + + +
    + {getFieldDecorator('captcha', { + rules: [{ + required: true, message: '请输入验证码!', + }], + })( + + )} + + + + + + + + + 使用已有账户登录 + + + + ); + } +} diff --git a/src/routes/User/Register.less b/src/routes/User/Register.less new file mode 100644 index 00000000..d538f738 --- /dev/null +++ b/src/routes/User/Register.less @@ -0,0 +1,84 @@ +@import "~antd/lib/style/themes/default.less"; + +.main { + width: 368px; + margin: 0 auto; + + :global { + .ant-form-item { + margin-bottom: 16px; + } + } + + h3 { + font-size: 16px; + margin-bottom: 16px; + } + + .mobileGroup { + :global { + .ant-form-item { + margin-bottom: 0; + display: table-cell; + vertical-align: top; + + &:first-child { + width: 20%; + + .ant-select-selection { + border-right-width: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + } + + &:last-child { + width: 80%; + + .ant-input { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + } + } + } + } + + .getCaptcha { + display: block; + width: 100%; + } + + .submit { + width: 50%; + } + + .login { + float: right; + } +} + +.success, .warning, .error { + transition: color .3s; +} + +.success { + color: @success-color; +} + +.warning { + color: @warning-color; +} + +.error { + color: @error-color; +} + +.progress-pass > .progress { + :global { + .ant-progress-bg { + background-color: @warning-color; + } + } +} + diff --git a/src/routes/User/RegisterResult.js b/src/routes/User/RegisterResult.js new file mode 100644 index 00000000..af91ee67 --- /dev/null +++ b/src/routes/User/RegisterResult.js @@ -0,0 +1,23 @@ +import React from 'react'; +import { Button } from 'antd'; +import { Link } from 'dva/router'; +import Result from '../../components/Result'; + +const actions = ( +
    + + +
    +); + +export default () => ( +
    + +
    +); diff --git a/src/services/api.js b/src/services/api.js new file mode 100644 index 00000000..39984bac --- /dev/null +++ b/src/services/api.js @@ -0,0 +1,82 @@ +import { stringify } from 'qs'; +import request from '../utils/request'; + +export async function queryProjectNotice() { + return request('/api/project/notice'); +} + +export async function queryActivities() { + return request('/api/activities'); +} + +export async function queryRule(params) { + return request(`/api/rule?${stringify(params)}`); +} + +export async function removeRule(params) { + return request('/api/rule', { + method: 'POST', + body: { + ...params, + method: 'delete', + }, + }); +} + +export async function addRule(params) { + return request('/api/rule', { + method: 'POST', + body: { + ...params, + method: 'post', + }, + }); +} + +export async function fakeSubmitForm(params) { + return request('/api/forms', { + method: 'POST', + body: params, + }); +} + +export async function fakeChartData() { + return request('/api/fake_chart_data'); +} + +export async function queryTags() { + return request('/api/tags'); +} + +export async function queryProfile() { + return request('/api/profile'); +} + +export async function queryFakeList(params) { + return request(`/api/fake_list?${stringify(params)}`); +} + +export async function fakeAccountLogin(params) { + return request('/api/login/account', { + method: 'POST', + body: params, + }); +} + +export async function fakeMobileLogin(params) { + return request('/api/login/mobile', { + method: 'POST', + body: params, + }); +} + +export async function fakeRegister(params) { + return request('/api/register', { + method: 'POST', + body: params, + }); +} + +export async function queryNotices() { + return request('/api/notices'); +} diff --git a/src/services/user.js b/src/services/user.js new file mode 100644 index 00000000..c4defb4f --- /dev/null +++ b/src/services/user.js @@ -0,0 +1,9 @@ +import request from '../utils/request'; + +export async function query() { + return request('/api/users'); +} + +export async function queryCurrent() { + return request('/api/currentUser'); +} diff --git a/src/utils/request.js b/src/utils/request.js new file mode 100644 index 00000000..4d62c87f --- /dev/null +++ b/src/utils/request.js @@ -0,0 +1,38 @@ +import fetch from 'dva/fetch'; + +function checkStatus(response) { + if (response.status >= 200 && response.status < 300) { + return response; + } + + const error = new Error(response.statusText); + error.response = response; + throw error; +} + +/** + * Requests a URL, returning a promise. + * + * @param {string} url The URL we want to request + * @param {object} [options] The options we want to pass to "fetch" + * @return {object} An object containing either "data" or "err" + */ +export default function request(url, options) { + const defaultOptions = { + credentials: 'include', + }; + const newOptions = { ...defaultOptions, ...options }; + if (newOptions.method === 'POST' || newOptions.method === 'PUT') { + newOptions.headers = { + Accept: 'application/json', + 'Content-Type': 'application/json; charset=utf-8', + ...newOptions.headers, + }; + newOptions.body = JSON.stringify(newOptions.body); + } + + return fetch(url, newOptions) + .then(checkStatus) + .then(response => response.json()) + .catch(err => ({ err })); +} diff --git a/src/utils/utils.js b/src/utils/utils.js new file mode 100644 index 00000000..47366503 --- /dev/null +++ b/src/utils/utils.js @@ -0,0 +1,55 @@ +import moment from 'moment'; + +function fixedZero(val) { + return val * 1 < 10 ? `0${val}` : val; +} + +function getTimeDistance(type) { + const now = new Date(); + const oneDay = 1000 * 60 * 60 * 24; + + if (type === 'today') { + now.setHours(0); + now.setMinutes(0); + now.setSeconds(0); + return [moment(now), moment(now.getTime() + (oneDay - 1000))]; + } + + if (type === 'week') { + let day = now.getDay(); + now.setHours(0); + now.setMinutes(0); + now.setSeconds(0); + + if (day === 0) { + day = 6; + } else { + day -= 1; + } + + const beginTime = now.getTime() - (day * oneDay); + + return [moment(beginTime), moment(beginTime + ((7 * oneDay) - 1000))]; + } + + if (type === 'month') { + const year = now.getFullYear(); + const month = now.getMonth(); + const nextDate = moment(now).add(1, 'months'); + const nextYear = nextDate.year(); + const nextMonth = nextDate.month(); + + return [moment(`${year}-${fixedZero(month + 1)}-01 00:00:00`), moment(new Date(`${nextYear}-${fixedZero(nextMonth + 1)}-01 00:00:00`).getTime() - 1000)]; + } + + if (type === 'year') { + const year = now.getFullYear(); + + return [moment(`${year}-01-01 00:00:00`), moment(`${year}-12-31 23:59:59`)]; + } +} + +export default { + fixedZero, + getTimeDistance, +}; diff --git a/src/utils/utils.less b/src/utils/utils.less new file mode 100644 index 00000000..b728d9ae --- /dev/null +++ b/src/utils/utils.less @@ -0,0 +1,48 @@ +.textOverflow() { + overflow: hidden; + text-overflow: ellipsis; + word-break: break-all; + white-space: nowrap; +} + +.textOverflowMulti(@line: 3) { + overflow: hidden; + position: relative; + line-height: 1.5em; + max-height: @line * 1.5em; + text-align: justify; + margin-right: -1em; + padding-right: 1em; + &:before { + content: '...'; + position: absolute; + right: 0; + bottom: 0; + } + &:after { + background: white; + content: ''; + margin-top: 0.2em; + position: absolute; + right: 0; + width: 1em; + height: 1em; + } +} + +// mixins for clearfix +// ------------------------ +.clearfix() { + zoom: 1; + &:before, + &:after { + content: " "; + display: table; + } + &:after { + clear: both; + visibility: hidden; + font-size: 0; + height: 0; + } +}