├── mock ├── .gitkeep ├── utils.js ├── notices.js ├── profile.js ├── chart.js ├── rule.js └── api.js ├── src ├── routes │ ├── Dashboard │ │ ├── Analysis.less │ │ ├── Monitor.less │ │ ├── Workplace.less │ │ ├── Monitor.js │ │ ├── Workplace.js │ │ └── Analysis.js │ ├── Forms │ │ ├── BasicForm.less │ │ └── BasicForm.js │ └── User │ │ ├── Login.less │ │ └── Login.js ├── assets │ └── login-bg.jpg ├── common │ ├── config.js │ └── nav.js ├── services │ ├── user.js │ └── api.js ├── components │ └── theme.js ├── layouts │ ├── UserLayout.less │ ├── UserLayout.js │ ├── BasicLayout.less │ └── BasicLayout.js ├── index.ejs ├── rollbar.js ├── models │ ├── index.js │ ├── user.js │ ├── login.js │ └── global.js ├── index.less ├── app.js ├── utils │ ├── utils.less │ ├── request.js │ └── utils.js └── router.js ├── jsconfig.json ├── .editorconfig ├── .gitignore ├── .webpackrc ├── .travis.yml ├── .stylelintrc ├── .eslintrc ├── .roadhogrc.mock.js ├── package.json └── README.md /mock/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/routes/Dashboard/Analysis.less: -------------------------------------------------------------------------------- 1 | div { 2 | font-size: 13px; 3 | } 4 | -------------------------------------------------------------------------------- /src/routes/Dashboard/Monitor.less: -------------------------------------------------------------------------------- 1 | div { 2 | font-size: 13px; 3 | } 4 | -------------------------------------------------------------------------------- /src/routes/Forms/BasicForm.less: -------------------------------------------------------------------------------- 1 | div { 2 | font-size: 13px; 3 | } 4 | -------------------------------------------------------------------------------- /src/routes/Dashboard/Workplace.less: -------------------------------------------------------------------------------- 1 | div { 2 | font-size: 13px; 3 | } 4 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true 4 | } 5 | } -------------------------------------------------------------------------------- /src/assets/login-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiubug/react-antd-dva/HEAD/src/assets/login-bg.jpg -------------------------------------------------------------------------------- /src/common/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | api: 'http://admin.sosout.com', 3 | USER_TOKEN: 'USER_TOKEN', 4 | USER_ID: 'USER_ID', 5 | }; 6 | -------------------------------------------------------------------------------- /src/services/user.js: -------------------------------------------------------------------------------- 1 | import { stringify } from 'qs'; 2 | import request from '../utils/request'; 3 | 4 | export async function queryCurrent(params) { 5 | return request(`/user/getUser?${stringify(params)}`); 6 | } 7 | -------------------------------------------------------------------------------- /src/components/theme.js: -------------------------------------------------------------------------------- 1 | // https://github.com/ant-design/ant-design/blob/master/components/style/themes/default.less 2 | module.exports = { 3 | // 'primary-color': '#10e99b', 4 | 'card-actions-background': '#f5f8fa', 5 | }; 6 | -------------------------------------------------------------------------------- /src/layouts/UserLayout.less: -------------------------------------------------------------------------------- 1 | @import "~antd/lib/style/themes/default.less"; 2 | 3 | .user-layout { 4 | position: relative; 5 | width: 100%; 6 | min-width: 1000px; 7 | height: 100vh; 8 | background: url('../assets/login-bg.jpg') no-repeat 50%; 9 | background-size: cover; 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | # roadhog-api-doc ignore 6 | /src/utils/request-temp.js 7 | _roadhog-api-doc 8 | 9 | # production 10 | /dist 11 | 12 | # misc 13 | .DS_Store 14 | npm-debug.log* 15 | 16 | /coverage 17 | -------------------------------------------------------------------------------- /src/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | react antd dva 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/rollbar.js: -------------------------------------------------------------------------------- 1 | import Rollbar from 'rollbar'; 2 | 3 | // 检测、诊断和调试错误 4 | if (location.host === 'dva.sosout.com') { 5 | Rollbar.init({ 6 | accessToken: 'a7bfb90e615c42cbb4bd11196ae23afd', 7 | captureUncaught: true, 8 | captureUnhandledRejections: true, 9 | payload: { 10 | environment: 'production', 11 | }, 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /src/services/api.js: -------------------------------------------------------------------------------- 1 | import Store from 'store'; 2 | import request from '../utils/request'; 3 | 4 | // 用户登录 5 | export async function signIn(params) { 6 | return request('/user/login', { 7 | method: 'POST', 8 | body: params, 9 | }); 10 | } 11 | 12 | // 用户退出了 13 | export async function signOut() { 14 | // 清除TOKEN,模拟退出 15 | Store.clearAll(); 16 | return true; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/routes/Dashboard/Monitor.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { connect } from 'dva'; 3 | 4 | @connect(state => ({ 5 | monitor: state.monitor, 6 | })) 7 | export default class Monitor extends PureComponent { 8 | state = {} 9 | 10 | componentDidMount() {} 11 | 12 | render() { 13 | return ( 14 |
15 | 监控页 16 |
17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/models/index.js: -------------------------------------------------------------------------------- 1 | // Use require.context to require reducers automatically 2 | // Ref: https://webpack.github.io/docs/context.html 3 | const context = require.context('./', false, /\.js$/); 4 | const keys = context.keys().filter(item => item !== './index.js'); 5 | 6 | const models = []; 7 | for (let i = 0; i < keys.length; i += 1) { 8 | models.push(context(keys[i])); 9 | } 10 | 11 | export default models; 12 | -------------------------------------------------------------------------------- /src/routes/Dashboard/Workplace.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { connect } from 'dva'; 3 | 4 | @connect(state => ({ 5 | monitor: state.monitor, 6 | })) 7 | export default class Workplace extends PureComponent { 8 | state = {} 9 | 10 | componentDidMount() {} 11 | 12 | render() { 13 | return ( 14 |
15 | 工作台 16 |
17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/routes/Forms/BasicForm.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { connect } from 'dva'; 3 | 4 | @connect(state => ({ 5 | monitor: state.monitor, 6 | })) 7 | export default class BasicForms extends PureComponent { 8 | state = {} 9 | 10 | componentDidMount() {} 11 | 12 | render() { 13 | return ( 14 |
15 | 基础表单 16 |
17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/routes/Dashboard/Analysis.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'dva'; 3 | 4 | @connect(state => ({ 5 | chart: state.chart, 6 | })) 7 | export default class Analysis extends Component { 8 | state = {} 9 | 10 | componentDidMount() {} 11 | 12 | componentWillUnmount() { 13 | } 14 | 15 | render() { 16 | return ( 17 |
18 | 分析页 19 |
20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/index.less: -------------------------------------------------------------------------------- 1 | body, html { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | body { 7 | height: 100%; 8 | overflow-y: hidden; 9 | background-color: #f4f7ed; 10 | text-rendering: optimizeLegibility; 11 | -webkit-font-smoothing: antialiased; 12 | -moz-osx-font-smoothing: grayscale; 13 | } 14 | 15 | :global(#root) { 16 | height: 100vh; 17 | position: relative; 18 | } 19 | 20 | .globalSpin { 21 | width: 100%; 22 | margin: 40px 0 !important; 23 | } 24 | -------------------------------------------------------------------------------- /src/models/user.js: -------------------------------------------------------------------------------- 1 | import { queryCurrent } from '../services/user'; 2 | 3 | export default { 4 | namespace: 'user', 5 | state: { 6 | currentUser: {}, 7 | }, 8 | 9 | effects: { 10 | *fetchCurrent({ payload }, { call, put }) { 11 | const response = yield call(queryCurrent, payload); 12 | yield put({ 13 | type: 'saveCurrentUser', 14 | payload: response, 15 | }); 16 | }, 17 | }, 18 | 19 | reducers: { 20 | saveCurrentUser(state, action) { 21 | return { 22 | ...state, 23 | currentUser: action.payload[0], 24 | }; 25 | }, 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /.webpackrc: -------------------------------------------------------------------------------- 1 | { 2 | "entry": "src/app.js", 3 | "publicPath": "/", 4 | "extraBabelPlugins": [ 5 | ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": true }] 6 | ], 7 | "env": { 8 | "development": { 9 | "extraBabelPlugins": [ 10 | "dva-hmr" 11 | ] 12 | } 13 | }, 14 | "browserslist": [ 15 | ">1%", 16 | "last 4 versions", 17 | "Firefox ESR", 18 | "not ie < 9" // React doesn't support IE8 anyway 19 | ], 20 | "externals": {}, 21 | "ignoreMomentLocale": true, 22 | "theme": "./src/components/theme.js", 23 | "html": { 24 | "template": "./src/index.ejs" 25 | }, 26 | "hash": true 27 | } 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "8" 5 | 6 | env: 7 | matrix: 8 | - TEST_TYPE=lint 9 | - TEST_TYPE=test-all 10 | - TEST_TYPE=test-dist 11 | 12 | addons: 13 | apt: 14 | packages: 15 | - xvfb 16 | 17 | install: 18 | - export DISPLAY=':99.0' 19 | - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 20 | - npm install 21 | 22 | script: 23 | - | 24 | if [ "$TEST_TYPE" = lint ]; then 25 | npm run lint 26 | elif [ "$TEST_TYPE" = test-all ]; then 27 | npm run test:all 28 | elif [ "$TEST_TYPE" = test-dist ]; then 29 | npm run site 30 | mv dist/* ./ 31 | php -S localhost:8000 & 32 | npm test .e2e.js 33 | fi 34 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import '@babel/polyfill'; 2 | import dva from 'dva'; 3 | import 'moment/locale/zh-cn'; 4 | import browserHistory from 'history/createBrowserHistory'; 5 | // import { createLogger } from 'redux-logger'; 6 | import { message } from 'antd'; 7 | import './rollbar'; 8 | import './index.less'; 9 | import router from './router'; 10 | 11 | // 1. 创建应用,返回 dva 实例 12 | const app = dva({ 13 | history: browserHistory(), 14 | onError(e) { 15 | message.error(e.message, /* duration */3); 16 | }, 17 | // onAction: createLogger({}), 18 | }); 19 | 20 | // 2. 配置 hooks 或者注册插件 21 | // app.use({}); 22 | 23 | // 3. 注册 model 24 | app.model(require('./models/global').default); 25 | 26 | // 4. 注册路由表 27 | app.router(router); 28 | 29 | // 5. 启动应用 30 | app.start('#root'); 31 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard", 3 | "rules": { 4 | "selector-pseudo-class-no-unknown": null, 5 | "shorthand-property-no-redundant-values": null, 6 | "at-rule-empty-line-before": null, 7 | "at-rule-name-space-after": null, 8 | "comment-empty-line-before": null, 9 | "declaration-bang-space-before": null, 10 | "declaration-empty-line-before": null, 11 | "function-comma-newline-after": null, 12 | "function-name-case": null, 13 | "function-parentheses-newline-inside": null, 14 | "function-max-empty-lines": null, 15 | "function-whitespace-after": null, 16 | "number-leading-zero": null, 17 | "number-no-trailing-zeros": null, 18 | "rule-empty-line-before": null, 19 | "selector-combinator-space-after": null, 20 | "selector-list-comma-newline-after": null, 21 | "selector-pseudo-element-colon-notation": null, 22 | "unit-no-unknown": null, 23 | "value-list-max-empty-lines": null 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/routes/User/Login.less: -------------------------------------------------------------------------------- 1 | @import "~antd/lib/style/themes/default.less"; 2 | 3 | .login-form { 4 | position: absolute; 5 | top: 50%; 6 | left: 50%; 7 | margin: -160px 0 0 -160px; 8 | width: 320px; 9 | height: 320px; 10 | padding: 36px; 11 | box-shadow: 0 0 100px rgba(0, 0, 0, .08); 12 | background-color: #fff; 13 | border-radius: 4px; 14 | .login-logo { 15 | text-align: center; 16 | height: 40px; 17 | line-height: 40px; 18 | cursor: pointer; 19 | margin-bottom: 24px; 20 | img { 21 | width: 40px; 22 | margin-right: 8px; 23 | } 24 | span { 25 | font-size: 16px; 26 | text-transform: uppercase; 27 | display: inline-block; 28 | } 29 | } 30 | button { 31 | width: 100%; 32 | } 33 | .login-account { 34 | color: #999; 35 | text-align: center; 36 | margin-top: 16px; 37 | span { 38 | &:first-child { 39 | margin-right: 16px; 40 | } 41 | } 42 | } 43 | :global { 44 | .ant-form-item { 45 | margin-bottom: 24px; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/utils/utils.less: -------------------------------------------------------------------------------- 1 | .textOverflow() { 2 | overflow: hidden; 3 | text-overflow: ellipsis; 4 | word-break: break-all; 5 | white-space: nowrap; 6 | } 7 | 8 | .textOverflowMulti(@line: 3, @bg: #fff) { 9 | overflow: hidden; 10 | position: relative; 11 | line-height: 1.5em; 12 | max-height: @line * 1.5em; 13 | text-align: justify; 14 | margin-right: -1em; 15 | padding-right: 1em; 16 | &:before { 17 | background: @bg; 18 | content: '...'; 19 | padding: 0 1px; 20 | position: absolute; 21 | right: 14px; 22 | bottom: 0; 23 | } 24 | &:after { 25 | background: white; 26 | content: ''; 27 | margin-top: 0.2em; 28 | position: absolute; 29 | right: 14px; 30 | width: 1em; 31 | height: 1em; 32 | } 33 | } 34 | 35 | // mixins for clearfix 36 | // ------------------------ 37 | .clearfix() { 38 | zoom: 1; 39 | &:before, 40 | &:after { 41 | content: " "; 42 | display: table; 43 | } 44 | &:after { 45 | clear: both; 46 | visibility: hidden; 47 | font-size: 0; 48 | height: 0; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/models/login.js: -------------------------------------------------------------------------------- 1 | import { routerRedux } from 'dva/router'; 2 | import { signIn, signOut } from '../services/api'; 3 | 4 | export default { 5 | namespace: 'login', 6 | 7 | state: { 8 | status: undefined, 9 | }, 10 | 11 | effects: { 12 | *accountSubmit({ payload }, { call, put }) { 13 | yield put({ 14 | type: 'changeSubmitting', 15 | payload: true, 16 | }); 17 | const response = yield call(signIn, payload); 18 | yield put({ 19 | type: 'changeLoginStatus', 20 | payload: response, 21 | }); 22 | yield put({ 23 | type: 'changeSubmitting', 24 | payload: false, 25 | }); 26 | }, 27 | *logout(_, { call, put }) { 28 | const response = yield call(signOut); 29 | if (response) { 30 | yield put(routerRedux.push('/user/login')); 31 | } 32 | }, 33 | }, 34 | 35 | reducers: { 36 | changeLoginStatus(state, { payload }) { 37 | return { 38 | ...state, 39 | status: payload.length > 0 ? 'ok' : 'error', 40 | type: payload.type, 41 | info: payload, 42 | }; 43 | }, 44 | changeSubmitting(state, { payload }) { 45 | return { 46 | ...state, 47 | status: payload, 48 | submitting: payload, 49 | }; 50 | }, 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /src/layouts/UserLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Route } from 'dva/router'; 4 | import DocumentTitle from 'react-document-title'; 5 | import styles from './UserLayout.less'; 6 | 7 | class UserLayout extends React.PureComponent { 8 | static childContextTypes = { 9 | location: PropTypes.object, 10 | } 11 | getChildContext() { 12 | const { location } = this.props; 13 | return { location }; 14 | } 15 | getPageTitle() { 16 | const { getRouteData, location } = this.props; 17 | const { pathname } = location; 18 | let title = 'react antd dva'; 19 | getRouteData('UserLayout').forEach((item) => { 20 | if (item.path === pathname) { 21 | title = `${item.name} - react antd dva`; 22 | } 23 | }); 24 | return title; 25 | } 26 | render() { 27 | const { getRouteData } = this.props; 28 | return ( 29 | 30 |
31 | { 32 | getRouteData('UserLayout').map(item => 33 | ( 34 | 40 | ) 41 | ) 42 | } 43 |
44 |
45 | ); 46 | } 47 | } 48 | 49 | export default UserLayout; 50 | -------------------------------------------------------------------------------- /mock/utils.js: -------------------------------------------------------------------------------- 1 | export const imgMap = { 2 | user: 'https://gw.alipayobjects.com/zos/rmsportal/UjusLxePxWGkttaqqmUI.png', 3 | a: 'https://gw.alipayobjects.com/zos/rmsportal/ZrkcSjizAKNWwJTwcadT.png', 4 | b: 'https://gw.alipayobjects.com/zos/rmsportal/KYlwHMeomKQbhJDRUVvt.png', 5 | c: 'https://gw.alipayobjects.com/zos/rmsportal/gabvleTstEvzkbQRfjxu.png', 6 | d: 'https://gw.alipayobjects.com/zos/rmsportal/jvpNzacxUYLlNsHTtrAD.png', 7 | }; 8 | 9 | // refers: https://www.sitepoint.com/get-url-parameters-with-javascript/ 10 | export function getUrlParams(url) { 11 | const d = decodeURIComponent; 12 | let queryString = url ? url.split('?')[1] : window.location.search.slice(1); 13 | const obj = {}; 14 | if (queryString) { 15 | queryString = queryString.split('#')[0]; // eslint-disable-line 16 | const arr = queryString.split('&'); 17 | for (let i = 0; i < arr.length; i += 1) { 18 | const a = arr[i].split('='); 19 | let paramNum; 20 | const paramName = a[0].replace(/\[\d*\]/, (v) => { 21 | paramNum = v.slice(1, -1); 22 | return ''; 23 | }); 24 | const paramValue = typeof (a[1]) === 'undefined' ? true : a[1]; 25 | if (obj[paramName]) { 26 | if (typeof obj[paramName] === 'string') { 27 | obj[paramName] = d([obj[paramName]]); 28 | } 29 | if (typeof paramNum === 'undefined') { 30 | obj[paramName].push(d(paramValue)); 31 | } else { 32 | obj[paramName][paramNum] = d(paramValue); 33 | } 34 | } else { 35 | obj[paramName] = d(paramValue); 36 | } 37 | } 38 | } 39 | return obj; 40 | } 41 | 42 | export default { 43 | getUrlParams, 44 | imgMap, 45 | }; 46 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "plugins": ["compat"], 5 | "env": { 6 | "browser": true, 7 | "node": true, 8 | "es6": true, 9 | "mocha": true, 10 | "jest": true, 11 | "jasmine": true 12 | }, 13 | "rules": { 14 | "linebreak-style": "off", 15 | "generator-star-spacing": [0], 16 | "consistent-return": [0], 17 | "react/forbid-prop-types": [0], 18 | "react/jsx-filename-extension": [1, { "extensions": [".js"] }], 19 | "global-require": [1], 20 | "import/prefer-default-export": [0], 21 | "react/jsx-no-bind": [0], 22 | "react/prop-types": [0], 23 | "react/prefer-stateless-function": [0], 24 | "no-else-return": [0], 25 | "no-restricted-syntax": [0], 26 | "import/no-extraneous-dependencies": [0], 27 | "no-use-before-define": [0], 28 | "jsx-a11y/no-static-element-interactions": [0], 29 | "jsx-a11y/no-noninteractive-element-interactions": [0], 30 | "jsx-a11y/click-events-have-key-events": [0], 31 | "jsx-a11y/anchor-is-valid": [0], 32 | "no-nested-ternary": [0], 33 | "arrow-body-style": [0], 34 | "import/extensions": [0], 35 | "no-bitwise": [0], 36 | "no-cond-assign": [0], 37 | "import/no-unresolved": [0], 38 | "comma-dangle": ["error", { 39 | "arrays": "always-multiline", 40 | "objects": "always-multiline", 41 | "imports": "always-multiline", 42 | "exports": "always-multiline", 43 | "functions": "ignore" 44 | }], 45 | "object-curly-newline": [0], 46 | "function-paren-newline": [0], 47 | "no-restricted-globals": [0], 48 | "require-yield": [1], 49 | "compat/compat": "error" 50 | }, 51 | "parserOptions": { 52 | "ecmaFeatures": { 53 | "experimentalObjectRestSpread": true 54 | } 55 | }, 56 | "settings": { 57 | "polyfills": ["fetch", "promises"] 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | import fetch from 'dva/fetch'; 2 | import { notification } from 'antd'; 3 | import Config from '../common/config'; 4 | 5 | function checkStatus(response) { 6 | if (response.status >= 200 && response.status < 300) { 7 | return response; 8 | } 9 | notification.error({ 10 | message: `请求错误 ${response.status}: ${response.url}`, 11 | description: response.statusText, 12 | }); 13 | const error = new Error(response.statusText); 14 | error.response = response; 15 | throw error; 16 | } 17 | 18 | /** 19 | * Requests a URL, returning a promise. 20 | * 21 | * @param {string} url The URL we want to request 22 | * @param {object} [options] The options we want to pass to "fetch" 23 | * @return {object} An object containing either "data" or "err" 24 | */ 25 | export default function request(sUrl, options) { 26 | const url = Config.api + sUrl; 27 | const defaultOptions = { 28 | mode: 'cors', 29 | // cache: 'force-cache', 表示fetch请求不顾一切的依赖缓存, 即使缓存过期了, 它依然从缓存中读取. 除非没有任何缓存, 那么它将发送一个正常的request. 30 | // credentials: 'include', Fetch 请求默认是不带 cookie 的,需要设置 fetch(url, {credentials: 'include'}) 31 | }; 32 | const newOptions = { ...defaultOptions, ...options }; 33 | if (newOptions.method === 'POST' || newOptions.method === 'PUT') { 34 | newOptions.headers = { 35 | Accept: 'application/json', 36 | 'Content-Type': 'application/json; charset=utf-8', 37 | ...newOptions.headers, 38 | }; 39 | newOptions.body = JSON.stringify(newOptions.body); 40 | } 41 | 42 | return fetch(url, newOptions) 43 | .then(checkStatus) 44 | .then(response => response.json()) 45 | .catch((error) => { 46 | if (error.code) { 47 | notification.error({ 48 | message: error.name, 49 | description: error.message, 50 | }); 51 | } 52 | if ('stack' in error && 'message' in error) { 53 | notification.error({ 54 | message: `请求错误: ${url}`, 55 | description: error.message, 56 | }); 57 | } 58 | return error; 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /src/common/nav.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 我们为了统一方便的管理路由和页面的关系,将配置信息统一抽离到 common/nav.js 下,同时应用动态路由 3 | */ 4 | 5 | import dynamic from 'dva/dynamic'; 6 | 7 | // dynamic包装 函数 8 | const dynamicWrapper = (app, models, component) => dynamic({ 9 | app, 10 | models: () => models.map(m => import(`../models/${m}.js`)), 11 | component, 12 | }); 13 | 14 | // nav data 15 | export const getNavData = app => [ 16 | { 17 | component: dynamicWrapper(app, ['user', 'login'], () => import('../layouts/BasicLayout')), 18 | layout: 'BasicLayout', 19 | name: '首页', 20 | path: '/', 21 | children: [ 22 | { 23 | name: 'Dashboard', 24 | icon: 'dashboard', 25 | path: 'dashboard', 26 | children: [ 27 | { 28 | name: '分析页', 29 | path: 'analysis', 30 | component: dynamicWrapper(app, [], () => import('../routes/Dashboard/Analysis')), 31 | }, 32 | { 33 | name: '监控页', 34 | path: 'monitor', 35 | component: dynamicWrapper(app, [], () => import('../routes/Dashboard/Monitor')), 36 | }, 37 | { 38 | name: '工作台', 39 | path: 'workplace', 40 | component: dynamicWrapper(app, [], () => import('../routes/Dashboard/Workplace')), 41 | }, 42 | ], 43 | }, 44 | { 45 | name: '表单页', 46 | path: 'form', 47 | icon: 'form', 48 | children: [ 49 | { 50 | name: '基础表单', 51 | path: 'basic-form', 52 | component: dynamicWrapper(app, [], () => import('../routes/Forms/BasicForm')), 53 | }, 54 | ], 55 | }, 56 | ], 57 | }, 58 | { 59 | component: dynamicWrapper(app, [], () => import('../layouts/UserLayout')), 60 | path: '/user', 61 | layout: 'UserLayout', 62 | children: [ 63 | { 64 | name: '账户', 65 | icon: 'user', 66 | path: 'user', 67 | children: [ 68 | { 69 | name: '登录', 70 | path: 'login', 71 | component: dynamicWrapper(app, ['login'], () => import('../routes/User/Login')), 72 | }, 73 | ], 74 | }, 75 | ], 76 | }, 77 | ]; 78 | -------------------------------------------------------------------------------- /src/models/global.js: -------------------------------------------------------------------------------- 1 | import { messageError } from '../utils/utils'; 2 | 3 | export default { 4 | namespace: 'global', // model 的命名空间,同时也是他在全局 state 上的属性,只能用字符串,不支持通过 . 的方式创建多层命名空间 5 | 6 | state: { // 初始值,优先级低于传给 dva() 的 opts.initialState。 7 | collapsed: false, 8 | messageStatus: false, 9 | }, 10 | 11 | /** 12 | * 以 key/value 格式定义 effect。用于处理异步操作和业务逻辑,不直接修改 state。由 action 触发, 13 | * 可以触发 action,可以和服务器交互,可以获取全局 state 的数据等等。 14 | * '格式为 *(action, effects) => void 或 [*(action, effects) => void, { type }]。 15 | * type 类型有: 16 | * takeEvery 17 | * takeLatest 18 | * throttle 19 | * watcher 20 | */ 21 | effects: { 22 | *changeMessage({ payload }, { call, put }) { 23 | yield put({ 24 | type: 'changeMessageStatus', 25 | payload: true, 26 | }); 27 | const response = yield call(messageError, payload); 28 | yield put({ 29 | type: 'changeMessageStatus', 30 | payload: response, 31 | }); 32 | }, 33 | }, 34 | 35 | /** 36 | * 以 key/value 格式定义 reducer。用于处理同步操作,唯一可以修改 state 的地方。由 action 触发。 37 | * 格式为 (state, action) => newState 或 [(state, action) => newState, enhancer]。 38 | */ 39 | reducers: { 40 | changeLayoutCollapsed(state, { payload }) { 41 | return { 42 | ...state, 43 | collapsed: payload, 44 | }; 45 | }, 46 | changeMessageStatus(state, { payload }) { 47 | return { 48 | ...state, 49 | messageStatus: payload, 50 | }; 51 | }, 52 | }, 53 | 54 | /** 55 | * 以 key/value 格式定义 subscription。subscription 是订阅,用于订阅一个数据源, 56 | * 然后根据需要 dispatch 相应的 action。在 app.start() 时被执行,数据源可以是当前的时间、 57 | * 服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。 58 | * 格式为 ({ dispatch, history }, done) => unlistenFunction。 59 | * 注意:如果要使用 app.unmodel(),subscription 必须返回 unlisten 方法,用于取消数据订阅。 60 | */ 61 | subscriptions: { 62 | setup({ history }) { 63 | // Subscribe history(url) change, trigger `load` action if pathname is `/` 64 | return history.listen(({ pathname, search }) => { 65 | if (typeof window.ga !== 'undefined') { 66 | window.ga('send', 'pageview', pathname + search); 67 | } 68 | }); 69 | }, 70 | }, 71 | }; 72 | -------------------------------------------------------------------------------- /.roadhogrc.mock.js: -------------------------------------------------------------------------------- 1 | import mockjs from 'mockjs'; 2 | import { getRule, postRule } from './mock/rule'; 3 | import { getActivities, getNotice, getFakeList } from './mock/api'; 4 | import { getFakeChartData } from './mock/chart'; 5 | import { imgMap } from './mock/utils'; 6 | import { getProfileBasicData } from './mock/profile'; 7 | import { getProfileAdvancedData } from './mock/profile'; 8 | import { getNotices } from './mock/notices'; 9 | import { format, delay } from 'roadhog-api-doc'; 10 | 11 | // 是否禁用代理 12 | const noProxy = process.env.NO_PROXY === 'true'; 13 | 14 | // 代码中会兼容本地 service mock 以及部署站点的静态数据 15 | const proxy = { 16 | // 支持值为 Object 和 Array 17 | 'GET /api/currentUser': { 18 | $desc: "获取当前用户接口", 19 | $params: { 20 | pageSize: { 21 | desc: '分页', 22 | exp: 2, 23 | }, 24 | }, 25 | $body: { 26 | name: 'Serati Ma', 27 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/dRFVcIqZOYPcSNrlJsqQ.png', 28 | userid: '00000001', 29 | notifyCount: 12, 30 | }, 31 | }, 32 | // GET POST 可省略 33 | 'GET /api/users': [{ 34 | key: '1', 35 | name: 'John Brown', 36 | age: 32, 37 | address: 'New York No. 1 Lake Park', 38 | }, { 39 | key: '2', 40 | name: 'Jim Green', 41 | age: 42, 42 | address: 'London No. 1 Lake Park', 43 | }, { 44 | key: '3', 45 | name: 'Joe Black', 46 | age: 32, 47 | address: 'Sidney No. 1 Lake Park', 48 | }], 49 | 'GET /api/project/notice': getNotice, 50 | 'GET /api/activities': getActivities, 51 | 'GET /api/rule': getRule, 52 | 'POST /api/rule': { 53 | $params: { 54 | pageSize: { 55 | desc: '分页', 56 | exp: 2, 57 | }, 58 | }, 59 | $body: postRule, 60 | }, 61 | 'POST /api/forms': (req, res) => { 62 | res.send({ message: 'Ok' }); 63 | }, 64 | 'GET /api/tags': mockjs.mock({ 65 | 'list|100': [{ name: '@city', 'value|1-100': 150, 'type|0-2': 1 }] 66 | }), 67 | 'GET /api/fake_list': getFakeList, 68 | 'GET /api/fake_chart_data': getFakeChartData, 69 | 'GET /api/profile/basic': getProfileBasicData, 70 | 'GET /api/profile/advanced': getProfileAdvancedData, 71 | 'POST /api/login/account': (req, res) => { 72 | const { password, userName } = req.body; 73 | res.send({ status: password === 'sosout' && userName === 'sosout' ? 'ok' : 'error', type: 'account' }); 74 | }, 75 | 'POST /api/login/mobile': (req, res) => { 76 | res.send({ status: 'ok', type: 'mobile' }); 77 | }, 78 | 'POST /api/register': (req, res) => { 79 | res.send({ status: 'ok' }); 80 | }, 81 | 'GET /api/notices': getNotices, 82 | }; 83 | 84 | export default noProxy ? {} : delay(proxy, 1000); 85 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Router, Route, Switch, Redirect } from 'dva/router'; 3 | import { LocaleProvider, Spin } from 'antd'; 4 | import zhCN from 'antd/lib/locale-provider/zh_CN'; 5 | import dynamic from 'dva/dynamic'; 6 | import cloneDeep from 'lodash/cloneDeep'; 7 | import Store from 'store'; 8 | import { getNavData } from './common/nav'; 9 | import { getPlainNode } from './utils/utils'; 10 | 11 | import styles from './index.less'; 12 | import Config from './common/config'; 13 | 14 | // 设置默认的加载组件 15 | dynamic.setDefaultLoadingComponent(() => { 16 | return ; 17 | }); 18 | 19 | function getRouteData(navData, path) { 20 | if (!navData.some(item => item.layout === path) || 21 | !(navData.filter(item => item.layout === path)[0].children)) { 22 | return null; 23 | } 24 | const route = cloneDeep(navData.filter(item => item.layout === path)[0]); 25 | const nodeList = getPlainNode(route.children); 26 | return nodeList; 27 | } 28 | 29 | function getLayout(navData, path) { 30 | if (!navData.some(item => item.layout === path) || 31 | !(navData.filter(item => item.layout === path)[0].children)) { 32 | return null; 33 | } 34 | const route = navData.filter(item => item.layout === path)[0]; 35 | return { 36 | component: route.component, 37 | layout: route.layout, 38 | name: route.name, 39 | path: route.path, 40 | }; 41 | } 42 | 43 | // 登录验证 44 | function requireAuth(Layout, props, passProps) { 45 | // 模拟token失效时间 46 | const token = Store.get(Config.USER_TOKEN); 47 | const current = (new Date()).getTime(); 48 | if (token && current - token < 7200000) { 49 | return ; 50 | } else { 51 | return ; 52 | } 53 | } 54 | 55 | function RouterConfig({ history, app }) { 56 | const navData = getNavData(app); 57 | const UserLayout = getLayout(navData, 'UserLayout').component; 58 | const BasicLayout = getLayout(navData, 'BasicLayout').component; 59 | const passProps = { 60 | app, 61 | navData: navData.filter((item) => { 62 | return item.layout !== 'UserLayout'; 63 | }), // 剔除掉无需登录模块 64 | getRouteData: (path) => { 65 | return getRouteData(navData, path); 66 | }, 67 | }; 68 | return ( 69 | 70 | 71 | 72 | } /> 73 | requireAuth(BasicLayout, props, passProps)} /> 74 | 75 | 76 | 77 | 78 | ); 79 | } 80 | 81 | export default RouterConfig; 82 | -------------------------------------------------------------------------------- /mock/notices.js: -------------------------------------------------------------------------------- 1 | export default { 2 | getNotices(req, res) { 3 | res.json([{ 4 | id: '000000001', 5 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png', 6 | title: '你收到了 14 份新周报', 7 | datetime: '2017-08-09', 8 | type: '通知', 9 | }, { 10 | id: '000000002', 11 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png', 12 | title: '你推荐的 曲妮妮 已通过第三轮面试', 13 | datetime: '2017-08-08', 14 | type: '通知', 15 | }, { 16 | id: '000000003', 17 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png', 18 | title: '这种模板可以区分多种通知类型', 19 | datetime: '2017-08-07', 20 | read: true, 21 | type: '通知', 22 | }, { 23 | id: '000000004', 24 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png', 25 | title: '左侧图标用于区分不同的类型', 26 | datetime: '2017-08-07', 27 | type: '通知', 28 | }, { 29 | id: '000000005', 30 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png', 31 | title: '内容不要超过两行字,超出时自动截断', 32 | datetime: '2017-08-07', 33 | type: '通知', 34 | }, { 35 | id: '000000006', 36 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', 37 | title: '曲丽丽 评论了你', 38 | description: '描述信息描述信息描述信息', 39 | datetime: '2017-08-07', 40 | type: '消息', 41 | }, { 42 | id: '000000007', 43 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', 44 | title: '朱偏右 回复了你', 45 | description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像', 46 | datetime: '2017-08-07', 47 | type: '消息', 48 | }, { 49 | id: '000000008', 50 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', 51 | title: '标题', 52 | description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像', 53 | datetime: '2017-08-07', 54 | type: '消息', 55 | }, { 56 | id: '000000009', 57 | title: '任务名称', 58 | description: '任务需要在 2017-01-12 20:00 前启动', 59 | extra: '未开始', 60 | status: 'todo', 61 | type: '待办', 62 | }, { 63 | id: '000000010', 64 | title: '第三方紧急代码变更', 65 | description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务', 66 | extra: '马上到期', 67 | status: 'urgent', 68 | type: '待办', 69 | }, { 70 | id: '000000011', 71 | title: '信息安全考试', 72 | description: '指派竹尔于 2017-01-09 前完成更新并发布', 73 | extra: '已耗时 8 天', 74 | status: 'doing', 75 | type: '待办', 76 | }, { 77 | id: '000000012', 78 | title: 'ABCD 版本发布', 79 | description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务', 80 | extra: '进行中', 81 | status: 'processing', 82 | type: '待办', 83 | }]); 84 | }, 85 | }; 86 | -------------------------------------------------------------------------------- /src/utils/utils.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { message } from 'antd'; 3 | 4 | export function fixedZero(val) { 5 | return val * 1 < 10 ? `0${val}` : val; 6 | } 7 | 8 | export function getTimeDistance(type) { 9 | const now = new Date(); 10 | const oneDay = 1000 * 60 * 60 * 24; 11 | 12 | if (type === 'today') { 13 | now.setHours(0); 14 | now.setMinutes(0); 15 | now.setSeconds(0); 16 | return [moment(now), moment(now.getTime() + (oneDay - 1000))]; 17 | } 18 | 19 | if (type === 'week') { 20 | let day = now.getDay(); 21 | now.setHours(0); 22 | now.setMinutes(0); 23 | now.setSeconds(0); 24 | 25 | if (day === 0) { 26 | day = 6; 27 | } else { 28 | day -= 1; 29 | } 30 | 31 | const beginTime = now.getTime() - (day * oneDay); 32 | 33 | return [moment(beginTime), moment(beginTime + ((7 * oneDay) - 1000))]; 34 | } 35 | 36 | if (type === 'month') { 37 | const year = now.getFullYear(); 38 | const month = now.getMonth(); 39 | const nextDate = moment(now).add(1, 'months'); 40 | const nextYear = nextDate.year(); 41 | const nextMonth = nextDate.month(); 42 | 43 | return [moment(`${year}-${fixedZero(month + 1)}-01 00:00:00`), moment(moment(`${nextYear}-${fixedZero(nextMonth + 1)}-01 00:00:00`).valueOf() - 1000)]; 44 | } 45 | 46 | if (type === 'year') { 47 | const year = now.getFullYear(); 48 | 49 | return [moment(`${year}-01-01 00:00:00`), moment(`${year}-12-31 23:59:59`)]; 50 | } 51 | } 52 | 53 | export function getPlainNode(nodeList, parentPath = '') { 54 | const arr = []; 55 | nodeList.forEach((node) => { 56 | const item = node; 57 | item.path = `${parentPath}/${item.path || ''}`.replace(/\/+/g, '/'); 58 | item.exact = true; 59 | if (item.children && !item.component) { 60 | arr.push(...getPlainNode(item.children, item.path)); 61 | } else { 62 | if (item.children && item.component) { 63 | item.exact = false; 64 | } 65 | arr.push(item); 66 | } 67 | }); 68 | return arr; 69 | } 70 | 71 | export function digitUppercase(n) { 72 | const fraction = ['角', '分']; 73 | const digit = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖']; 74 | const unit = [ 75 | ['元', '万', '亿'], 76 | ['', '拾', '佰', '仟'], 77 | ]; 78 | let num = Math.abs(n); 79 | let s = ''; 80 | fraction.forEach((item, index) => { 81 | s += (digit[Math.floor(num * 10 * (10 ** index)) % 10] + item).replace(/零./, ''); 82 | }); 83 | s = s || '整'; 84 | num = Math.floor(num); 85 | for (let i = 0; i < unit[0].length && num > 0; i += 1) { 86 | let p = ''; 87 | for (let j = 0; j < unit[1].length && num > 0; j += 1) { 88 | p = digit[num % 10] + unit[1][j] + p; 89 | num = Math.floor(num / 10); 90 | } 91 | s = p.replace(/(零.)*零$/, '').replace(/^$/, '零') + unit[0][i] + s; 92 | } 93 | 94 | return s.replace(/(零.)*零元/, '元').replace(/(零.)+/g, '零').replace(/^整$/, '零元整'); 95 | } 96 | 97 | // 弹窗未完全关闭禁止再次提交 98 | export function messageError(payload) { 99 | return new Promise((resolve) => { 100 | message.error(payload, () => { 101 | resolve(false); 102 | }); 103 | }); 104 | } 105 | -------------------------------------------------------------------------------- /mock/profile.js: -------------------------------------------------------------------------------- 1 | const basicGoods = [ 2 | { 3 | id: '1234561', 4 | name: '矿泉水 550ml', 5 | barcode: '12421432143214321', 6 | price: '2.00', 7 | num: '1', 8 | amount: '2.00', 9 | }, 10 | { 11 | id: '1234562', 12 | name: '凉茶 300ml', 13 | barcode: '12421432143214322', 14 | price: '3.00', 15 | num: '2', 16 | amount: '6.00', 17 | }, 18 | { 19 | id: '1234563', 20 | name: '好吃的薯片', 21 | barcode: '12421432143214323', 22 | price: '7.00', 23 | num: '4', 24 | amount: '28.00', 25 | }, 26 | { 27 | id: '1234564', 28 | name: '特别好吃的蛋卷', 29 | barcode: '12421432143214324', 30 | price: '8.50', 31 | num: '3', 32 | amount: '25.50', 33 | }, 34 | ]; 35 | 36 | const basicProgress = [ 37 | { 38 | key: '1', 39 | time: '2017-10-01 14:10', 40 | rate: '联系客户', 41 | status: 'processing', 42 | operator: '取货员 ID1234', 43 | cost: '5mins', 44 | }, 45 | { 46 | key: '2', 47 | time: '2017-10-01 14:05', 48 | rate: '取货员出发', 49 | status: 'success', 50 | operator: '取货员 ID1234', 51 | cost: '1h', 52 | }, 53 | { 54 | key: '3', 55 | time: '2017-10-01 13:05', 56 | rate: '取货员接单', 57 | status: 'success', 58 | operator: '取货员 ID1234', 59 | cost: '5mins', 60 | }, 61 | { 62 | key: '4', 63 | time: '2017-10-01 13:00', 64 | rate: '申请审批通过', 65 | status: 'success', 66 | operator: '系统', 67 | cost: '1h', 68 | }, 69 | { 70 | key: '5', 71 | time: '2017-10-01 12:00', 72 | rate: '发起退货申请', 73 | status: 'success', 74 | operator: '用户', 75 | cost: '5mins', 76 | }, 77 | ]; 78 | 79 | const advancedOperation1 = [ 80 | { 81 | key: 'op1', 82 | type: '订购关系生效', 83 | name: '曲丽丽', 84 | status: 'agree', 85 | updatedAt: '2017-10-03 19:23:12', 86 | memo: '-', 87 | }, 88 | { 89 | key: 'op2', 90 | type: '财务复审', 91 | name: '付小小', 92 | status: 'reject', 93 | updatedAt: '2017-10-03 19:23:12', 94 | memo: '不通过原因', 95 | }, 96 | { 97 | key: 'op3', 98 | type: '部门初审', 99 | name: '周毛毛', 100 | status: 'agree', 101 | updatedAt: '2017-10-03 19:23:12', 102 | memo: '-', 103 | }, 104 | { 105 | key: 'op4', 106 | type: '提交订单', 107 | name: '林东东', 108 | status: 'agree', 109 | updatedAt: '2017-10-03 19:23:12', 110 | memo: '很棒', 111 | }, 112 | { 113 | key: 'op5', 114 | type: '创建订单', 115 | name: '汗牙牙', 116 | status: 'agree', 117 | updatedAt: '2017-10-03 19:23:12', 118 | memo: '-', 119 | }, 120 | ]; 121 | 122 | const advancedOperation2 = [ 123 | { 124 | key: 'op1', 125 | type: '订购关系生效', 126 | name: '曲丽丽', 127 | status: 'agree', 128 | updatedAt: '2017-10-03 19:23:12', 129 | memo: '-', 130 | }, 131 | ]; 132 | 133 | const advancedOperation3 = [ 134 | { 135 | key: 'op1', 136 | type: '创建订单', 137 | name: '汗牙牙', 138 | status: 'agree', 139 | updatedAt: '2017-10-03 19:23:12', 140 | memo: '-', 141 | }, 142 | ]; 143 | 144 | export const getProfileBasicData = { 145 | basicGoods, 146 | basicProgress, 147 | }; 148 | 149 | export const getProfileAdvancedData = { 150 | advancedOperation1, 151 | advancedOperation2, 152 | advancedOperation3, 153 | }; 154 | 155 | export default { 156 | getProfileBasicData, 157 | getProfileAdvancedData, 158 | }; 159 | -------------------------------------------------------------------------------- /src/routes/User/Login.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'dva'; 3 | import { routerRedux } from 'dva/router'; 4 | import { Form, Input, Button, Icon } from 'antd'; 5 | import Store from 'store'; 6 | import Config from '../../common/config'; 7 | import styles from './Login.less'; 8 | 9 | const FormItem = Form.Item; 10 | 11 | @connect(state => ({ 12 | login: state.login, 13 | messageStatus: state.global.messageStatus, 14 | })) 15 | @Form.create() 16 | 17 | export default class Login extends Component { 18 | state = { 19 | type: 'account', 20 | } 21 | componentWillReceiveProps(nextProps) { 22 | // 登录成功 23 | if (nextProps.login.status === 'ok') { 24 | const userInfo = nextProps.login.info; 25 | // 模拟登录成功用户Token,2个小时超时哦 26 | Store.set(Config.USER_TOKEN, (new Date()).getTime()); 27 | Store.set(Config.USER_ID, userInfo[0].id); // 存储登录信息 28 | 29 | this.props.dispatch(routerRedux.push('/')); 30 | } 31 | 32 | // 登录失败 33 | if (nextProps.login.status === 'error') { 34 | this.props.dispatch({ 35 | type: 'global/changeMessage', 36 | payload: '账户或密码错误', 37 | }); 38 | } 39 | } 40 | 41 | handleSubmit = (e) => { 42 | e.preventDefault(); 43 | if (this.props.messageStatus) return; // 弹窗未完全关闭禁止再次提交 44 | const { type } = this.state; 45 | this.props.form.validateFields({ force: true }, 46 | (err, values) => { 47 | if (!err) { 48 | this.props.dispatch({ 49 | type: `login/${type}Submit`, 50 | payload: values, 51 | }); 52 | } 53 | } 54 | ); 55 | } 56 | 57 | render() { 58 | const { form, login } = this.props; 59 | const { getFieldDecorator } = form; 60 | const { type } = this.state; 61 | return ( 62 |
63 |
64 | 65 | Ant Design 66 |
67 |
68 | 69 | {getFieldDecorator('username', { 70 | rules: [{ 71 | required: type === 'account', message: '请输入账户名!', 72 | }], 73 | })( 74 | } 76 | placeholder="sosout" 77 | /> 78 | )} 79 | 80 | 81 | {getFieldDecorator('password', { 82 | rules: [{ 83 | required: type === 'account', message: '请输入密码!', 84 | }], 85 | })( 86 | } 88 | type="password" 89 | placeholder="sosout" 90 | /> 91 | )} 92 | 93 | 94 | 97 | 98 |
99 | 账号:sosout 100 | 密码:sosout 101 |
102 |
103 |
104 | ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-antd-dva", 3 | "version": "0.0.1", 4 | "description": "基于react + ant-design + dva + Mock 企业级后台管理系统最佳实践", 5 | "private": true, 6 | "scripts": { 7 | "precommit": "npm run lint-staged", 8 | "start": "cross-env PORT=8888 roadhog dev", 9 | "start:no-proxy": "cross-env PORT=8888 NO_PROXY=true roadhog dev", 10 | "build": "roadhog build", 11 | "site": "roadhog-api-doc static && gh-pages -d dist", 12 | "analyze": "roadhog build --analyze", 13 | "lint:style": "stylelint \"src/**/*.less\" --syntax less", 14 | "lint": "eslint --ext .js src mock tests && npm run lint:style", 15 | "lint:fix": "eslint --fix --ext .js src mock tests && npm run lint:style", 16 | "lint-staged": "lint-staged", 17 | "lint-staged:js": "eslint --ext .js", 18 | "test": "jest", 19 | "test:all": "node ./tests/run-tests.js" 20 | }, 21 | "dependencies": { 22 | "@babel/polyfill": "^7.0.0-beta.36", 23 | "antd": "^3.0.0-beta.1", 24 | "babel-runtime": "^6.9.2", 25 | "classnames": "^2.2.5", 26 | "core-js": "^2.5.1", 27 | "dva": "^2.1.0", 28 | "g-cloud": "^1.0.2-beta", 29 | "g2": "^2.3.13", 30 | "g2-plugin-slider": "^1.2.1", 31 | "lodash": "^4.17.4", 32 | "lodash-decorators": "^4.4.1", 33 | "lodash.clonedeep": "^4.5.0", 34 | "moment": "^2.19.1", 35 | "numeral": "^2.0.6", 36 | "prop-types": "^15.5.10", 37 | "qs": "^6.5.0", 38 | "react": "^16.0.0", 39 | "react-container-query": "^0.9.1", 40 | "react-document-title": "^2.0.3", 41 | "react-dom": "^16.0.0", 42 | "react-fittext": "^1.0.0", 43 | "store": "^2.0.12" 44 | }, 45 | "devDependencies": { 46 | "babel-eslint": "^8.1.2", 47 | "babel-jest": "^22.0.4", 48 | "babel-plugin-dva-hmr": "^0.4.1", 49 | "babel-plugin-import": "^1.6.3", 50 | "babel-plugin-transform-class-properties": "^6.24.1", 51 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 52 | "babel-plugin-transform-runtime": "^6.23.0", 53 | "babel-preset-env": "^1.6.1", 54 | "babel-preset-react": "^6.24.1", 55 | "cross-env": "^5.1.1", 56 | "cross-port-killer": "^1.0.1", 57 | "enzyme": "^3.1.0", 58 | "enzyme-adapter-react-16": "^1.0.2", 59 | "eslint": "^4.14.0", 60 | "eslint-config-airbnb": "^16.0.0", 61 | "eslint-plugin-babel": "^4.0.0", 62 | "eslint-plugin-compat": "^2.1.0", 63 | "eslint-plugin-import": "^2.8.0", 64 | "eslint-plugin-jsx-a11y": "^6.0.3", 65 | "eslint-plugin-markdown": "^1.0.0-beta.6", 66 | "eslint-plugin-react": "^7.0.1", 67 | "gh-pages": "^1.0.0", 68 | "husky": "^0.14.3", 69 | "jest": "^22.0.4", 70 | "jsdom": "^11.5.1", 71 | "lint-staged": "^6.0.0", 72 | "mockjs": "^1.0.1-beta3", 73 | "pro-download": "^1.0.1", 74 | "react-test-renderer": "^16.2.0", 75 | "redbox-react": "^1.5.0", 76 | "roadhog": "^2.0.5", 77 | "roadhog-api-doc": "^0.3.3", 78 | "rollbar": "^2.3.4", 79 | "stylelint": "^8.4.0", 80 | "stylelint-config-standard": "^18.0.0" 81 | }, 82 | "optionalDependencies": { 83 | "nightmare": "^2.10.0" 84 | }, 85 | "babel": { 86 | "presets": [ 87 | "env", 88 | "react" 89 | ], 90 | "plugins": [ 91 | "transform-decorators-legacy", 92 | "transform-class-properties" 93 | ] 94 | }, 95 | "jest": { 96 | "setupFiles": [ 97 | "/tests/setupTests.js" 98 | ], 99 | "testMatch": [ 100 | "**/?(*.)(spec|test|e2e).js?(x)" 101 | ], 102 | "setupTestFrameworkScriptFile": "/tests/jasmine.js", 103 | "moduleFileExtensions": [ 104 | "js", 105 | "jsx" 106 | ], 107 | "moduleNameMapper": { 108 | "\\.(css|less)$": "/tests/styleMock.js" 109 | } 110 | }, 111 | "lint-staged": { 112 | "**/*.{js,jsx}": "lint-staged:js", 113 | "**/*.less": "stylelint --syntax less" 114 | }, 115 | "browserslist": [ 116 | "> 1%", 117 | "last 2 versions", 118 | "not ie <= 10" 119 | ] 120 | } 121 | -------------------------------------------------------------------------------- /mock/chart.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | // mock data 4 | const visitData = []; 5 | const beginDay = new Date().getTime(); 6 | 7 | const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5]; 8 | for (let i = 0; i < fakeY.length; i += 1) { 9 | visitData.push({ 10 | x: moment(new Date(beginDay + (1000 * 60 * 60 * 24 * i))).format('YYYY-MM-DD'), 11 | y: fakeY[i], 12 | }); 13 | } 14 | 15 | const visitData2 = []; 16 | const fakeY2 = [1, 6, 4, 8, 3, 7, 2]; 17 | for (let i = 0; i < fakeY2.length; i += 1) { 18 | visitData2.push({ 19 | x: moment(new Date(beginDay + (1000 * 60 * 60 * 24 * i))).format('YYYY-MM-DD'), 20 | y: fakeY2[i], 21 | }); 22 | } 23 | 24 | const salesData = []; 25 | for (let i = 0; i < 12; i += 1) { 26 | salesData.push({ 27 | x: `${i + 1}月`, 28 | y: Math.floor(Math.random() * 1000) + 200, 29 | }); 30 | } 31 | const searchData = []; 32 | for (let i = 0; i < 50; i += 1) { 33 | searchData.push({ 34 | index: i + 1, 35 | keyword: `搜索关键词-${i}`, 36 | count: Math.floor(Math.random() * 1000), 37 | range: Math.floor(Math.random() * 100), 38 | status: Math.floor((Math.random() * 10) % 2), 39 | }); 40 | } 41 | const salesTypeData = [ 42 | { 43 | x: '家用电器', 44 | y: 4544, 45 | }, 46 | { 47 | x: '食用酒水', 48 | y: 3321, 49 | }, 50 | { 51 | x: '个护健康', 52 | y: 3113, 53 | }, 54 | { 55 | x: '服饰箱包', 56 | y: 2341, 57 | }, 58 | { 59 | x: '母婴产品', 60 | y: 1231, 61 | }, 62 | { 63 | x: '其他', 64 | y: 1231, 65 | }, 66 | ]; 67 | 68 | const salesTypeDataOnline = [ 69 | { 70 | x: '家用电器', 71 | y: 244, 72 | }, 73 | { 74 | x: '食用酒水', 75 | y: 321, 76 | }, 77 | { 78 | x: '个护健康', 79 | y: 311, 80 | }, 81 | { 82 | x: '服饰箱包', 83 | y: 41, 84 | }, 85 | { 86 | x: '母婴产品', 87 | y: 121, 88 | }, 89 | { 90 | x: '其他', 91 | y: 111, 92 | }, 93 | ]; 94 | 95 | const salesTypeDataOffline = [ 96 | { 97 | x: '家用电器', 98 | y: 99, 99 | }, 100 | { 101 | x: '个护健康', 102 | y: 188, 103 | }, 104 | { 105 | x: '服饰箱包', 106 | y: 344, 107 | }, 108 | { 109 | x: '母婴产品', 110 | y: 255, 111 | }, 112 | { 113 | x: '其他', 114 | y: 65, 115 | }, 116 | ]; 117 | 118 | const offlineData = []; 119 | for (let i = 0; i < 10; i += 1) { 120 | offlineData.push({ 121 | name: `门店${i}`, 122 | cvr: Math.ceil(Math.random() * 9) / 10, 123 | }); 124 | } 125 | const offlineChartData = []; 126 | for (let i = 0; i < 20; i += 1) { 127 | offlineChartData.push({ 128 | x: (new Date().getTime()) + (1000 * 60 * 30 * i), 129 | y1: Math.floor(Math.random() * 100) + 10, 130 | y2: Math.floor(Math.random() * 100) + 10, 131 | }); 132 | } 133 | 134 | const radarOriginData = [ 135 | { 136 | name: '个人', 137 | ref: 10, 138 | koubei: 8, 139 | output: 4, 140 | contribute: 5, 141 | hot: 7, 142 | }, 143 | { 144 | name: '团队', 145 | ref: 3, 146 | koubei: 9, 147 | output: 6, 148 | contribute: 3, 149 | hot: 1, 150 | }, 151 | { 152 | name: '部门', 153 | ref: 4, 154 | koubei: 1, 155 | output: 6, 156 | contribute: 5, 157 | hot: 7, 158 | }, 159 | ]; 160 | 161 | // 162 | const radarData = []; 163 | const radarTitleMap = { 164 | ref: '引用', 165 | koubei: '口碑', 166 | output: '产量', 167 | contribute: '贡献', 168 | hot: '热度', 169 | }; 170 | radarOriginData.forEach((item) => { 171 | Object.keys(item).forEach((key) => { 172 | if (key !== 'name') { 173 | radarData.push({ 174 | name: item.name, 175 | label: radarTitleMap[key], 176 | value: item[key], 177 | }); 178 | } 179 | }); 180 | }); 181 | 182 | export const getFakeChartData = { 183 | visitData, 184 | visitData2, 185 | salesData, 186 | searchData, 187 | offlineData, 188 | offlineChartData, 189 | salesTypeData, 190 | salesTypeDataOnline, 191 | salesTypeDataOffline, 192 | radarData, 193 | }; 194 | 195 | export default { 196 | getFakeChartData, 197 | }; 198 | -------------------------------------------------------------------------------- /mock/rule.js: -------------------------------------------------------------------------------- 1 | import { getUrlParams } from './utils'; 2 | 3 | // mock tableListDataSource 4 | let tableListDataSource = []; 5 | for (let i = 0; i < 46; i += 1) { 6 | tableListDataSource.push({ 7 | key: i, 8 | disabled: ((i % 6) === 0), 9 | href: 'https://ant.design', 10 | avatar: ['https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png', 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png'][i % 2], 11 | no: `TradeCode ${i}`, 12 | title: `一个任务名称 ${i}`, 13 | owner: '曲丽丽', 14 | description: '这是一段描述', 15 | callNo: Math.floor(Math.random() * 1000), 16 | status: Math.floor(Math.random() * 10) % 4, 17 | updatedAt: new Date(`2017-07-${Math.floor(i / 2) + 1}`), 18 | createdAt: new Date(`2017-07-${Math.floor(i / 2) + 1}`), 19 | progress: Math.ceil(Math.random() * 100), 20 | }); 21 | } 22 | 23 | export function getRule(req, res, u) { 24 | let url = u; 25 | if (!url || Object.prototype.toString.call(url) !== '[object String]') { 26 | url = req.url; // eslint-disable-line 27 | } 28 | 29 | const params = getUrlParams(url); 30 | 31 | let dataSource = [...tableListDataSource]; 32 | 33 | if (params.sorter) { 34 | const s = params.sorter.split('_'); 35 | dataSource = dataSource.sort((prev, next) => { 36 | if (s[1] === 'descend') { 37 | return next[s[0]] - prev[s[0]]; 38 | } 39 | return prev[s[0]] - next[s[0]]; 40 | }); 41 | } 42 | 43 | if (params.status) { 44 | const status = params.status.split(','); 45 | let filterDataSource = []; 46 | status.forEach((s) => { 47 | filterDataSource = filterDataSource.concat( 48 | [...dataSource].filter(data => parseInt(data.status, 10) === parseInt(s[0], 10)) 49 | ); 50 | }); 51 | dataSource = filterDataSource; 52 | } 53 | 54 | if (params.no) { 55 | dataSource = dataSource.filter(data => data.no.indexOf(params.no) > -1); 56 | } 57 | 58 | let pageSize = 10; 59 | if (params.pageSize) { 60 | pageSize = params.pageSize * 1; 61 | } 62 | 63 | const result = { 64 | list: dataSource, 65 | pagination: { 66 | total: dataSource.length, 67 | pageSize, 68 | current: parseInt(params.currentPage, 10) || 1, 69 | }, 70 | }; 71 | 72 | if (res && res.json) { 73 | res.json(result); 74 | } else { 75 | return result; 76 | } 77 | } 78 | 79 | export function postRule(req, res, u, b) { 80 | let url = u; 81 | if (!url || Object.prototype.toString.call(url) !== '[object String]') { 82 | url = req.url; // eslint-disable-line 83 | } 84 | 85 | const body = (b && b.body) || req.body; 86 | const { method, no, description } = body; 87 | 88 | switch (method) { 89 | /* eslint no-case-declarations:0 */ 90 | case 'delete': 91 | tableListDataSource = tableListDataSource.filter(item => no.indexOf(item.no) === -1); 92 | break; 93 | case 'post': 94 | const i = Math.ceil(Math.random() * 10000); 95 | tableListDataSource.unshift({ 96 | key: i, 97 | href: 'https://ant.design', 98 | avatar: ['https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png', 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png'][i % 2], 99 | no: `TradeCode ${i}`, 100 | title: `一个任务名称 ${i}`, 101 | owner: '曲丽丽', 102 | description, 103 | callNo: Math.floor(Math.random() * 1000), 104 | status: Math.floor(Math.random() * 10) % 2, 105 | updatedAt: new Date(), 106 | createdAt: new Date(), 107 | progress: Math.ceil(Math.random() * 100), 108 | }); 109 | break; 110 | default: 111 | break; 112 | } 113 | 114 | const result = { 115 | list: tableListDataSource, 116 | pagination: { 117 | total: tableListDataSource.length, 118 | }, 119 | }; 120 | 121 | if (res && res.json) { 122 | res.json(result); 123 | } else { 124 | return result; 125 | } 126 | } 127 | 128 | export default { 129 | getRule, 130 | postRule, 131 | }; 132 | -------------------------------------------------------------------------------- /src/layouts/BasicLayout.less: -------------------------------------------------------------------------------- 1 | @import "~antd/lib/style/themes/default.less"; 2 | 3 | .basic-layout { 4 | height: 100vh; 5 | .basic-header { 6 | position: fixed; 7 | top: 0; 8 | left: 0; 9 | background-color: #09c; 10 | height: 50px; 11 | line-height: 50px; 12 | color: #fff; 13 | width: 100%; 14 | z-index: 10; 15 | padding: 0; 16 | .header-home { 17 | position: relative; 18 | display: block; 19 | width: 50px; 20 | background: #0087b4; 21 | font-size: 28px; 22 | color: #fff; 23 | text-align: center; 24 | height: 50px; 25 | line-height: 50px; 26 | overflow: hidden; 27 | float: left; 28 | &:before { 29 | content: ''; 30 | position: absolute; 31 | width: 28px; 32 | height: 28px; 33 | left: 11px; 34 | top: 11px; 35 | display: inline-block; 36 | background: url(https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg) no-repeat center center; 37 | background-size: cover; 38 | } 39 | } 40 | .header-home-link { 41 | display: inline-block; 42 | height: 50px; 43 | box-sizing: border-box; 44 | padding: 0 20px; 45 | color: #fff; 46 | font-size: 14px; 47 | line-height: 50px; 48 | border-right: 1px solid #008fbf; 49 | &:focus { 50 | text-decoration: none; 51 | } 52 | } 53 | .header-right { 54 | float: right; 55 | line-height: 50px; 56 | height: 50px; 57 | display: block; 58 | z-index: 2; 59 | background: #09c; 60 | color: #fff; 61 | font-size: 14px; 62 | text-overflow: ellipsis; 63 | white-space: nowrap; 64 | overflow: hidden; 65 | cursor: pointer; 66 | .user-name { 67 | width: 100px; 68 | padding: 0 10px; 69 | border-left: 1px solid #008fbf; 70 | text-align: center; 71 | } 72 | .avatar { 73 | background: rgba(255, 255, 255, .85); 74 | vertical-align: middle; 75 | margin-right: 5px; 76 | } 77 | } 78 | } 79 | .basic-sider { 80 | min-height: 100vh; 81 | box-shadow: 2px 0 6px rgba(0, 21, 41, .35); 82 | flex: 0 0 180px !important; 83 | max-width: 180px !important; 84 | min-width: 180px !important; 85 | width: 180px !important; 86 | margin-top: 50px; 87 | overflow: hidden; 88 | :global { 89 | .ant-layout-sider-trigger { 90 | width: 180px !important; 91 | } 92 | } 93 | &.collapsed { 94 | flex: 0 0 80px !important; 95 | max-width: 80px !important; 96 | min-width: 80px !important; 97 | width: 80px !important; 98 | :global { 99 | .ant-layout-sider-trigger { 100 | width: 80px !important; 101 | } 102 | } 103 | } 104 | .basic-menu { 105 | padding-bottom: 80px; 106 | width: 200px; 107 | overflow-x: hidden; 108 | overflow-y: scroll; 109 | &.collapsed { 110 | width: 100px; 111 | :global { 112 | .ant-menu-submenu { 113 | width: 100px; 114 | } 115 | } 116 | } 117 | .basic-menu-item { 118 | padding-left: 68px !important; 119 | } 120 | :global { 121 | .ant-menu-submenu { 122 | width: 200px; 123 | } 124 | } 125 | } 126 | } 127 | .basic-content { 128 | width: 100%; 129 | margin-top: 50px; 130 | padding: 12px 20px 40px 20px; 131 | overflow-y: scroll; 132 | min-height: 100%; 133 | &::-webkit-scrollbar-thumb { 134 | background-color: transparent; 135 | } 136 | 137 | &::-webkit-scrollbar { 138 | width: 8px; 139 | height: 8px; 140 | } 141 | } 142 | .basic-footer { 143 | position: fixed; 144 | height: 30px; 145 | left: 0; 146 | line-height: 30px; 147 | padding: 0; 148 | bottom: 0; 149 | width: 100%; 150 | text-align: center; 151 | } 152 | } 153 | 154 | :global { 155 | .ant-menu-dark { 156 | .ant-menu-sub { 157 | margin-left: -20px; 158 | } 159 | } 160 | .ant-menu-submenu-arrow { 161 | margin-right: 20px; 162 | } 163 | .ant-layout-sider-children { 164 | width: 200px; 165 | overflow-y: scroll; 166 | } 167 | .ant-dropdown { 168 | .anticon { 169 | margin-right: 5px; 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-antd-dva 2 | 3 | 基于react + ant-design + dva + Mock 企业级后台管理系统最佳实践 4 | 5 | - 预览:http://dva.sosout.com/ 6 | 7 | ## 特性 8 | 9 | - :gem: **优雅美观**:基于 Ant Design 体系精心设计 10 | - :rocket: **最新技术栈**:使用 React/dva/antd 等前端前沿技术开发 11 | - :1234: **Mock 数据**:实用的本地数据调试方案 12 | 13 | ## 模板 14 | - [x] roadhog版本升级2.0 15 | - [x] 项目搭建 16 | - [x] 登录 17 | - [x] 基础布局 18 | - [ ] 分析页 19 | ## 使用 20 | 21 | ```bash 22 | $ git clone https://github.com/sosout/react-antd-dva.git 23 | $ cd react-antd-dva 24 | $ yarn install 25 | $ yarn start # 访问 http://localhost:8888 26 | ``` 27 | 28 | ## 兼容性 29 | 30 | 现代浏览器及 IE11。 31 | 32 | 33 | ## dva用法 34 | 35 | ### dynamic 36 | 37 | 解决组件动态加载问题的 util 方法,基于 react-async-component 实现。比如: 38 | 39 | ```javascript 40 | import dynamic from 'dva/dynamic'; 41 | 42 | const UserPageComponent = dynamic({ 43 | app, 44 | models: () => [ 45 | import('./models/users'), 46 | ], 47 | component: () => import('./routes/UserPage'), 48 | }); 49 | ``` 50 | 51 | ## react-router 4.0 52 | 53 | ### exact(boolean类型) 54 | 55 | 如果为 true, 则仅在位置完全匹配时才应用。 56 | 57 | ```javascript 58 | path location.pathname exact matches? 59 | /one /one/two true no 60 | /one /one/two false yes 61 | ``` 62 | 63 | ## React PureComponent 64 | 65 | ### 为什么使用? 66 | 67 | React15.3中新加了一个 PureComponent 类,顾名思义, pure 是纯的意思, PureComponent 也就是纯组件,取代其前身 PureRenderMixin , PureComponent 是优化 React 应用程序最重要的方法之一,易于实施,只要把继承类从 Component 换成 PureComponent 即可,可以减少不必要的 render 操作的次数,从而提高性能,而且可以少写 shouldComponentUpdate 函数,节省了点代码。 68 | 69 | ### 原理 70 | 71 | 当组件更新时,如果组件的 props 和 state 都没发生改变, render 方法就不会触发,省去 Virtual DOM 的生成和比对过程,达到提升性能的目的。具体就是 React 自动帮我们做了一层浅比较: 72 | 73 | ```javascript 74 | if (this._compositeType === CompositeTypes.PureClass) { 75 | shouldUpdate = !shallowEqual(prevProps, nextProps) 76 | || !shallowEqual(inst.state, nextState); 77 | } 78 | ```` 79 | 80 | 而 shallowEqual 又做了什么呢?会比较 Object.keys(state | props) 的长度是否一致,每一个 key 是否两者都有,并且是否是一个引用,也就是只比较了第一层的值,确实很浅,所以深层的嵌套数据是对比不出来的。 81 | 82 | ### 使用指南 83 | 84 | #### 易变数据不能使用一个引用 85 | 86 | 示例: 87 | 88 | ```javascript 89 | class App extends PureComponent { 90 | state = { 91 | items: [1, 2, 3] 92 | } 93 | handleClick = () => { 94 | const { items } = this.state; 95 | items.pop(); 96 | this.setState({ items }); 97 | } 98 | render() { 99 | return (
100 |
    101 | {this.state.items.map(i =>
  • {i}
  • )} 102 |
103 | 104 |
) 105 | } 106 | } 107 | ``` 108 | 109 | 会发现,无论怎么点 delete 按钮, li 都不会变少,因为 items 用的是一个引用, shallowEqual 的结果为 true 。改正: 110 | 111 | ```javascript 112 | handleClick = () => { 113 | const { items } = this.state; 114 | items.pop(); 115 | this.setState({ items: [].concat(items) }); 116 | } 117 | ``` 118 | 119 | 这样每次改变都会产生一个新的数组,也就可以 render 了。这里有一个矛盾的地方,如果没有 items.pop(); 操作,每次 items 数据并没有变,但还是 render 了,这不就很操蛋么?呵呵,数据都不变,你 setState 干嘛? 120 | 121 | #### 不变数据使用一个引用 122 | 123 | ##### 子组件数据 124 | 125 | 上面易变数据不能使用一个引用的案例中有一个点击删除操作,如果我们删除的代码这么写: 126 | 127 | ```javascript 128 | handleClick = () => { 129 | const { items } = this.state; 130 | items.splice(items.length - 1, 1); 131 | this.setState({ items }); 132 | } 133 | ``` 134 | 135 | items 的引用也是改变的,但如果 items 里面是引用类型数据: 136 | 137 | ```javascript 138 | items: [{a: 1}, {a: 2}, {a: 3}] 139 | ``` 140 | 141 | 这个时候 142 | 143 | ```javascript 144 | state.items[0] === nextState.items[0] // false 145 | ``` 146 | 147 | 子组件里还是re-render了。这样就需要我们保证不变的子组件数据的引用不能改变。这个时候可以使用immutable-js函数库。 148 | 149 | ##### 函数属性 150 | 151 | 我们在给组件传一个函数的时候,有时候总喜欢: 152 | 153 | ```javascript 154 | // 1 155 | this.props.update(e.target.value)} /> 156 | // 2 157 | update(e) { 158 | this.props.update(e.target.value) 159 | } 160 | render() { 161 | return 162 | } 163 | ``` 164 | 165 | 由于每次 render 操作 MyInput 组件的 onChange 属性都会返回一个新的函数,由于引用不一样,所以父组件的 render 也会导致 MyInput 组件的 render ,即使没有任何改动,所以需要尽量避免这样的写法,最好这样写: 166 | 167 | ```javascript 168 | // 1,2 169 | update = (e) => { 170 | this.props.update(e.target.value) 171 | } 172 | render() { 173 | return 174 | } 175 | ``` 176 | 177 | ##### 空对象、空数组或固定对象 178 | 179 | 有时候后台返回的数据中,数组长度为0或者对象没有属性会直接给一个 null ,这时候我们需要做一些容错: 180 | 181 | ```javascript 182 | class App extends PureComponent { 183 | state = { 184 | items: [{ name: 'test1' }, null, { name: 'test3' }] 185 | } 186 | store = (id, value) => { 187 | const { items } = this.state; 188 | items[id] = assign({}, items[id], { name: value }); 189 | this.setState({ items: [].concat(items) }); 190 | } 191 | render() { 192 | return (
193 |
    194 | {this.state.items.map((i, k) => 195 | ) 196 | } 197 |
