├── .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 | 3 | 4 | Group 28 Copy 5 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 42 | 43 | -------------------------------------------------------------------------------- /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 |
11 | {links.map(link => ( 12 | 13 | {link.title} 14 | 15 | ))} 16 |
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 | 80 | 81 | 个人中心 82 | 83 | 84 | 设置 85 | 86 | 87 | 触发报错 88 | 89 | 90 | 91 | 退出登录 92 | 93 | 94 | ); 95 | const noticeData = this.getNoticeData(); 96 | return ( 97 |
98 | {isMobile && [ 99 | 100 | logo 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 ? not found : 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 icon; 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 | logo 218 |

Ant Design Pro

219 | 220 |
221 | 230 | {this.getNavMenuItems(this.menus)} 231 | 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 |
38 | 49 |
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 |
105 | 106 | {getFieldDecorator('name', { 107 | rules: [{ required: true }], 108 | })( 109 | 110 | )} 111 | 112 | 113 | {getFieldDecorator('desc')( 114 | 115 | )} 116 | 117 | 118 | {getFieldDecorator('url', { 119 | rules: [{ type: 'url' }], 120 | })( 121 | 122 | )} 123 | 124 | 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 | } --------------------------------------------------------------------------------