├── .editorconfig ├── .eslintrc ├── .gitignore ├── .roadhogrc ├── README.md ├── package.json └── src ├── assets └── yay.jpg ├── components ├── MainLayout │ ├── Header.js │ ├── MainLayout.css │ └── MainLayout.js └── Users │ ├── UserModal.js │ ├── Users.css │ └── Users.js ├── constants.js ├── index.css ├── index.html ├── index.js ├── models └── users.js ├── router.js ├── routes ├── IndexPage.css ├── IndexPage.js ├── Users.css └── Users.js ├── services └── users.js └── utils └── request.js /.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 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "rules": { 5 | "generator-star-spacing": [0], 6 | "consistent-return": [0], 7 | "react/forbid-prop-types": [0], 8 | "react/jsx-filename-extension": [1, { "extensions": [".js"] }], 9 | "global-require": [1], 10 | "import/prefer-default-export": [0], 11 | "react/jsx-no-bind": [0], 12 | "react/prop-types": [0], 13 | "react/prefer-stateless-function": [0], 14 | "no-else-return": [0], 15 | "no-restricted-syntax": [0], 16 | "import/no-extraneous-dependencies": [0], 17 | "no-use-before-define": [0], 18 | "jsx-a11y/no-static-element-interactions": [0], 19 | "no-nested-ternary": [0], 20 | "arrow-body-style": [0], 21 | "import/extensions": [0], 22 | "no-bitwise": [0], 23 | "no-cond-assign": [0], 24 | "import/no-unresolved": [0], 25 | "require-yield": [1] 26 | }, 27 | "parserOptions": { 28 | "ecmaFeatures": { 29 | "experimentalObjectRestSpread": true 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .DS_Store 4 | 5 | .idea/ 6 | -------------------------------------------------------------------------------- /.roadhogrc: -------------------------------------------------------------------------------- 1 | { 2 | "entry": "src/index.js", 3 | "disableCSSModules": false, 4 | "publicPath": "/", 5 | "theme": { 6 | "@primary-color": "#1DA57A", 7 | "@link-color": "#1DA57A", 8 | "@border-radius-base": "2px", 9 | "@font-size-base": "16px", 10 | "@line-height-base": "1.2" 11 | }, 12 | "autoprefixer": null, 13 | "proxy": { 14 | "/api": { 15 | "target": "http://jsonplaceholder.typicode.com/", 16 | "changeOrigin": true, 17 | "pathRewrite": { "^/api" : "" } 18 | } 19 | }, 20 | "extraBabelPlugins": [ 21 | "transform-runtime", 22 | ["import", { "libraryName": "antd", "style": true }] 23 | ], 24 | "env": { 25 | "development": { 26 | "extraBabelPlugins": [ 27 | "dva-hmr" 28 | ] 29 | } 30 | }, 31 | "xdllPlugin": { 32 | "exclude": [ 33 | "babel-runtime" 34 | ], 35 | "include": [ 36 | "dva/router", 37 | "dva/saga", 38 | "dva/fetch" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dva-example-user-dashboard 2 | 3 | 详见[《12 步 30 分钟,完成用户管理的 CURD 应用 (react+dva+antd)》](https://github.com/sorrycc/blog/issues/18)。 4 | 5 | --- 6 | 7 |

8 | 9 |

10 | 11 | ## Getting Started 12 | Install dependencies. 13 | 14 | ```bash 15 | $ npm install 16 | ``` 17 | 18 | Start server. 19 | 20 | ```bash 21 | $ npm start 22 | ``` 23 | 24 | If success, app will be open in your default browser automatically. 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "start": "roadhog server", 5 | "build": "roadhog build", 6 | "build:dll": "roadhog buildDll", 7 | "test": "roadhog test", 8 | "lint": "eslint --ext .js src test", 9 | "precommit": "npm run lint" 10 | }, 11 | "dependencies": { 12 | "antd": "^2.6.4", 13 | "babel-runtime": "^6.22.0", 14 | "dva": "^1.4.0-beta.1", 15 | "dva-loading": "^0.2.0", 16 | "react": "^16.1.1", 17 | "react-dom": "^16.1.1" 18 | }, 19 | "devDependencies": { 20 | "babel-eslint": "^7.1.1", 21 | "babel-plugin-dva-hmr": "^0.3.2", 22 | "babel-plugin-import": "^1.1.0", 23 | "babel-plugin-transform-runtime": "^6.22.0", 24 | "eslint": "^3.14.0", 25 | "eslint-config-airbnb": "^14.0.0", 26 | "eslint-plugin-import": "^2.2.0", 27 | "eslint-plugin-jsx-a11y": "^3.0.2", 28 | "eslint-plugin-react": "^6.9.0", 29 | "expect": "^1.20.2", 30 | "husky": "^0.13.0", 31 | "redbox-react": "^1.3.2", 32 | "roadhog": "^0.6.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/assets/yay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvajs/dva-example-user-dashboard/0a09e7f0ca4956669a32cfc2fc725e58eace52ad/src/assets/yay.jpg -------------------------------------------------------------------------------- /src/components/MainLayout/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Menu, Icon } from 'antd'; 3 | import { Link } from 'dva/router'; 4 | 5 | function Header({ location }) { 6 | return ( 7 | 12 | 13 | Users 14 | 15 | 16 | Home 17 | 18 | 19 | 404 20 | 21 | 22 | dva 23 | 24 | 25 | ); 26 | } 27 | 28 | export default Header; 29 | -------------------------------------------------------------------------------- /src/components/MainLayout/MainLayout.css: -------------------------------------------------------------------------------- 1 | 2 | .normal { 3 | display: flex; 4 | flex-direction: column; 5 | height: 100%; 6 | } 7 | 8 | .content { 9 | flex: 1; 10 | display: flex; 11 | } 12 | 13 | .main { 14 | padding: 0 8px; 15 | flex: 1 0 auto; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/MainLayout/MainLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './MainLayout.css'; 3 | import Header from './Header'; 4 | 5 | function MainLayout({ children, location }) { 6 | return ( 7 |
8 |
9 |
10 |
11 | {children} 12 |
13 |
14 |
15 | ); 16 | } 17 | 18 | export default MainLayout; 19 | -------------------------------------------------------------------------------- /src/components/Users/UserModal.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Modal, Form, Input } from 'antd'; 3 | 4 | const FormItem = Form.Item; 5 | 6 | class UserEditModal extends Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | visible: false, 12 | }; 13 | } 14 | 15 | showModelHandler = (e) => { 16 | if (e) e.stopPropagation(); 17 | this.setState({ 18 | visible: true, 19 | }); 20 | }; 21 | 22 | hideModelHandler = () => { 23 | this.setState({ 24 | visible: false, 25 | }); 26 | }; 27 | 28 | okHandler = () => { 29 | const { onOk } = this.props; 30 | this.props.form.validateFields((err, values) => { 31 | if (!err) { 32 | onOk(values); 33 | this.hideModelHandler(); 34 | } 35 | }); 36 | }; 37 | 38 | render() { 39 | const { children } = this.props; 40 | const { getFieldDecorator } = this.props.form; 41 | const { name, email, website } = this.props.record; 42 | const formItemLayout = { 43 | labelCol: { span: 6 }, 44 | wrapperCol: { span: 14 }, 45 | }; 46 | 47 | return ( 48 | 49 | 50 | { children } 51 | 52 | 58 |
59 | 63 | { 64 | getFieldDecorator('name', { 65 | initialValue: name, 66 | })() 67 | } 68 | 69 | 73 | { 74 | getFieldDecorator('email', { 75 | initialValue: email, 76 | })() 77 | } 78 | 79 | 83 | { 84 | getFieldDecorator('website', { 85 | initialValue: website, 86 | })() 87 | } 88 | 89 |
90 |
91 |
92 | ); 93 | } 94 | } 95 | 96 | export default Form.create()(UserEditModal); 97 | -------------------------------------------------------------------------------- /src/components/Users/Users.css: -------------------------------------------------------------------------------- 1 | 2 | .normal { 3 | } 4 | 5 | .create { 6 | margin-bottom: 1.5em; 7 | } 8 | 9 | .operation a { 10 | margin: 0 .5em; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Users/Users.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'dva'; 3 | import { Table, Pagination, Popconfirm, Button } from 'antd'; 4 | import { routerRedux } from 'dva/router'; 5 | import styles from './Users.css'; 6 | import { PAGE_SIZE } from '../../constants'; 7 | import UserModal from './UserModal'; 8 | 9 | function Users({ dispatch, list: dataSource, loading, total, page: current }) { 10 | function deleteHandler(id) { 11 | dispatch({ 12 | type: 'users/remove', 13 | payload: id, 14 | }); 15 | } 16 | 17 | function pageChangeHandler(page) { 18 | dispatch(routerRedux.push({ 19 | pathname: '/users', 20 | query: { page }, 21 | })); 22 | } 23 | 24 | function editHandler(id, values) { 25 | dispatch({ 26 | type: 'users/patch', 27 | payload: { id, values }, 28 | }); 29 | } 30 | 31 | function createHandler(values) { 32 | dispatch({ 33 | type: 'users/create', 34 | payload: values, 35 | }); 36 | } 37 | 38 | const columns = [ 39 | { 40 | title: 'Name', 41 | dataIndex: 'name', 42 | key: 'name', 43 | render: text => {text}, 44 | }, 45 | { 46 | title: 'Email', 47 | dataIndex: 'email', 48 | key: 'email', 49 | }, 50 | { 51 | title: 'Website', 52 | dataIndex: 'website', 53 | key: 'website', 54 | }, 55 | { 56 | title: 'Operation', 57 | key: 'operation', 58 | render: (text, record) => ( 59 | 60 | 61 | Edit 62 | 63 | 64 | Delete 65 | 66 | 67 | ), 68 | }, 69 | ]; 70 | 71 | return ( 72 |
73 |
74 |
75 | 76 | 77 | 78 |
79 | record.id} 84 | pagination={false} 85 | /> 86 | 93 | 94 | 95 | ); 96 | } 97 | 98 | function mapStateToProps(state) { 99 | const { list, total, page } = state.users; 100 | return { 101 | loading: state.loading.models.users, 102 | list, 103 | total, 104 | page, 105 | }; 106 | } 107 | 108 | export default connect(mapStateToProps)(Users); 109 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | 2 | export const PAGE_SIZE = 3; 3 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | 2 | html, body, :global(#root) { 3 | height: 100%; 4 | } 5 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dva Demo 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import dva from 'dva'; 2 | import { browserHistory } from 'dva/router'; 3 | import createLoading from 'dva-loading'; 4 | import { message } from 'antd'; 5 | import './index.html'; 6 | import './index.css'; 7 | 8 | const ERROR_MSG_DURATION = 3; // 3 秒 9 | 10 | // 1. Initialize 11 | const app = dva({ 12 | history: browserHistory, 13 | onError(e) { 14 | message.error(e.message, ERROR_MSG_DURATION); 15 | }, 16 | }); 17 | 18 | // 2. Plugins 19 | app.use(createLoading()); 20 | 21 | // 3. Model 22 | // Moved to router.js 23 | 24 | // 4. Router 25 | app.router(require('./router')); 26 | 27 | // 5. Start 28 | app.start('#root'); 29 | -------------------------------------------------------------------------------- /src/models/users.js: -------------------------------------------------------------------------------- 1 | import * as usersService from '../services/users'; 2 | 3 | export default { 4 | namespace: 'users', 5 | state: { 6 | list: [], 7 | total: null, 8 | page: null, 9 | }, 10 | reducers: { 11 | save(state, { payload: { data: list, total, page } }) { 12 | return { ...state, list, total, page }; 13 | }, 14 | }, 15 | effects: { 16 | *fetch({ payload: { page = 1 } }, { call, put }) { 17 | const { data, headers } = yield call(usersService.fetch, { page }); 18 | yield put({ 19 | type: 'save', 20 | payload: { 21 | data, 22 | total: parseInt(headers['x-total-count'], 10), 23 | page: parseInt(page, 10), 24 | }, 25 | }); 26 | }, 27 | *remove({ payload: id }, { call, put }) { 28 | yield call(usersService.remove, id); 29 | yield put({ type: 'reload' }); 30 | }, 31 | *patch({ payload: { id, values } }, { call, put }) { 32 | yield call(usersService.patch, id, values); 33 | yield put({ type: 'reload' }); 34 | }, 35 | *create({ payload: values }, { call, put }) { 36 | yield call(usersService.create, values); 37 | yield put({ type: 'reload' }); 38 | }, 39 | *reload(action, { put, select }) { 40 | const page = yield select(state => state.users.page); 41 | yield put({ type: 'fetch', payload: { page } }); 42 | }, 43 | }, 44 | subscriptions: { 45 | setup({ dispatch, history }) { 46 | return history.listen(({ pathname, query }) => { 47 | if (pathname === '/users') { 48 | dispatch({ type: 'fetch', payload: query }); 49 | } 50 | }); 51 | }, 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Router } from 'dva/router'; 3 | 4 | const cached = {}; 5 | function registerModel(app, model) { 6 | if (!cached[model.namespace]) { 7 | app.model(model); 8 | cached[model.namespace] = 1; 9 | } 10 | } 11 | 12 | function RouterConfig({ history, app }) { 13 | const routes = [ 14 | { 15 | path: '/', 16 | name: 'IndexPage', 17 | getComponent(nextState, cb) { 18 | require.ensure([], (require) => { 19 | cb(null, require('./routes/IndexPage')); 20 | }); 21 | }, 22 | }, 23 | { 24 | path: '/users', 25 | name: 'UsersPage', 26 | getComponent(nextState, cb) { 27 | require.ensure([], (require) => { 28 | registerModel(app, require('./models/users')); 29 | cb(null, require('./routes/Users')); 30 | }); 31 | }, 32 | }, 33 | ]; 34 | 35 | return ; 36 | } 37 | 38 | export default RouterConfig; 39 | -------------------------------------------------------------------------------- /src/routes/IndexPage.css: -------------------------------------------------------------------------------- 1 | 2 | .normal { 3 | font-family: Georgia, sans-serif; 4 | margin-top: 3em; 5 | text-align: center; 6 | } 7 | 8 | .title { 9 | font-size: 2.5rem; 10 | font-weight: normal; 11 | letter-spacing: -1px; 12 | } 13 | 14 | .welcome { 15 | height: 328px; 16 | background: url(../assets/yay.jpg) no-repeat center 0; 17 | background-size: 388px 328px; 18 | } 19 | 20 | .list { 21 | font-size: 1.2em; 22 | margin-top: 1.8em; 23 | list-style: none; 24 | line-height: 1.5em; 25 | } 26 | 27 | .list code { 28 | background: #f7f7f7; 29 | } 30 | -------------------------------------------------------------------------------- /src/routes/IndexPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'dva'; 3 | import styles from './IndexPage.css'; 4 | import MainLayout from '../components/MainLayout/MainLayout'; 5 | 6 | function IndexPage({ location }) { 7 | return ( 8 | 9 |
10 |

Yay! Welcome to dva!

11 |
12 |
    13 |
  • To get started, edit src/index.js and save to reload.
  • 14 |
  • Getting Started
  • 15 |
16 |
17 | 18 | ); 19 | } 20 | 21 | IndexPage.propTypes = { 22 | }; 23 | 24 | export default connect()(IndexPage); 25 | -------------------------------------------------------------------------------- /src/routes/Users.css: -------------------------------------------------------------------------------- 1 | 2 | .normal { 3 | width: 900px; 4 | margin: 3em auto 0; 5 | } 6 | -------------------------------------------------------------------------------- /src/routes/Users.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'dva'; 3 | import styles from './Users.css'; 4 | import UsersComponent from '../components/Users/Users'; 5 | import MainLayout from '../components/MainLayout/MainLayout'; 6 | 7 | function Users({ location }) { 8 | return ( 9 | 10 |
11 | 12 |
13 |
14 | ); 15 | } 16 | 17 | export default connect()(Users); 18 | -------------------------------------------------------------------------------- /src/services/users.js: -------------------------------------------------------------------------------- 1 | import request from '../utils/request'; 2 | import { PAGE_SIZE } from '../constants'; 3 | 4 | export function fetch({ page }) { 5 | return request(`/api/users?_page=${page}&_limit=${PAGE_SIZE}`); 6 | } 7 | 8 | export function remove(id) { 9 | return request(`/api/users/${id}`, { 10 | method: 'DELETE', 11 | }); 12 | } 13 | 14 | export function patch(id, values) { 15 | return request(`/api/users/${id}`, { 16 | method: 'PATCH', 17 | body: JSON.stringify(values), 18 | }); 19 | } 20 | 21 | export function create(values) { 22 | return request('/api/users', { 23 | method: 'POST', 24 | body: JSON.stringify(values), 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/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 | 23 | checkStatus(response); 24 | 25 | const data = await response.json(); 26 | 27 | const ret = { 28 | data, 29 | headers: {}, 30 | }; 31 | 32 | if (response.headers.get('x-total-count')) { 33 | ret.headers['x-total-count'] = response.headers.get('x-total-count'); 34 | } 35 | 36 | return ret; 37 | } 38 | --------------------------------------------------------------------------------