198 |
) 199 | } 200 | } 201 | ``` 202 | 203 | 当某一个子组件调用 store 函数改变了自己的那条属性,触发 render 操作,如果数据是 null 的话 data 属性每次都是一个 {},{} ==== {} 是 false 的,这样无端的让这几个子组件重新 render 了。{ color: 'red' }也是一样。 204 | 205 | 最好设置一个 defaultValue 为 {},如下: 206 | 207 | ```javascript 208 | static defaultValue = {} 209 | const style = { color: 'red' }; 210 | 211 | ``` 212 | 213 | ### 复杂状态与简单状态不要共用一个组件 214 | 215 | 这点可能和 PureComponent 没多少关系,但做的不好可能会浪费很多性能,比如一个页面上面一部分是一个复杂的列表,下面是一个输入框,抽象代码: 216 | 217 | ```javascript 218 | change = (e) => { 219 | this.setState({ value: e.target.value }); 220 | } 221 | render() { 222 | return (
223 |
    224 | {this.state.items.map((i, k) =>
  • {...}
  • )} 225 |
226 | 227 |
) 228 | } 229 | ``` 230 | 231 | 表单和列表其实是没有什么关联的,表单的值也可能经常变动,但它的会给列表也带来必然的 diff 操作,这是没必要的,最好是给列表抽出成一个单独的 PureComponent 组件,这样 state.items 不变的话,列表就不会重新 render 了。 232 | 233 | ### 与 shouldComponentUpdate 共存 234 | 235 | 如果 PureComponent 里有 shouldComponentUpdate 函数的话,直接使用 shouldComponentUpdate 的结果作为是否更新的依据,没有 shouldComponentUpdate 函数的话,才会去判断是不是 PureComponent ,是的话再去做 shallowEqual 浅比较。 236 | 237 | ```javascript 238 | // 这个变量用来控制组件是否需要更新 239 | var shouldUpdate = true; 240 | // inst 是组件实例 241 | if (inst.shouldComponentUpdate) { 242 | shouldUpdate = inst.shouldComponentUpdate(nextProps, nextState, nextContext); 243 | } else { 244 | if (this._compositeType === CompositeType.PureClass) { 245 | shouldUpdate = !shallowEqual(prevProps, nextProps) || 246 | !shallowEqual(inst.state, nextState); 247 | } 248 | } 249 | ``` 250 | 251 | ### 老版本兼容写法 252 | 253 | ```javascript 254 | import React { PureComponent, Component } from 'react'; 255 | class Foo extends (PureComponent || Component) { 256 | //... 257 | } 258 | ``` 259 | 260 | 这样在老版本的 React 里也不会挂掉。 261 | 262 | ### 总结 263 | 264 | PureComponent 真正起作用的,只是在一些纯展示组件上,复杂组件用了也没关系,反正 shallowEqual 那一关就过不了,不过记得 props 和 state 不能使用同一个引用哦。 265 | -------------------------------------------------------------------------------- /mock/api.js: -------------------------------------------------------------------------------- 1 | import { getUrlParams } from './utils'; 2 | 3 | const titles = [ 4 | 'Alipay', 5 | 'Angular', 6 | 'Ant Design', 7 | 'Ant Design Pro', 8 | 'Bootstrap', 9 | 'React', 10 | 'Vue', 11 | 'Webpack', 12 | ]; 13 | const avatars = [ 14 | 'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png', // Alipay 15 | 'https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png', // Angular 16 | 'https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png', // Ant Design 17 | 'https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png', // Ant Design Pro 18 | 'https://gw.alipayobjects.com/zos/rmsportal/siCrBXXhmvTQGWPNLBow.png', // Bootstrap 19 | 'https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png', // React 20 | 'https://gw.alipayobjects.com/zos/rmsportal/ComBAopevLwENQdKWiIn.png', // Vue 21 | 'https://gw.alipayobjects.com/zos/rmsportal/nxkuOJlFJuAUhzlMTCEe.png', // Webpack 22 | ]; 23 | const covers = [ 24 | 'https://gw.alipayobjects.com/zos/rmsportal/uMfMFlvUuceEyPpotzlq.png', 25 | 'https://gw.alipayobjects.com/zos/rmsportal/iZBVOIhGJiAnhplqjvZW.png', 26 | 'https://gw.alipayobjects.com/zos/rmsportal/uVZonEtjWwmUZPBQfycs.png', 27 | 'https://gw.alipayobjects.com/zos/rmsportal/gLaIAoVWTtLbBWZNYEMg.png', 28 | ]; 29 | const desc = [ 30 | '那是一种内在的东西, 他们到达不了,也无法触及的', 31 | '希望是一个好东西,也许是最好的,好东西是不会消亡的', 32 | '生命就像一盒巧克力,结果往往出人意料', 33 | '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆', 34 | '那时候我只会想自己想要什么,从不想自己拥有什么', 35 | ]; 36 | 37 | const user = [ 38 | '付小小', 39 | '曲丽丽', 40 | '林东东', 41 | '周星星', 42 | '吴加好', 43 | '朱偏右', 44 | '鱼酱', 45 | '乐哥', 46 | '谭小仪', 47 | '仲尼', 48 | ]; 49 | 50 | export function fakeList(count) { 51 | const list = []; 52 | for (let i = 0; i < count; i += 1) { 53 | list.push({ 54 | id: `fake-list-${i}`, 55 | owner: user[i % 10], 56 | title: titles[i % 8], 57 | avatar: avatars[i % 8], 58 | cover: parseInt(i / 4, 10) % 2 === 0 ? covers[i % 4] : covers[3 - (i % 4)], 59 | status: ['active', 'exception', 'normal'][i % 3], 60 | percent: Math.ceil(Math.random() * 50) + 50, 61 | logo: avatars[i % 8], 62 | href: 'https://ant.design', 63 | updatedAt: new Date(new Date().getTime() - (1000 * 60 * 60 * 2 * i)), 64 | createdAt: new Date(new Date().getTime() - (1000 * 60 * 60 * 2 * i)), 65 | subDescription: desc[i % 5], 66 | description: '在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。', 67 | activeUser: Math.ceil(Math.random() * 100000) + 100000, 68 | newUser: Math.ceil(Math.random() * 1000) + 1000, 69 | star: Math.ceil(Math.random() * 100) + 100, 70 | like: Math.ceil(Math.random() * 100) + 100, 71 | message: Math.ceil(Math.random() * 10) + 10, 72 | content: '段落示意:蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。', 73 | members: [ 74 | { 75 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ZiESqWwCXBRQoaPONSJe.png', 76 | name: '曲丽丽', 77 | }, 78 | { 79 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/tBOxZPlITHqwlGjsJWaF.png', 80 | name: '王昭君', 81 | }, 82 | { 83 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/sBxjgqiuHMGRkIjqlQCd.png', 84 | name: '董娜娜', 85 | }, 86 | ], 87 | }); 88 | } 89 | 90 | return list; 91 | } 92 | 93 | export function getFakeList(req, res, u) { 94 | let url = u; 95 | if (!url || Object.prototype.toString.call(url) !== '[object String]') { 96 | url = req.url; // eslint-disable-line 97 | } 98 | 99 | const params = getUrlParams(url); 100 | 101 | const count = (params.count * 1) || 20; 102 | 103 | const result = fakeList(count); 104 | 105 | if (res && res.json) { 106 | res.json(result); 107 | } else { 108 | return result; 109 | } 110 | } 111 | 112 | export const getNotice = [ 113 | { 114 | id: 'xxx1', 115 | title: titles[0], 116 | logo: avatars[0], 117 | description: '那是一种内在的东西,他们到达不了,也无法触及的', 118 | updatedAt: new Date(), 119 | member: '科学搬砖组', 120 | href: '', 121 | memberLink: '', 122 | }, 123 | { 124 | id: 'xxx2', 125 | title: titles[1], 126 | logo: avatars[1], 127 | description: '希望是一个好东西,也许是最好的,好东西是不会消亡的', 128 | updatedAt: new Date('2017-07-24'), 129 | member: '全组都是吴彦祖', 130 | href: '', 131 | memberLink: '', 132 | }, 133 | { 134 | id: 'xxx3', 135 | title: titles[2], 136 | logo: avatars[2], 137 | description: '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆', 138 | updatedAt: new Date(), 139 | member: '中二少女团', 140 | href: '', 141 | memberLink: '', 142 | }, 143 | { 144 | id: 'xxx4', 145 | title: titles[3], 146 | logo: avatars[3], 147 | description: '那时候我只会想自己想要什么,从不想自己拥有什么', 148 | updatedAt: new Date('2017-07-23'), 149 | member: '程序员日常', 150 | href: '', 151 | memberLink: '', 152 | }, 153 | { 154 | id: 'xxx5', 155 | title: titles[4], 156 | logo: avatars[4], 157 | description: '凛冬将至', 158 | updatedAt: new Date('2017-07-23'), 159 | member: '高逼格设计天团', 160 | href: '', 161 | memberLink: '', 162 | }, 163 | { 164 | id: 'xxx6', 165 | title: titles[5], 166 | logo: avatars[5], 167 | description: '生命就像一盒巧克力,结果往往出人意料', 168 | updatedAt: new Date('2017-07-23'), 169 | member: '骗你来学计算机', 170 | href: '', 171 | memberLink: '', 172 | }, 173 | ]; 174 | 175 | export const getActivities = [ 176 | { 177 | id: 'trend-1', 178 | updatedAt: new Date(), 179 | user: { 180 | name: '林东东', 181 | avatar: avatars[0], 182 | }, 183 | group: { 184 | name: '高逼格设计天团', 185 | link: 'http://github.com/', 186 | }, 187 | project: { 188 | name: '六月迭代', 189 | link: 'http://github.com/', 190 | }, 191 | template: '在 @{group} 新建项目 @{project}', 192 | }, 193 | { 194 | id: 'trend-2', 195 | updatedAt: new Date(), 196 | user: { 197 | name: '付小小', 198 | avatar: avatars[1], 199 | }, 200 | group: { 201 | name: '高逼格设计天团', 202 | link: 'http://github.com/', 203 | }, 204 | project: { 205 | name: '六月迭代', 206 | link: 'http://github.com/', 207 | }, 208 | template: '在 @{group} 新建项目 @{project}', 209 | }, 210 | { 211 | id: 'trend-3', 212 | updatedAt: new Date(), 213 | user: { 214 | name: '曲丽丽', 215 | avatar: avatars[2], 216 | }, 217 | group: { 218 | name: '中二少女团', 219 | link: 'http://github.com/', 220 | }, 221 | project: { 222 | name: '六月迭代', 223 | link: 'http://github.com/', 224 | }, 225 | template: '在 @{group} 新建项目 @{project}', 226 | }, 227 | { 228 | id: 'trend-4', 229 | updatedAt: new Date(), 230 | user: { 231 | name: '周星星', 232 | avatar: avatars[3], 233 | }, 234 | project: { 235 | name: '5 月日常迭代', 236 | link: 'http://github.com/', 237 | }, 238 | template: '将 @{project} 更新至已发布状态', 239 | }, 240 | { 241 | id: 'trend-5', 242 | updatedAt: new Date(), 243 | user: { 244 | name: '朱偏右', 245 | avatar: avatars[4], 246 | }, 247 | project: { 248 | name: '工程效能', 249 | link: 'http://github.com/', 250 | }, 251 | comment: { 252 | name: '留言', 253 | link: 'http://github.com/', 254 | }, 255 | template: '在 @{project} 发布了 @{comment}', 256 | }, 257 | { 258 | id: 'trend-6', 259 | updatedAt: new Date(), 260 | user: { 261 | name: '乐哥', 262 | avatar: avatars[5], 263 | }, 264 | group: { 265 | name: '程序员日常', 266 | link: 'http://github.com/', 267 | }, 268 | project: { 269 | name: '品牌迭代', 270 | link: 'http://github.com/', 271 | }, 272 | template: '在 @{group} 新建项目 @{project}', 273 | }, 274 | ]; 275 | 276 | 277 | export default { 278 | getNotice, 279 | getActivities, 280 | getFakeList, 281 | }; 282 | -------------------------------------------------------------------------------- /src/layouts/BasicLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Layout, Menu, Icon, Tag, Dropdown, Avatar } from 'antd'; 4 | import DocumentTitle from 'react-document-title'; 5 | import { connect } from 'dva'; 6 | import { Link, Route, Redirect, Switch } from 'dva/router'; 7 | import moment from 'moment'; 8 | import groupBy from 'lodash/groupBy'; 9 | import { ContainerQuery } from 'react-container-query'; 10 | import classNames from 'classnames'; 11 | import Debounce from 'lodash-decorators/debounce'; 12 | import Store from 'store'; 13 | import Config from '../common/config'; 14 | import styles from './BasicLayout.less'; 15 | 16 | const { SubMenu } = Menu; 17 | const { Header, Content, Footer, Sider } = Layout; 18 | 19 | const query = { 20 | 'screen-xs': { 21 | maxWidth: 575, 22 | }, 23 | 'screen-sm': { 24 | minWidth: 576, 25 | maxWidth: 767, 26 | }, 27 | 'screen-md': { 28 | minWidth: 768, 29 | maxWidth: 991, 30 | }, 31 | 'screen-lg': { 32 | minWidth: 992, 33 | maxWidth: 1199, 34 | }, 35 | 'screen-xl': { 36 | minWidth: 1200, 37 | }, 38 | }; 39 | 40 | class BasicLayout extends React.PureComponent { 41 | static childContextTypes = { 42 | location: PropTypes.object, 43 | breadcrumbNameMap: PropTypes.object, 44 | } 45 | constructor(props) { 46 | super(props); 47 | // 把一级 Layout 的 children 作为菜单项 48 | this.menus = props.navData.reduce((arr, current) => arr.concat(current.children), []); 49 | this.state = { 50 | openKeys: this.getDefaultCollapsedSubMenus(props), 51 | }; 52 | } 53 | getChildContext() { 54 | const { location, navData, getRouteData } = this.props; 55 | const routeData = getRouteData('BasicLayout'); 56 | const firstMenuData = navData.reduce((arr, current) => arr.concat(current.children), []); 57 | const menuData = this.getMenuData(firstMenuData, ''); 58 | const breadcrumbNameMap = {}; 59 | 60 | routeData.concat(menuData).forEach((item) => { 61 | breadcrumbNameMap[item.path] = item.name; 62 | }); 63 | return { location, breadcrumbNameMap }; 64 | } 65 | componentDidMount() { 66 | // 获取用户信息 67 | this.props.dispatch({ 68 | type: 'user/fetchCurrent', 69 | payload: { id: Store.get(Config.USER_ID) }, 70 | }); 71 | } 72 | componentWillUnmount() { 73 | this.triggerResizeEvent.cancel(); 74 | } 75 | onCollapse = (collapsed) => { 76 | this.props.dispatch({ 77 | type: 'global/changeLayoutCollapsed', 78 | payload: collapsed, 79 | }); 80 | } 81 | onMenuClick = ({ key }) => { 82 | if (key === 'logout') { 83 | this.props.dispatch({ 84 | type: 'login/logout', 85 | }); 86 | } 87 | } 88 | getMenuData = (data, parentPath) => { 89 | let arr = []; 90 | data.forEach((item) => { 91 | if (item.children) { 92 | arr.push({ path: `${parentPath}/${item.path}`, name: item.name }); 93 | arr = arr.concat(this.getMenuData(item.children, `${parentPath}/${item.path}`)); 94 | } 95 | }); 96 | return arr; 97 | } 98 | getDefaultCollapsedSubMenus(props) { 99 | const currentMenuSelectedKeys = [...this.getCurrentMenuSelectedKeys(props)]; 100 | currentMenuSelectedKeys.splice(-1, 1); 101 | if (currentMenuSelectedKeys.length === 0) { 102 | return ['dashboard']; 103 | } 104 | return currentMenuSelectedKeys; 105 | } 106 | getCurrentMenuSelectedKeys(props) { 107 | const { location: { pathname } } = props || this.props; 108 | const keys = pathname.split('/').slice(1); 109 | if (keys.length === 1 && keys[0] === '') { 110 | return [this.menus[0].key]; 111 | } 112 | return keys; 113 | } 114 | getNavMenuItems(menusData, parentPath = '') { 115 | if (!menusData) { 116 | return []; 117 | } 118 | return menusData.map((item) => { 119 | if (!item.name) { 120 | return null; 121 | } 122 | let itemPath; 123 | if (item.path.indexOf('http') === 0) { 124 | itemPath = item.path; 125 | } else { 126 | itemPath = `${parentPath}/${item.path || ''}`.replace(/\/+/g, '/'); 127 | } 128 | if (item.children && item.children.some(child => child.name)) { 129 | return ( 130 | 134 | 135 | {item.name} 136 | 137 | ) : item.name 138 | } 139 | key={item.key || item.path} 140 | > 141 | {this.getNavMenuItems(item.children, itemPath)} 142 | 143 | ); 144 | } 145 | const icon = item.icon && ; 146 | return ( 147 | 148 | { 149 | /^https?:\/\//.test(itemPath) ? ( 150 | 151 | {icon}{item.name} 152 | 153 | ) : ( 154 | 159 | {icon}{item.name} 160 | 161 | ) 162 | } 163 | 164 | ); 165 | }); 166 | } 167 | getPageTitle() { // 获取页面标题 168 | const { location, getRouteData } = this.props; 169 | const { pathname } = location; 170 | let title = 'React Antd Dva'; 171 | getRouteData('BasicLayout').forEach((item) => { 172 | if (item.path === pathname) { 173 | title = `${item.name} - React Antd Dva`; 174 | } 175 | }); 176 | return title; 177 | } 178 | getNoticeData() { 179 | const { notices = [] } = this.props; 180 | if (notices.length === 0) { 181 | return {}; 182 | } 183 | const newNotices = notices.map((notice) => { 184 | const newNotice = { ...notice }; 185 | if (newNotice.datetime) { 186 | newNotice.datetime = moment(notice.datetime).fromNow(); 187 | } 188 | // transform id to item key 189 | if (newNotice.id) { 190 | newNotice.key = newNotice.id; 191 | } 192 | if (newNotice.extra && newNotice.status) { 193 | const color = ({ 194 | todo: '', 195 | processing: 'blue', 196 | urgent: 'red', 197 | doing: 'gold', 198 | })[newNotice.status]; 199 | newNotice.extra = {newNotice.extra}; 200 | } 201 | return newNotice; 202 | }); 203 | return groupBy(newNotices, 'type'); 204 | } 205 | handleOpenChange = (openKeys) => { 206 | const lastOpenKey = openKeys[openKeys.length - 1]; 207 | const isMainMenu = this.menus.some( 208 | item => lastOpenKey && (item.key === lastOpenKey || item.path === lastOpenKey) 209 | ); 210 | this.setState({ 211 | openKeys: isMainMenu ? [lastOpenKey] : [...openKeys], 212 | }); 213 | } 214 | toggle = () => { 215 | const { collapsed } = this.props; 216 | this.props.dispatch({ 217 | type: 'global/changeLayoutCollapsed', 218 | payload: !collapsed, 219 | }); 220 | this.triggerResizeEvent(); 221 | } 222 | @Debounce(600) 223 | triggerResizeEvent() { // eslint-disable-line 224 | const event = document.createEvent('HTMLEvents'); 225 | event.initEvent('resize', true, false); 226 | window.dispatchEvent(event); 227 | } 228 | render() { 229 | const { getRouteData, collapsed, currentUser } = this.props; 230 | const menuProps = collapsed ? {} : { 231 | openKeys: this.state.openKeys, 232 | }; 233 | const menu = ( 234 | 235 | 个人中心 236 | 设置 237 | 238 | 退出登录 239 | 240 | ); 241 | const layout = ( 242 | 243 |
244 |
245 |
246 | 管理控制台 247 |
248 | 249 |
250 |
251 | 252 | {currentUser.user_name} 253 |
254 |
255 |
256 |
257 | 258 | 265 | 273 | {this.getNavMenuItems(this.menus)} 274 | 275 | 276 | 277 |
278 | 279 | { 280 | getRouteData('BasicLayout').map(item => 281 | ( 282 | 288 | ) 289 | ) 290 | } 291 | 292 | 293 |
294 |
295 | React Antd Dva ©2017 Created by sosout 296 |
297 |
298 |
299 |
300 | ); 301 | return ( 302 | 303 | 304 | {params =>
{layout}
} 305 |
306 |
307 | ); 308 | } 309 | } 310 | 311 | export default connect(state => ({ 312 | currentUser: state.user.currentUser, 313 | collapsed: state.global.collapsed, 314 | fetchingNotices: state.global.fetchingNotices, 315 | notices: state.global.notices, 316 | }))(BasicLayout); 317 | --------------------------------------------------------------------------------