├── .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 |
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 |
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 |
--------------------------------------------------------------------------------