├── .editorconfig
├── .eslintrc
├── .gitignore
├── README.md
├── config
└── config.js
├── jest.config.js
├── mock
├── cards.js
└── puzzlecards.js
├── package.json
├── src
├── assets
│ └── logo.svg
├── common
│ └── menu.js
├── component
│ ├── GlobalFooter
│ │ ├── index.js
│ │ └── index.less
│ ├── GlobalHeader
│ │ ├── index.js
│ │ └── index.less
│ ├── HeaderSearch
│ │ ├── index.js
│ │ └── index.less
│ ├── NoticeIcon
│ │ ├── NoticeList.js
│ │ ├── NoticeList.less
│ │ ├── index.js
│ │ └── index.less
│ ├── SampleChart.js
│ ├── SiderMenu
│ │ ├── SiderMenu.js
│ │ ├── index.js
│ │ └── index.less
│ ├── TestDemo.js
│ └── _utils
│ │ └── pathTools.js
├── layout
│ └── index.js
├── locale
│ ├── en-US.js
│ └── zh-CN.js
├── model
│ ├── cards.js
│ └── puzzlecards.js
├── page
│ ├── UmiLocale.js
│ ├── cards.js
│ ├── dashboard
│ │ └── analysis.js
│ ├── helloworld.js
│ ├── index.js
│ ├── list
│ │ └── index.js
│ ├── locale.js
│ ├── puzzlecards.js
│ └── tsdemo.tsx
├── service
│ └── cards.js
└── util
│ └── request.js
├── test
└── helloworld.test.js
├── tool
├── bigfish.js
├── executeRule.js
└── tobigfish.sh
├── tsconfig.json
└── tslint.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | end_of_line = lf
8 | indent_style = space
9 | indent_size = 2
10 | trim_trailing_whitespace = true
11 | insert_final_newline = true
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint-config-umi"
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .umi
2 | node_modules
3 | .idea
4 | dist
5 | course-demo-bigfish
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Ant Design 实战课程配套代码
2 |
3 |
4 |
--------------------------------------------------------------------------------
/config/config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | singular: true,
3 | plugins: [
4 | ['umi-plugin-react', {
5 | antd: true,
6 | dva: true,
7 | locale: {
8 | enable: true,
9 | },
10 | }],
11 | ],
12 | routes: [
13 | {
14 | path: '/',
15 | component: '../layout',
16 | routes: [
17 | {
18 | path: '/',
19 | component: './index'
20 | },
21 | {
22 | path: 'dashboard',
23 | routes: [
24 | { path: 'analysis', component: './dashboard/analysis' }
25 | ]
26 | },
27 | {
28 | path: 'helloworld',
29 | component: './HelloWorld'
30 | },
31 | { path: 'cards', component: './cards' },
32 | { path: 'puzzlecards', component: './puzzlecards' },
33 | { path: 'list', component: './list' },
34 | { path: 'typescript', component: './tsdemo' },
35 | { path: 'locale', component: './locale' }
36 | ]
37 | }
38 | ],
39 | proxy: {
40 | '/dev': {
41 | target: 'https://08ad1pao69.execute-api.us-east-1.amazonaws.com',
42 | changeOrigin: true,
43 | },
44 | },
45 | };
46 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testURL: 'http://localhost:7001',
3 | };
4 |
--------------------------------------------------------------------------------
/mock/cards.js:
--------------------------------------------------------------------------------
1 | let data = [
2 | {
3 | id: 1,
4 | name: 'umi',
5 | desc: '极快的类 Next.js 的 React 应用框架。',
6 | url: 'https://umijs.org'
7 | },
8 | {
9 | id: 2,
10 | name: 'antd',
11 | desc: '一个服务于企业级产品的设计体系。',
12 | url: 'https://ant.design/index-cn'
13 | },
14 | {
15 | id: 3,
16 | name: 'antd-pro',
17 | desc: '一个服务于企业级产品的设计体系。',
18 | url: 'https://ant.design/index-cn'
19 | }
20 | ];
21 |
22 | export default {
23 | 'get /api/cards': function (req, res, next) {
24 | setTimeout(() => {
25 | res.json({
26 | result: data,
27 | })
28 | }, 250)
29 | },
30 | 'delete /api/cards/:id': function (req, res, next) {
31 | data = data.filter(v => v.id !== parseInt(req.params.id));
32 | console.log(req.params.id);
33 | console.log(data);
34 | setTimeout(() => {
35 | res.json({
36 | success: true,
37 | })
38 | }, 250)
39 | },
40 | 'post /api/cards/add': function (req, res, next) {
41 | data = [...data, {
42 | ...req.body,
43 | id: data[data.length - 1].id + 1,
44 | }];
45 |
46 | res.json({
47 | success: true,
48 | });
49 | },
50 | 'get /api/cards/:id/statistic': function (req, res, next) {
51 | res.json({
52 | result: [
53 | { genre: 'Sports', sold: 275 },
54 | { genre: 'Strategy', sold: 1150 },
55 | { genre: 'Action', sold: 120 },
56 | { genre: 'Shooter', sold: 350 },
57 | { genre: 'Other', sold: 150 },
58 | ]
59 | });
60 | },
61 | }
62 |
--------------------------------------------------------------------------------
/mock/puzzlecards.js:
--------------------------------------------------------------------------------
1 | /*
2 | const random_jokes = [
3 | {
4 | setup: 'What is the object oriented way to get wealthy ?',
5 | punchline: 'Inheritance',
6 | },
7 | {
8 | setup: 'To understand what recursion is...',
9 | punchline: "You must first understand what recursion is",
10 | },
11 | {
12 | setup: 'What do you call a factory that sells passable products?',
13 | punchline: 'A satisfactory',
14 | },
15 | ];
16 |
17 | let random_joke_call_count = 0;
18 |
19 | export default {
20 | 'get /dev/random_joke': function (req, res) {
21 | const responseObj = random_jokes[random_joke_call_count % random_jokes.length];
22 | random_joke_call_count += 1;
23 | setTimeout(() => {
24 | res.json(responseObj);
25 | }, 3000);
26 | },
27 | };
28 | */
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "@antv/g2": "^3.1.2",
4 | "@types/react": "^16.4.6",
5 | "@types/react-dom": "^16.0.6",
6 | "ant-design-pro": "^1.3.0",
7 | "antd": "^3.6.3",
8 | "classnames": "^2.2.6",
9 | "eslint": "^4.19.1",
10 | "install": "^0.12.1",
11 | "lodash": "^4.17.10",
12 | "lodash-decorators": "^5.0.0",
13 | "moment": "^2.22.1",
14 | "path-to-regexp": "^1.7.0",
15 | "rc-drawer-menu": "^0.5.7",
16 | "react-intl": "^2.4.0",
17 | "tslint": "^5.11.0",
18 | "tslint-config-prettier": "^1.13.0",
19 | "tslint-react": "^3.6.0",
20 | "umi": "^2.0.0",
21 | "umi-plugin-react": "^1.0.0"
22 | },
23 | "scripts": {
24 | "dev": "umi dev",
25 | "lint": "eslint --ext .js src",
26 | "test": "umi test",
27 | "_sync": "sh tool/tobigfish.sh"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/common/menu.js:
--------------------------------------------------------------------------------
1 | /* eslint no-useless-escape:0 */
2 | const reg = /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/g;
3 |
4 | export function isUrl(path) {
5 | return reg.test(path);
6 | }
7 |
8 | const menuData = [
9 | {
10 | name: 'Pages',
11 | icon: 'dashboard',
12 | path: 'dashboard',
13 | children: [
14 | {
15 | name: '分析页',
16 | path: 'analysis',
17 | },
18 | {
19 | name: '监控页',
20 | path: 'monitor',
21 | },
22 | {
23 | name: '工作台',
24 | path: 'workplace',
25 | // hideInBreadcrumb: true,
26 | // hideInMenu: true,
27 | },
28 | ],
29 | },
30 | {
31 | name: 'typescript',
32 | icon: 'dashboard',
33 | path: 'typescript',
34 | }
35 | ];
36 |
37 | function formatter(data, parentPath = '/', parentAuthority) {
38 | return data.map(item => {
39 | let { path } = item;
40 | if (!isUrl(path)) {
41 | path = parentPath + item.path;
42 | }
43 | const result = {
44 | ...item,
45 | path,
46 | authority: item.authority || parentAuthority,
47 | };
48 | if (item.children) {
49 | result.children = formatter(item.children, `${parentPath}${item.path}/`, item.authority);
50 | }
51 | return result;
52 | });
53 | }
54 |
55 | export const getMenuData = () => formatter(menuData);
56 |
--------------------------------------------------------------------------------
/src/component/GlobalFooter/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 | import styles from './index.less';
4 |
5 | const GlobalFooter = ({ className, links, copyright }) => {
6 | const clsString = classNames(styles.globalFooter, className);
7 | return (
8 |
9 | {links && (
10 |
17 | )}
18 | {copyright &&
{copyright}
}
19 |
20 | );
21 | };
22 |
23 | export default GlobalFooter;
24 |
--------------------------------------------------------------------------------
/src/component/GlobalFooter/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/lib/style/themes/default.less';
2 |
3 | .globalFooter {
4 | padding: 0 16px;
5 | margin: 48px 0 24px 0;
6 | text-align: center;
7 |
8 | .links {
9 | margin-bottom: 8px;
10 |
11 | a {
12 | color: @text-color-secondary;
13 | transition: all 0.3s;
14 |
15 | &:not(:last-child) {
16 | margin-right: 40px;
17 | }
18 |
19 | &:hover {
20 | color: @text-color;
21 | }
22 | }
23 | }
24 |
25 | .copyright {
26 | color: @text-color-secondary;
27 | font-size: @font-size-base;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/component/GlobalHeader/index.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { Menu, Icon, Spin, Tag, Dropdown, Avatar, Divider, Tooltip, Button } from 'antd';
3 | import moment from 'moment';
4 | import groupBy from 'lodash/groupBy';
5 | import Debounce from 'lodash-decorators/debounce';
6 | import Link from 'umi/link';
7 | import { FormattedMessage, setLocale, getLocale } from 'umi/locale';
8 | import NoticeIcon from '../NoticeIcon';
9 | import HeaderSearch from '../HeaderSearch';
10 | import styles from './index.less';
11 |
12 | export default class GlobalHeader extends PureComponent {
13 | componentWillUnmount() {
14 | this.triggerResizeEvent.cancel();
15 | }
16 | getNoticeData() {
17 | const { notices = [] } = this.props;
18 | if (notices.length === 0) {
19 | return {};
20 | }
21 | const newNotices = notices.map(notice => {
22 | const newNotice = { ...notice };
23 | if (newNotice.datetime) {
24 | newNotice.datetime = moment(notice.datetime).fromNow();
25 | }
26 | // transform id to item key
27 | if (newNotice.id) {
28 | newNotice.key = newNotice.id;
29 | }
30 | if (newNotice.extra && newNotice.status) {
31 | const color = {
32 | todo: '',
33 | processing: 'blue',
34 | urgent: 'red',
35 | doing: 'gold',
36 | }[newNotice.status];
37 | newNotice.extra = (
38 |
39 | {newNotice.extra}
40 |
41 | );
42 | }
43 | return newNotice;
44 | });
45 | return groupBy(newNotices, 'type');
46 | }
47 | toggle = () => {
48 | const { collapsed, onCollapse } = this.props;
49 | onCollapse(!collapsed);
50 | this.triggerResizeEvent();
51 | };
52 | /* eslint-disable*/
53 | @Debounce(600)
54 | triggerResizeEvent() {
55 | const event = document.createEvent('HTMLEvents');
56 | event.initEvent('resize', true, false);
57 | window.dispatchEvent(event);
58 | }
59 | changLang() {
60 | const locale = getLocale();
61 | if (!locale || locale === 'zh-CN') {
62 | setLocale('en-US');
63 | } else {
64 | setLocale('zh-CN');
65 | }
66 | }
67 | render() {
68 | const {
69 | currentUser = {},
70 | collapsed,
71 | fetchingNotices,
72 | isMobile,
73 | logo,
74 | onNoticeVisibleChange,
75 | onMenuClick,
76 | onNoticeClear,
77 | } = this.props;
78 | const menu = (
79 |
94 | );
95 | const noticeData = this.getNoticeData();
96 | return (
97 |
98 | {isMobile && [
99 |
100 |

101 | ,
102 |
,
103 | ]}
104 |
109 |
110 |
{
115 | console.log('input', value); // eslint-disable-line
116 | }}
117 | onPressEnter={value => {
118 | console.log('enter', value); // eslint-disable-line
119 | }}
120 | />
121 |
122 |
128 |
129 |
130 |
131 | {
135 | console.log(item, tabProps); // eslint-disable-line
136 | }}
137 | onClear={onNoticeClear}
138 | onPopupVisibleChange={onNoticeVisibleChange}
139 | loading={fetchingNotices}
140 | popupAlign={{ offset: [20, -16] }}
141 | >
142 |
148 |
154 |
160 |
161 | {currentUser.name ? (
162 |
163 |
164 |
165 | {currentUser.name}
166 |
167 |
168 | ) : (
169 |
170 | )}
171 |
179 |
180 |
181 | );
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/src/component/GlobalHeader/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/lib/style/themes/default.less';
2 |
3 | .header {
4 | height: 64px;
5 | padding: 0 12px 0 0;
6 | background: #fff;
7 | box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
8 | position: relative;
9 | }
10 |
11 | :global {
12 | .ant-layout {
13 | min-height: 100vh;
14 | overflow-x: hidden;
15 | }
16 | }
17 |
18 | .logo {
19 | height: 64px;
20 | line-height: 58px;
21 | vertical-align: top;
22 | display: inline-block;
23 | padding: 0 0 0 24px;
24 | cursor: pointer;
25 | font-size: 20px;
26 | img {
27 | display: inline-block;
28 | vertical-align: middle;
29 | }
30 | }
31 |
32 | .menu {
33 | :global(.anticon) {
34 | margin-right: 8px;
35 | }
36 | :global(.ant-dropdown-menu-item) {
37 | width: 160px;
38 | }
39 | }
40 |
41 | i.trigger {
42 | font-size: 20px;
43 | line-height: 64px;
44 | cursor: pointer;
45 | transition: all 0.3s, padding 0s;
46 | padding: 0 24px;
47 | &:hover {
48 | background: @primary-1;
49 | }
50 | }
51 |
52 | .right {
53 | float: right;
54 | height: 100%;
55 | .action {
56 | cursor: pointer;
57 | padding: 0 12px;
58 | display: inline-block;
59 | transition: all 0.3s;
60 | height: 100%;
61 | > i {
62 | font-size: 16px;
63 | vertical-align: middle;
64 | color: @text-color;
65 | }
66 | &:hover,
67 | &:global(.ant-popover-open) {
68 | background: @primary-1;
69 | }
70 | }
71 | .search {
72 | padding: 0;
73 | margin: 0 12px;
74 | &:hover {
75 | background: transparent;
76 | }
77 | }
78 | .account {
79 | .avatar {
80 | margin: 20px 8px 20px 0;
81 | color: @primary-color;
82 | background: rgba(255, 255, 255, 0.85);
83 | vertical-align: middle;
84 | }
85 | }
86 | }
87 |
88 | @media only screen and (max-width: @screen-md) {
89 | .header {
90 | :global(.ant-divider-vertical) {
91 | vertical-align: unset;
92 | }
93 | .name {
94 | display: none;
95 | }
96 | i.trigger {
97 | padding: 0 12px;
98 | }
99 | .logo {
100 | padding-right: 12px;
101 | position: relative;
102 | }
103 | .right {
104 | position: absolute;
105 | right: 12px;
106 | top: 0;
107 | background: #fff;
108 | .account {
109 | .avatar {
110 | margin-right: 0;
111 | }
112 | }
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/component/HeaderSearch/index.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Input, Icon, AutoComplete } from 'antd';
4 | import classNames from 'classnames';
5 | import styles from './index.less';
6 |
7 | export default class HeaderSearch extends PureComponent {
8 | static defaultProps = {
9 | defaultActiveFirstOption: false,
10 | onPressEnter: () => {},
11 | onSearch: () => {},
12 | className: '',
13 | placeholder: '',
14 | dataSource: [],
15 | defaultOpen: false,
16 | };
17 | static propTypes = {
18 | className: PropTypes.string,
19 | placeholder: PropTypes.string,
20 | onSearch: PropTypes.func,
21 | onPressEnter: PropTypes.func,
22 | defaultActiveFirstOption: PropTypes.bool,
23 | dataSource: PropTypes.array,
24 | defaultOpen: PropTypes.bool,
25 | };
26 | state = {
27 | searchMode: this.props.defaultOpen,
28 | value: '',
29 | };
30 | componentWillUnmount() {
31 | clearTimeout(this.timeout);
32 | }
33 | onKeyDown = e => {
34 | if (e.key === 'Enter') {
35 | this.timeout = setTimeout(() => {
36 | this.props.onPressEnter(this.state.value); // Fix duplicate onPressEnter
37 | }, 0);
38 | }
39 | };
40 | onChange = value => {
41 | this.setState({ value });
42 | if (this.props.onChange) {
43 | this.props.onChange();
44 | }
45 | };
46 | enterSearchMode = () => {
47 | this.setState({ searchMode: true }, () => {
48 | if (this.state.searchMode) {
49 | this.input.focus();
50 | }
51 | });
52 | };
53 | leaveSearchMode = () => {
54 | this.setState({
55 | searchMode: false,
56 | value: '',
57 | });
58 | };
59 | render() {
60 | const { className, placeholder, ...restProps } = this.props;
61 | delete restProps.defaultOpen; // for rc-select not affected
62 | const inputClass = classNames(styles.input, {
63 | [styles.show]: this.state.searchMode,
64 | });
65 | return (
66 |
67 |
68 |
75 | {
78 | this.input = node;
79 | }}
80 | onKeyDown={this.onKeyDown}
81 | onBlur={this.leaveSearchMode}
82 | />
83 |
84 |
85 | );
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/component/HeaderSearch/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/lib/style/themes/default.less';
2 |
3 | .headerSearch {
4 | :global(.anticon-search) {
5 | cursor: pointer;
6 | font-size: 16px;
7 | }
8 | .input {
9 | transition: width 0.3s, margin-left 0.3s;
10 | width: 0;
11 | background: transparent;
12 | border-radius: 0;
13 | :global(.ant-select-selection) {
14 | background: transparent;
15 | }
16 | input {
17 | border: 0;
18 | padding-left: 0;
19 | padding-right: 0;
20 | box-shadow: none !important;
21 | }
22 | &,
23 | &:hover,
24 | &:focus {
25 | border-bottom: 1px solid @border-color-base;
26 | }
27 | &.show {
28 | width: 210px;
29 | margin-left: 8px;
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/component/NoticeIcon/NoticeList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Avatar, List } from 'antd';
3 | import classNames from 'classnames';
4 | import styles from './NoticeList.less';
5 |
6 | export default function NoticeList({
7 | data = [],
8 | onClick,
9 | onClear,
10 | title,
11 | locale,
12 | emptyText,
13 | emptyImage,
14 | }) {
15 | if (data.length === 0) {
16 | return (
17 |
18 | {emptyImage ?

: null}
19 |
{emptyText || locale.emptyText}
20 |
21 | );
22 | }
23 | return (
24 |
25 |
26 | {data.map((item, i) => {
27 | const itemCls = classNames(styles.item, {
28 | [styles.read]: item.read,
29 | });
30 | return (
31 | onClick(item)}>
32 | : null}
35 | title={
36 |
37 | {item.title}
38 |
{item.extra}
39 |
40 | }
41 | description={
42 |
43 |
44 | {item.description}
45 |
46 |
{item.datetime}
47 |
48 | }
49 | />
50 |
51 | );
52 | })}
53 |
54 |
55 | {locale.clear}
56 | {title}
57 |
58 |
59 | );
60 | }
61 |
--------------------------------------------------------------------------------
/src/component/NoticeIcon/NoticeList.less:
--------------------------------------------------------------------------------
1 | @import '~antd/lib/style/themes/default.less';
2 |
3 | .list {
4 | max-height: 400px;
5 | overflow: auto;
6 | .item {
7 | transition: all 0.3s;
8 | overflow: hidden;
9 | cursor: pointer;
10 | padding-left: 24px;
11 | padding-right: 24px;
12 |
13 | .meta {
14 | width: 100%;
15 | }
16 |
17 | .avatar {
18 | background: #fff;
19 | margin-top: 4px;
20 | }
21 |
22 | &.read {
23 | opacity: 0.4;
24 | }
25 | &:last-child {
26 | border-bottom: 0;
27 | }
28 | &:hover {
29 | background: @primary-1;
30 | }
31 | .title {
32 | font-weight: normal;
33 | margin-bottom: 8px;
34 | }
35 | .description {
36 | font-size: 12px;
37 | line-height: @line-height-base;
38 | }
39 | .datetime {
40 | font-size: 12px;
41 | margin-top: 4px;
42 | line-height: @line-height-base;
43 | }
44 | .extra {
45 | float: right;
46 | color: @text-color-secondary;
47 | font-weight: normal;
48 | margin-right: 0;
49 | margin-top: -1.5px;
50 | }
51 | }
52 | }
53 |
54 | .notFound {
55 | text-align: center;
56 | padding: 73px 0 88px 0;
57 | color: @text-color-secondary;
58 | img {
59 | display: inline-block;
60 | margin-bottom: 16px;
61 | height: 76px;
62 | }
63 | }
64 |
65 | .clear {
66 | height: 46px;
67 | line-height: 46px;
68 | text-align: center;
69 | color: @text-color;
70 | border-radius: 0 0 @border-radius-base @border-radius-base;
71 | border-top: 1px solid @border-color-split;
72 | transition: all 0.3s;
73 | cursor: pointer;
74 |
75 | &:hover {
76 | color: @heading-color;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/component/NoticeIcon/index.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { Popover, Icon, Tabs, Badge, Spin } from 'antd';
3 | import classNames from 'classnames';
4 | import List from './NoticeList';
5 | import styles from './index.less';
6 |
7 | const { TabPane } = Tabs;
8 |
9 | export default class NoticeIcon extends PureComponent {
10 | static defaultProps = {
11 | onItemClick: () => {},
12 | onPopupVisibleChange: () => {},
13 | onTabChange: () => {},
14 | onClear: () => {},
15 | loading: false,
16 | locale: {
17 | emptyText: '暂无数据',
18 | clear: '清空',
19 | },
20 | emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg',
21 | };
22 | static Tab = TabPane;
23 | constructor(props) {
24 | super(props);
25 | this.state = {};
26 | if (props.children && props.children[0]) {
27 | this.state.tabType = props.children[0].props.title;
28 | }
29 | }
30 | onItemClick = (item, tabProps) => {
31 | const { onItemClick } = this.props;
32 | onItemClick(item, tabProps);
33 | };
34 | onTabChange = tabType => {
35 | this.setState({ tabType });
36 | this.props.onTabChange(tabType);
37 | };
38 | getNotificationBox() {
39 | const { children, loading, locale } = this.props;
40 | if (!children) {
41 | return null;
42 | }
43 | const panes = React.Children.map(children, child => {
44 | const title =
45 | child.props.list && child.props.list.length > 0
46 | ? `${child.props.title} (${child.props.list.length})`
47 | : child.props.title;
48 | return (
49 |
50 | this.onItemClick(item, child.props)}
54 | onClear={() => this.props.onClear(child.props.title)}
55 | title={child.props.title}
56 | locale={locale}
57 | />
58 |
59 | );
60 | });
61 | return (
62 |
63 |
64 | {panes}
65 |
66 |
67 | );
68 | }
69 | render() {
70 | const { className, count, popupAlign, onPopupVisibleChange } = this.props;
71 | const noticeButtonClass = classNames(className, styles.noticeButton);
72 | const notificationBox = this.getNotificationBox();
73 | const trigger = (
74 |
75 |
76 |
77 |
78 |
79 | );
80 | if (!notificationBox) {
81 | return trigger;
82 | }
83 | const popoverProps = {};
84 | if ('popupVisible' in this.props) {
85 | popoverProps.visible = this.props.popupVisible;
86 | }
87 | return (
88 |
98 | {trigger}
99 |
100 | );
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/component/NoticeIcon/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/lib/style/themes/default.less';
2 |
3 | .popover {
4 | width: 336px;
5 | :global(.ant-popover-inner-content) {
6 | padding: 0;
7 | }
8 | }
9 |
10 | .noticeButton {
11 | cursor: pointer;
12 | display: inline-block;
13 | transition: all 0.3s;
14 | }
15 |
16 | .icon {
17 | font-size: 16px;
18 | padding: 4px;
19 | }
20 |
21 | .tabs {
22 | :global {
23 | .ant-tabs-nav-scroll {
24 | text-align: center;
25 | }
26 | .ant-tabs-bar {
27 | margin-bottom: 4px;
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/component/SampleChart.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import G2 from '@antv/g2';
3 |
4 | class SampleChart extends React.Component {
5 | constructor(props) {
6 | super(props);
7 | this.containerRef = React.createRef();
8 | }
9 |
10 | componentDidMount() {
11 | this.chart = new G2.Chart({
12 | container: this.containerRef.current,
13 | width: 450,
14 | height: 300
15 | });
16 | this.refreshChart();
17 | }
18 |
19 | componentDidUpdate(prevProps) {
20 | if (prevProps.data !== this.props.data) {
21 | this.refreshChart();
22 | }
23 | }
24 |
25 | componentWillUnmount() {
26 | if (this.chart) {
27 | this.chart.destroy();
28 | }
29 | }
30 |
31 | refreshChart = () => {
32 | this.chart.source(this.props.data);
33 | this.chart.interval().position('genre*sold').color('genre');
34 | this.chart.render();
35 | };
36 |
37 | render() {
38 | return (
39 |
40 | );
41 | }
42 | }
43 |
44 | export default SampleChart;
--------------------------------------------------------------------------------
/src/component/SiderMenu/SiderMenu.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { Layout, Menu, Icon } from 'antd';
3 | import pathToRegexp from 'path-to-regexp';
4 | import Link from 'umi/link';
5 | import styles from './index.less';
6 | import { urlToList } from '../_utils/pathTools';
7 |
8 | const { Sider } = Layout;
9 | const { SubMenu } = Menu;
10 |
11 | // Allow menu.js config icon as string or ReactNode
12 | // icon: 'setting',
13 | // icon: 'http://demo.com/icon.png',
14 | // icon: ,
15 | const getIcon = icon => {
16 | if (typeof icon === 'string' && icon.indexOf('http') === 0) {
17 | return
;
18 | }
19 | if (typeof icon === 'string') {
20 | return ;
21 | }
22 | return icon;
23 | };
24 |
25 | export const getMeunMatcheys = (flatMenuKeys, path) => {
26 | return flatMenuKeys.filter(item => {
27 | return pathToRegexp(item).test(path);
28 | });
29 | };
30 |
31 | export default class SiderMenu extends PureComponent {
32 | constructor(props) {
33 | super(props);
34 | this.menus = props.menuData;
35 | this.flatMenuKeys = this.getFlatMenuKeys(props.menuData);
36 | this.state = {
37 | openKeys: this.getDefaultCollapsedSubMenus(props),
38 | };
39 | }
40 | UNSAFE_componentWillReceiveProps(nextProps) {
41 | if (nextProps.location.pathname !== this.props.location.pathname) {
42 | this.setState({
43 | openKeys: this.getDefaultCollapsedSubMenus(nextProps),
44 | });
45 | }
46 | }
47 | /**
48 | * Convert pathname to openKeys
49 | * /list/search/articles = > ['list','/list/search']
50 | * @param props
51 | */
52 | getDefaultCollapsedSubMenus(props) {
53 | const { location: { pathname } } = props || this.props;
54 | return urlToList(pathname)
55 | .map(item => {
56 | return getMeunMatcheys(this.flatMenuKeys, item)[0];
57 | })
58 | .filter(item => item);
59 | }
60 | /**
61 | * Recursively flatten the data
62 | * [{path:string},{path:string}] => {path,path2}
63 | * @param menus
64 | */
65 | getFlatMenuKeys(menus) {
66 | let keys = [];
67 | menus.forEach(item => {
68 | if (item.children) {
69 | keys = keys.concat(this.getFlatMenuKeys(item.children));
70 | }
71 | keys.push(item.path);
72 | });
73 | return keys;
74 | }
75 | /**
76 | * 判断是否是http链接.返回 Link 或 a
77 | * Judge whether it is http link.return a or Link
78 | * @memberof SiderMenu
79 | */
80 | getMenuItemPath = item => {
81 | const itemPath = this.conversionPath(item.path);
82 | const icon = getIcon(item.icon);
83 | const { target, name } = item;
84 | // Is it a http link
85 | if (/^https?:\/\//.test(itemPath)) {
86 | return (
87 |
88 | {icon}
89 | {name}
90 |
91 | );
92 | }
93 | return (
94 | {
101 | this.props.onCollapse(true);
102 | }
103 | : undefined
104 | }
105 | >
106 | {icon}
107 | {name}
108 |
109 | );
110 | };
111 | /**
112 | * get SubMenu or Item
113 | */
114 | getSubMenuOrItem = item => {
115 | if (item.children && item.children.some(child => child.name)) {
116 | const childrenItems = this.getNavMenuItems(item.children);
117 | // 当无子菜单时就不展示菜单
118 | if (childrenItems && childrenItems.length > 0) {
119 | return (
120 |
124 | {getIcon(item.icon)}
125 | {item.name}
126 |
127 | ) : (
128 | item.name
129 | )
130 | }
131 | key={item.path}
132 | >
133 | {childrenItems}
134 |
135 | );
136 | }
137 | return null;
138 | } else {
139 | return {this.getMenuItemPath(item)};
140 | }
141 | };
142 | /**
143 | * 获得菜单子节点
144 | * @memberof SiderMenu
145 | */
146 | getNavMenuItems = menusData => {
147 | if (!menusData) {
148 | return [];
149 | }
150 | return menusData
151 | .filter(item => item.name && !item.hideInMenu)
152 | .map(item => {
153 | // make dom
154 | const ItemDom = this.getSubMenuOrItem(item);
155 | return this.checkPermissionItem(item.authority, ItemDom);
156 | })
157 | .filter(item => item);
158 | };
159 | // Get the currently selected menu
160 | getSelectedMenuKeys = () => {
161 | const { location: { pathname } } = this.props;
162 | return urlToList(pathname).map(itemPath => getMeunMatcheys(this.flatMenuKeys, itemPath).pop());
163 | };
164 | // conversion Path
165 | // 转化路径
166 | conversionPath = path => {
167 | if (path && path.indexOf('http') === 0) {
168 | return path;
169 | } else {
170 | return `/${path || ''}`.replace(/\/+/g, '/');
171 | }
172 | };
173 | // permission to check
174 | checkPermissionItem = (authority, ItemDom) => {
175 | if (this.props.Authorized && this.props.Authorized.check) {
176 | const { check } = this.props.Authorized;
177 | return check(authority, ItemDom);
178 | }
179 | return ItemDom;
180 | };
181 | isMainMenu = key => {
182 | return this.menus.some(item => key && (item.key === key || item.path === key));
183 | };
184 | handleOpenChange = openKeys => {
185 | const lastOpenKey = openKeys[openKeys.length - 1];
186 | const moreThanOne = openKeys.filter(openKey => this.isMainMenu(openKey)).length > 1;
187 | this.setState({
188 | openKeys: moreThanOne ? [lastOpenKey] : [...openKeys],
189 | });
190 | };
191 | render() {
192 | const { logo, collapsed, onCollapse } = this.props;
193 | const { openKeys } = this.state;
194 | // Don't show popup menu when it is been collapsed
195 | const menuProps = collapsed
196 | ? {}
197 | : {
198 | openKeys,
199 | };
200 | // if pathname can't match, use the nearest parent's key
201 | let selectedKeys = this.getSelectedMenuKeys();
202 | if (!selectedKeys.length) {
203 | selectedKeys = [openKeys[openKeys.length - 1]];
204 | }
205 | return (
206 |
215 |
216 |
217 |

218 |
Ant Design Pro
219 |
220 |
221 |
232 |
233 | );
234 | }
235 | }
236 |
--------------------------------------------------------------------------------
/src/component/SiderMenu/index.js:
--------------------------------------------------------------------------------
1 | import 'rc-drawer-menu/assets/index.css';
2 | import React from 'react';
3 | import DrawerMenu from 'rc-drawer-menu';
4 | import SiderMenu from './SiderMenu';
5 |
6 | const SiderMenuWrapper = props =>
7 | props.isMobile ? (
8 | {
14 | props.onCollapse(true);
15 | }}
16 | width="256px"
17 | >
18 |
19 |
20 | ) : (
21 |
22 | );
23 |
24 | export default SiderMenuWrapper;
25 |
--------------------------------------------------------------------------------
/src/component/SiderMenu/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/lib/style/themes/default.less';
2 | @ease-in-out-circ: cubic-bezier(0.78, 0.14, 0.15, 0.86);
3 | .logo {
4 | height: 64px;
5 | position: relative;
6 | line-height: 64px;
7 | padding-left: (@menu-collapsed-width - 32px) / 2;
8 | transition: all 0.3s;
9 | background: #002140;
10 | overflow: hidden;
11 | img {
12 | display: inline-block;
13 | vertical-align: middle;
14 | height: 32px;
15 | }
16 | h1 {
17 | color: white;
18 | display: inline-block;
19 | vertical-align: middle;
20 | font-size: 20px;
21 | margin: 0 0 0 12px;
22 | font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif;
23 | font-weight: 600;
24 | }
25 | }
26 |
27 | .sider {
28 | min-height: 100vh;
29 | box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
30 | position: relative;
31 | z-index: 10;
32 | &.ligth {
33 | background-color: white;
34 | .logo {
35 | background: white;
36 | h1 {
37 | color: #002140;
38 | }
39 | }
40 | }
41 | }
42 |
43 | .icon {
44 | width: 14px;
45 | margin-right: 10px;
46 | }
47 |
48 | :global {
49 | .drawer .drawer-content {
50 | background: #001529;
51 | }
52 | .ant-menu-inline-collapsed {
53 | & > .ant-menu-item .sider-menu-item-img + span,
54 | &
55 | > .ant-menu-item-group
56 | > .ant-menu-item-group-list
57 | > .ant-menu-item
58 | .sider-menu-item-img
59 | + span,
60 | & > .ant-menu-submenu > .ant-menu-submenu-title .sider-menu-item-img + span {
61 | max-width: 0;
62 | display: inline-block;
63 | opacity: 0;
64 | }
65 | }
66 | .ant-menu-item .sider-menu-item-img + span,
67 | .ant-menu-submenu-title .sider-menu-item-img + span {
68 | transition: opacity 0.3s @ease-in-out, width 0.3s @ease-in-out;
69 | opacity: 1;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/component/TestDemo.js:
--------------------------------------------------------------------------------
1 | export default () => {
2 | return test
;
3 | };
4 |
--------------------------------------------------------------------------------
/src/component/_utils/pathTools.js:
--------------------------------------------------------------------------------
1 | // /userinfo/2144/id => ['/userinfo','/useinfo/2144,'/userindo/2144/id']
2 | export function urlToList(url) {
3 | const urllist = url.split('/').filter(i => i);
4 | return urllist.map((urlItem, index) => {
5 | return `/${urllist.slice(0, index + 1).join('/')}`;
6 | });
7 | }
8 |
--------------------------------------------------------------------------------
/src/layout/index.js:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 | import { Layout } from 'antd';
3 | import SiderMenu from "../component/SiderMenu/SiderMenu";
4 | import { getMenuData } from '../common/menu';
5 | import logo from '../assets/logo.svg';
6 | import GlobalHeader from "../component/GlobalHeader";
7 |
8 | const { Content, Header } = Layout;
9 |
10 | class BasicLayout extends Component {
11 | constructor(props) {
12 | super(props);
13 | this.state = {
14 | collapsed: false,
15 | };
16 | }
17 |
18 | handleMenuCollapse = () => {
19 | this.setState({
20 | collapsed: !this.state.collapsed,
21 | });
22 | };
23 |
24 | render() {
25 | const { children, location } = this.props;
26 | const { collapsed } = this.state;
27 | return (
28 |
29 |
36 |
37 |
50 |
51 | { children }
52 |
53 |
54 |
55 | );
56 | }
57 | }
58 |
59 | export default BasicLayout;
60 |
--------------------------------------------------------------------------------
/src/locale/en-US.js:
--------------------------------------------------------------------------------
1 | export default {
2 | lang: 'English',
3 | helloworld: 'hello world',
4 | }
5 |
--------------------------------------------------------------------------------
/src/locale/zh-CN.js:
--------------------------------------------------------------------------------
1 | export default {
2 | lang: '中文',
3 | helloworld: '你好',
4 | }
5 |
--------------------------------------------------------------------------------
/src/model/cards.js:
--------------------------------------------------------------------------------
1 | import * as cardsService from '../service/cards';
2 |
3 | export default {
4 |
5 | namespace: 'cards',
6 |
7 | state: {
8 | cardsList: [],
9 | statistic: {},
10 | },
11 |
12 | effects: {
13 | *queryList({ _ }, { call, put }) {
14 | const rsp = yield call(cardsService.queryList);
15 | console.log('queryList');
16 | console.log(rsp);
17 | yield put({ type: 'saveList', payload: { cardsList: rsp.result } });
18 | },
19 | *deleteOne({ payload }, { call, put }) {
20 | const rsp = yield call(cardsService.deleteOne, payload);
21 | console.log('deleteOne');
22 | console.log(rsp);
23 | return rsp;
24 | },
25 | *addOne({ payload }, { call, put }) {
26 | const rsp = yield call(cardsService.addOne, payload);
27 | yield put({ type: 'queryList' });
28 | return rsp;
29 | },
30 | *getStatistic({ payload }, { call, put }) {
31 | const rsp = yield call(cardsService.getStatistic, payload);
32 | yield put({
33 | type: 'saveStatistic',
34 | payload: {
35 | id: payload,
36 | data: rsp.result,
37 | },
38 | });
39 | return rsp;
40 | },
41 | },
42 |
43 | reducers: {
44 | saveList(state, { payload: { cardsList } }) {
45 | return {
46 | ...state,
47 | cardsList,
48 | }
49 | },
50 | saveStatistic(state, { payload: { id, data } }) {
51 | return {
52 | ...state,
53 | statistic: {
54 | ...state.statistic,
55 | [id]: data,
56 | },
57 | }
58 | },
59 | },
60 | };
61 |
--------------------------------------------------------------------------------
/src/model/puzzlecards.js:
--------------------------------------------------------------------------------
1 | import request from '../util/request'; // request 是 demo 项目脚手架中提供的一个做 http 请求的方法,是对于 fetch 的封装,返回 Promise
2 |
3 | const delay = (millisecond) => {
4 | return new Promise((resolve) => {
5 | setTimeout(resolve, millisecond);
6 | });
7 | };
8 |
9 | export default {
10 | namespace: 'puzzlecards',
11 | state: {
12 | data: [],
13 | counter: 0,
14 | },
15 | effects: {
16 | *queryInitCards(_, sagaEffects) {
17 | const { call, put } = sagaEffects;
18 | const endPointURI = '/dev/random_joke';
19 |
20 | const puzzle = yield call(request, endPointURI);
21 | yield put({ type: 'addNewCard', payload: puzzle });
22 |
23 | yield call(delay, 3000);
24 |
25 | const puzzle2 = yield call(request, endPointURI);
26 | yield put({ type: 'addNewCard', payload: puzzle2 });
27 | }
28 | },
29 | reducers: {
30 | addNewCard(state, { payload: newCard }) {
31 | const nextCounter = state.counter + 1;
32 | const newCardWithId = { ...newCard, id: nextCounter };
33 | const nextData = state.data.concat(newCardWithId);
34 | return {
35 | data: nextData,
36 | counter: nextCounter,
37 | };
38 | }
39 | },
40 | };
--------------------------------------------------------------------------------
/src/page/UmiLocale.js:
--------------------------------------------------------------------------------
1 | import { DatePicker } from 'antd';
2 | import {
3 | FormattedMessage,
4 | } from 'umi/locale';
5 |
6 | export default () => {
7 | return (
8 |
9 |
10 |
11 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/src/page/cards.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'dva';
3 | // import Link from 'umi/link';
4 | import { Card, Icon, message } from 'antd';
5 |
6 | class CardsPage extends Component {
7 | componentDidMount() {
8 | this.queryList();
9 | }
10 |
11 | queryList = () => {
12 | this.props.dispatch({
13 | type: 'cards/queryList',
14 | });
15 | };
16 |
17 | deleteOne = (id) => {
18 | this.props.dispatch({
19 | type: 'cards/deleteOne',
20 | payload: id,
21 | }).then(() => {
22 | message.success('delete success, refresh');
23 | this.queryList();
24 | });
25 | };
26 |
27 | render() {
28 | const { cardsList = [] } = this.props;
29 | console.log('cardsList');
30 | console.log(cardsList);
31 |
32 | return (
33 |
34 | {cardsList.map(v => this.deleteOne(v.id)} />}
39 | >{v.desc})}
40 |
41 | );
42 | }
43 | }
44 |
45 | function mapStateToProps(state) {
46 | console.log('state');
47 | console.log(state);
48 | return {
49 | cardsList: state.cards.cardsList,
50 | };
51 | }
52 |
53 | export default connect(mapStateToProps)(CardsPage);
54 |
55 | // TODO replace antd Card with own Card.
56 |
--------------------------------------------------------------------------------
/src/page/dashboard/analysis.js:
--------------------------------------------------------------------------------
1 | import router from 'umi/router';
2 | import { Button } from 'antd';
3 |
4 | export default () =>
5 | <>
6 | Dashboard Analysis Page
7 |
10 | >
11 |
12 |
--------------------------------------------------------------------------------
/src/page/helloworld.js:
--------------------------------------------------------------------------------
1 | import {
2 | FormattedMessage,
3 | } from 'umi/locale';
4 |
5 | export default () => {
6 | return
;
7 | }
8 |
--------------------------------------------------------------------------------
/src/page/index.js:
--------------------------------------------------------------------------------
1 | import Link from 'umi/link';
2 |
3 | export default () =>
4 | <>
5 | Index Page
6 | Pages
7 |
8 | - /dashboard/analysis
9 | - /cards
10 | - /puzzlecards
11 | - /helloworld
12 | - /locale
13 |
14 | >
15 |
--------------------------------------------------------------------------------
/src/page/list/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Table, Modal, Button, Form, Input } from 'antd';
3 | import { connect } from 'dva';
4 | import SampleChart from '../../component/SampleChart';
5 |
6 | const FormItem = Form.Item;
7 |
8 | class List extends React.Component {
9 | state = {
10 | visible: false,
11 | statisticVisible: false,
12 | id: null,
13 | };
14 |
15 | columns = [
16 | {
17 | title: '名称',
18 | dataIndex: 'name',
19 | },
20 | {
21 | title: '描述',
22 | dataIndex: 'desc',
23 | },
24 | {
25 | title: '链接',
26 | dataIndex: 'url',
27 | render(value) {
28 | return (
29 | {value}
30 | );
31 | },
32 | },
33 | {
34 | title: '',
35 | dataIndex: 'statistic',
36 | render: (_, { id }) => {
37 | return (
38 |
39 | );
40 | },
41 | },
42 | ];
43 |
44 | componentDidMount() {
45 | this.props.dispatch({
46 | type: 'cards/queryList',
47 | });
48 | }
49 |
50 | showModal = () => {
51 | this.setState({ visible: true });
52 | };
53 |
54 | showStatistic = (id) => {
55 | this.props.dispatch({
56 | type: 'cards/getStatistic',
57 | payload: id,
58 | });
59 | this.setState({ id, statisticVisible: true });
60 | };
61 |
62 | handleOk = () => {
63 | const { dispatch, form: { validateFields } } = this.props;
64 |
65 | validateFields((err, values) => {
66 | if (!err) {
67 | dispatch({
68 | type: 'cards/addOne',
69 | payload: values,
70 | });
71 | this.setState({ visible: false });
72 | }
73 | });
74 | }
75 |
76 | handleCancel = () => {
77 | this.setState({
78 | visible: false,
79 | });
80 | }
81 |
82 | handleStatisticCancel = () => {
83 | this.setState({
84 | statisticVisible: false,
85 | });
86 | }
87 |
88 | render() {
89 | const { visible, statisticVisible, id } = this.state;
90 | const { cardsList, cardsLoading, form: { getFieldDecorator }, statistic } = this.props;
91 |
92 | return (
93 |
94 |
95 |
96 |
97 |
98 |
104 |
125 |
126 |
127 |
128 |
129 |
130 |
131 | );
132 | }
133 | }
134 |
135 | function mapStateToProps(state) {
136 | return {
137 | cardsList: state.cards.cardsList,
138 | cardsLoading: state.loading.effects['cards/queryList'],
139 | statistic: state.cards.statistic,
140 | };
141 | }
142 |
143 | export default connect(mapStateToProps)(Form.create()(List));
--------------------------------------------------------------------------------
/src/page/locale.js:
--------------------------------------------------------------------------------
1 | // 对应课程参考代码中的 src/page/locale.js
2 | import zhCN from 'antd/lib/locale-provider/zh_CN';
3 | import { DatePicker, LocaleProvider } from 'antd';
4 | import {
5 | FormattedMessage,
6 | IntlProvider,
7 | addLocaleData,
8 | } from 'react-intl';
9 | import zhData from 'react-intl/locale-data/zh';
10 |
11 | const messages = {
12 | 'helloworld': '你好',
13 | };
14 |
15 | addLocaleData(zhData);
16 |
17 | export default () => {
18 | return (
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/src/page/puzzlecards.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Card, /* Button */ } from 'antd';
3 | import { connect } from 'dva';
4 |
5 | const namespace = 'puzzlecards';
6 |
7 | const mapStateToProps = (state) => {
8 | const cardList = state[namespace].data;
9 | return {
10 | cardList,
11 | };
12 | };
13 |
14 | const mapDispatchToProps = (dispatch) => {
15 | return {
16 | onDidMount: () => {
17 | dispatch({
18 | type: `${namespace}/queryInitCards`,
19 | });
20 | },
21 | };
22 | };
23 |
24 | @connect(mapStateToProps, mapDispatchToProps)
25 | export default class PuzzleCardsPage extends Component {
26 | componentDidMount() {
27 | this.props.onDidMount();
28 | }
29 | render() {
30 | return (
31 |
32 | {
33 | this.props.cardList.map(card => {
34 | return (
35 |
36 | Q: {card.setup}
37 |
38 | A: {card.punchline}
39 |
40 |
41 | );
42 | })
43 | }
44 |
45 | );
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/page/tsdemo.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | // name为必填
4 | const Hello = ({ name }) => Hello,{name}
;
5 |
6 | // 约束 name 的类型
7 | const SFCHello: React.SFC<{ name: string }> = ({ name }) => (
8 | Hello,{name}
9 | );
10 |
11 | class Message extends React.Component<
12 | {
13 | message: string;
14 | },
15 | {
16 | count: number;
17 | }
18 | > {
19 | constructor(props) {
20 | super(props);
21 | this.state = {
22 | count: 0
23 | };
24 | }
25 | public increment = () => {
26 | const { count } = this.state;
27 | this.setState({
28 | count: count + 1
29 | });
30 | };
31 | public render() {
32 | return (
33 |
34 | {this.props.message}
35 | {this.state.count}
36 |
37 | );
38 | }
39 | }
40 |
41 | const App = () => (
42 | <>
43 |
44 |
45 |
46 | >
47 | );
48 |
49 | export default App;
50 |
--------------------------------------------------------------------------------
/src/service/cards.js:
--------------------------------------------------------------------------------
1 | import request from '../util/request';
2 |
3 | export function queryList() {
4 | return request('/api/cards');
5 | }
6 |
7 | export function deleteOne(id) {
8 | return request(`/api/cards/${id}`, {
9 | method: 'DELETE'
10 | });
11 | }
12 |
13 | export function addOne(data) {
14 | return request('/api/cards/add', {
15 | headers: {
16 | 'content-type': 'application/json',
17 | },
18 | method: 'POST',
19 | body: JSON.stringify(data),
20 | });
21 | }
22 |
23 | export function getStatistic(id) {
24 | return request(`/api/cards/${id}/statistic`);
25 | }
--------------------------------------------------------------------------------
/src/util/request.js:
--------------------------------------------------------------------------------
1 | // import fetch from 'dva/fetch';
2 |
3 | function checkStatus(response) {
4 | if (response.status >= 200 && response.status < 300) {
5 | return response;
6 | }
7 |
8 | const error = new Error(response.statusText);
9 | error.response = response;
10 | throw error;
11 | }
12 |
13 | /**
14 | * Requests a URL, returning a promise.
15 | *
16 | * @param {string} url The URL we want to request
17 | * @param {object} [options] The options we want to pass to "fetch"
18 | * @return {object} An object containing either "data" or "err"
19 | */
20 | export default async function request(url, options) {
21 | const response = await fetch(url, options);
22 | checkStatus(response);
23 | return await response.json();
24 | }
25 |
--------------------------------------------------------------------------------
/test/helloworld.test.js:
--------------------------------------------------------------------------------
1 | import { mount } from 'enzyme';
2 | import TestDemo from '../src/component/TestDemo';
3 |
4 | const sum = function (a, b) {
5 | return a + b;
6 | };
7 |
8 | test('adds 1 + 2 to equal 3', () => {
9 | expect(sum(1, 2)).toBe(3);
10 | });
11 |
12 | test('TestDemo', () => {
13 | const wrapper = mount();
14 | expect(wrapper.find('div').text()).toBe('test');
15 | });
16 |
--------------------------------------------------------------------------------
/tool/bigfish.js:
--------------------------------------------------------------------------------
1 | // 代码参考 http://gitlab.alipay-inc.com/bigfish/bigfish-antdpro-adapter
2 |
3 | const executeRule = require('./executeRule');
4 |
5 | const rules = [
6 | // copy 代码
7 | {
8 | pattern: '!(dist|node_modules|tool|.git|.gitlab-ci.yml)',
9 | operation: 'cp',
10 | target: 'dist/',
11 | },
12 | // 修改代码从 umi 到 bigfish
13 | {
14 | pattern: 'dist/package.json',
15 | operation: 'modify',
16 | ops: [{
17 | match: '"umi": "^2.0.0",',
18 | replace: '"@alipay/bigfish": "^2.0.0",'
19 | }, {
20 | match: `,
21 | "umi-plugin-react": "^1.0.0"`,
22 | replace: ''
23 | }, {
24 | match: /umi/g,
25 | replace: 'bigfish',
26 | }],
27 | },
28 | // 修改配置
29 | {
30 | pattern: 'dist/config/config.js',
31 | operation: 'modify',
32 | ops: [{
33 | match: `
34 | singular: true,
35 | plugins: [
36 | ['umi-plugin-react', {
37 | antd: true,
38 | dva: true,
39 | locale: {
40 | enable: true,
41 | },
42 | }],
43 | ],`,
44 | replace: `
45 | locale: {
46 | enable: true,
47 | },`,
48 | }],
49 | },
50 | // 修改组件中的依赖路径
51 | {
52 | pattern: 'dist/src/**/*.js',
53 | operation: 'modify',
54 | ops: [{
55 | match: '\'react\'',
56 | replace: '\'@alipay/bigfish/react\'',
57 | }, {
58 | match: '\'dva\'',
59 | replace: '\'@alipay/bigfish/sdk\'',
60 | }, {
61 | match: /'antd/g,
62 | replace: '\'@alipay/bigfish/antd',
63 | }, {
64 | match: '\'classnames\'',
65 | replace: '\'@alipay/bigfish/util/classnames\'',
66 | }, {
67 | match: 'import { routerRedux } from \'dva/router\'',
68 | replace: 'import history from \'@alipay/bigfish/sdk/history\';',
69 | }, {
70 | match: '\'dva/router\'',
71 | replace: '\'@alipay/bigfish/sdk/router\'',
72 | }, {
73 | match: '\'prop-types\'',
74 | replace: '\'@alipay/bigfish/util/prop-types\'',
75 | }, {
76 | match: '\'umi/locale\'',
77 | replace: '\'@alipay/bigfish/locale\'',
78 | }, {
79 | match: 'import Link from \'umi/link\';',
80 | replace: 'import { Link } from \'@alipay/bigfish/sdk/router\';',
81 | }]
82 | }
83 | ]
84 |
85 | rules.forEach((rule) => {
86 | executeRule(rule, true);
87 | });
88 |
89 | console.log('done');
90 | process.exit(0);
91 |
--------------------------------------------------------------------------------
/tool/executeRule.js:
--------------------------------------------------------------------------------
1 | const glob = require('glob');
2 | const fs = require('fs-extra');
3 | const { join, basename } = require('path');
4 |
5 | const getRealPath = (p) => {
6 | return join(__dirname, '..', p);
7 | };
8 |
9 | const rfRule = (path) => {
10 | fs.removeSync(path);
11 | };
12 |
13 | const cpRule = (path, { target }) => {
14 | let realPath = getRealPath(target);
15 | if (target.endsWith('/')) {
16 | realPath = join(realPath, basename(path))
17 | }
18 | fs.copySync(path, realPath);
19 | };
20 |
21 | const modifyRule = (path, { ops }) => {
22 | let fileContent = '' + fs.readFileSync(path);
23 | ops.forEach(({ match, replace }) => {
24 | fileContent = fileContent.replace(match, replace);
25 | });
26 | fs.writeFileSync(path, fileContent);
27 | };
28 |
29 | const renamRule = (path, { name }) => {
30 | const oldName = basename(path);
31 | if (typeof name === 'function') {
32 | name = name(oldName);
33 | }
34 | fs.moveSync(path, path.replace(oldName, name));
35 | };
36 |
37 | function executeRule(rule, dot) {
38 | const { pattern, operation } = rule;
39 | const files = glob.sync(pattern, { dot });
40 | console.log(`find ${files.length} matched for ${pattern}`);
41 | files.forEach((f => {
42 | const realPath = getRealPath(f);
43 | let op = () => { console.warn(`not find operation: ${operation}`); };
44 | switch(operation) {
45 | case 'rf':
46 | op = rfRule;
47 | break;
48 | case 'cp':
49 | op = cpRule;
50 | break;
51 | case 'modify':
52 | op = modifyRule;
53 | break;
54 | case 'rename':
55 | op = renamRule;
56 | break;
57 | }
58 | console.log(`start execute ${operation} for ${realPath} ...`);
59 | op(realPath, rule);
60 | }));
61 | };
62 |
63 | module.exports = executeRule;
64 |
65 |
--------------------------------------------------------------------------------
/tool/tobigfish.sh:
--------------------------------------------------------------------------------
1 | # 将 umi 的代码转换为 bigfish(蚂蚁金服基于 umi 封装的内部框架)
2 |
3 | rm -rf dist
4 | node tool/bigfish.js
5 |
6 | ## sync to bigfish git
7 | git clone $BIGFISH_GIT
8 | cd course-demo-bigfish
9 | rm -rf ./*
10 | rm .editorconfig .eslintrc .gitignore
11 | cp -r ../dist/* ./
12 | cp ../dist/.* ./
13 | git add -A
14 | git commit -m 'commit for bigfish'
15 | git push
16 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "build/dist",
4 | "module": "esnext",
5 | "target": "es2016",
6 | "lib": ["es6", "dom"],
7 | "sourceMap": true,
8 | "jsx": "react",
9 | "allowSyntheticDefaultImports": true,
10 | "moduleResolution": "node",
11 | "rootDir": "src",
12 | "forceConsistentCasingInFileNames": true,
13 | "noImplicitReturns": true,
14 | "suppressImplicitAnyIndexErrors": true,
15 | "noUnusedLocals": true,
16 | "experimentalDecorators": true
17 | },
18 | "exclude": [
19 | "node_modules",
20 | "build",
21 | "scripts",
22 | "acceptance-tests",
23 | "webpack",
24 | "jest",
25 | "src/setupTests.ts",
26 | "tslint:latest",
27 | "tslint-config-prettier"
28 | ]
29 | }
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["tslint:latest", "tslint-react", "tslint-config-prettier"],
3 | "rules": {
4 | "no-var-requires": false,
5 | "no-submodule-imports": false,
6 | "object-literal-sort-keys": false,
7 | "jsx-no-lambda": false,
8 | "no-implicit-dependencies": false
9 | }
10 | }
--------------------------------------------------------------------------------