[
  {
    "path": ".gitignore",
    "content": ".idea\n.vscode\nnode_modules\ndist\n"
  },
  {
    "path": ".roadhogrc",
    "content": "{\n  \"extraBabelPlugins\": [\n    [\"import\", {\n      \"libraryName\": \"antd\",\n      \"libraryDirectory\": \"lib\",\n      \"style\": \"css\"\n    }]\n  ]\n}\n"
  },
  {
    "path": "README.md",
    "content": "# React全家桶入门系列文章项目\n\n本教程涵盖React主流技术栈：\n\n- react - [github](https://github.com/facebook/react)\n- react-router - [github](https://github.com/ReactTraining/react-router)\n- redux - [github](https://github.com/reactjs/redux)\n- react-redux - [github](https://github.com/reactjs/react-redux)\n- react-router-redux - [github](https://github.com/reactjs/react-router-redux)\n- redux-saga - [github](https://github.com/redux-saga/redux-saga)\n- immutable - [github](https://github.com/facebook/immutable-js)\n- reselect - [github](https://github.com/reactjs/reselect)\n- antd - [github](https://github.com/ant-design/ant-design)\n\n请搭配[博客文章](http://blog.csdn.net/awaw00/article/category/6692955)食用。\n\n每篇文章的代码都打了相应的tag，如：\n\n[【React全家桶入门之二】项目搭建](http://blog.csdn.net/awaw00/article/details/54693780)的代码可以通过`git checkout -b 02 C02`来获取。\n\n其中C02就是第二篇的tag（由于第一篇为引言没有代码，为了tag与文章对应，tag从02开始）。\n\n*使用注意：由于一开始提交代码没有注意，C04的代码包括了【之五】的一部分代码（高阶组件），C05仅有【之五】剩余一部分的代码（组件化表单控件）。*\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"react-curd\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"src/index.js\",\n  \"scripts\": {\n    \"server\": \"node server/index.js\",\n    \"dev\": \"roadhog server\"\n  },\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"dependencies\": {\n    \"antd\": \"^2.7.0\",\n    \"react\": \"^15.4.2\",\n    \"react-dom\": \"^15.4.2\",\n    \"react-redux\": \"^5.0.3\",\n    \"react-router\": \"^3.0.2\",\n    \"redux\": \"^3.6.0\",\n    \"redux-thunk\": \"^2.2.0\"\n  },\n  \"devDependencies\": {\n    \"babel-plugin-import\": \"^1.1.0\",\n    \"json-server\": \"^0.9.4\"\n  }\n}\n"
  },
  {
    "path": "public/index.html",
    "content": "<!doctype html>\n<html lang=\"zh-cn\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\"\n        content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\">\n  <title>Hello React</title>\n</head>\n<body>\n  <div id=\"app\"></div>\n  <script src=\"./index.js\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "server/auth.js",
    "content": "const expireTime = 1000 * 60;\n\nmodule.exports = function (req, res, next) {\n  res.header('Access-Control-Expose-Headers', 'access-token');\n  const now = Date.now();\n\n  let unauthorized = true;\n  const token = req.headers['access-token'];\n  if (token) {\n    const expired = now - token > expireTime;\n    if (!expired) {\n      unauthorized = false;\n      res.header('access-token', now);\n    }\n  }\n\n  if (unauthorized) {\n    res.sendStatus(401);\n  } else {\n    next();\n  }\n};\n"
  },
  {
    "path": "server/db.json",
    "content": "{\n  \"user\": [\n    {\n      \"id\": 10000,\n      \"name\": \"一韬\",\n      \"age\": 25,\n      \"gender\": \"male\"\n    },\n    {\n      \"id\": 10001,\n      \"name\": \"张三\",\n      \"age\": 30,\n      \"gender\": \"female\"\n    }\n  ],\n  \"book\": [\n    {\n      \"name\": \"前端从入门到精通1\",\n      \"price\": 9300,\n      \"owner_id\": 10000,\n      \"id\": 10000\n    },\n    {\n      \"id\": 10001,\n      \"name\": \"Java从入门到放弃\",\n      \"price\": 1990,\n      \"owner_id\": 10001\n    }\n  ]\n}"
  },
  {
    "path": "server/index.js",
    "content": "const path = require('path');\nconst jsonServer = require('json-server');\nconst server = jsonServer.create();\nconst router = jsonServer.router(path.join(__dirname, 'db.json'));\nconst middlewares = jsonServer.defaults();\n\nserver.use(jsonServer.bodyParser);\nserver.use(middlewares);\n\nserver.post('/login', function (req, res, next) {\n  res.header('Access-Control-Expose-Headers', 'access-token');\n  const {account, password} = req.body;\n  if (account === 'admin' && password === '123456') {\n    res.header('access-token', Date.now());\n    res.json(true);\n  } else {\n    res.json(false);\n  }\n});\n\nserver.use(require('./auth'));\nserver.use(router);\n\nserver.listen(3000, function () {\n  console.log('JSON Server is running in http://localhost:3000');\n});\n"
  },
  {
    "path": "src/components/AutoComplete.js",
    "content": "import React, { PropTypes } from 'react';\nimport { Input } from 'antd';\nimport style from '../styles/auto-complete.less';\n\nfunction getItemValue (item) {\n  return item.value || item;\n}\n\nclass AutoComplete extends React.Component {\n  constructor (props) {\n    super(props);\n\n    this.state = {\n      show: false,\n      displayValue: '',\n      activeItemIndex: -1\n    };\n\n    this.handleKeyDown = this.handleKeyDown.bind(this);\n    this.handleLeave = this.handleLeave.bind(this);\n  }\n\n  handleChange (value) {\n    this.setState({activeItemIndex: -1, displayValue: ''});\n    this.props.onChange(value);\n  }\n\n  handleKeyDown (e) {\n    const {activeItemIndex} = this.state;\n    const {options} = this.props;\n\n    switch (e.keyCode) {\n      case 13: {\n        if (activeItemIndex >= 0) {\n          e.preventDefault();\n          e.stopPropagation();\n          this.handleChange(getItemValue(options[activeItemIndex]));\n        }\n        break;\n      }\n      case 38:\n      case 40: {\n        e.preventDefault();\n        this.moveItem(e.keyCode === 38 ? 'up' : 'down');\n        break;\n      }\n    }\n  }\n\n  moveItem (direction) {\n    const {activeItemIndex} = this.state;\n    const {options} = this.props;\n    const lastIndex = options.length - 1;\n    let newIndex = -1;\n\n    if (direction === 'up') {\n      if (activeItemIndex === -1) {\n        newIndex = lastIndex;\n      } else {\n        newIndex = activeItemIndex - 1;\n      }\n    } else {\n      if (activeItemIndex < lastIndex) {\n        newIndex = activeItemIndex + 1;\n      }\n    }\n\n    let newDisplayValue = '';\n    if (newIndex >= 0) {\n      newDisplayValue = getItemValue(options[newIndex]);\n    }\n\n    this.setState({\n      displayValue: newDisplayValue,\n      activeItemIndex: newIndex\n    });\n  }\n\n  handleEnter (index) {\n    const currentItem = this.props.options[index];\n    this.setState({activeItemIndex: index, displayValue: getItemValue(currentItem)});\n  }\n\n  handleLeave () {\n    this.setState({activeItemIndex: -1, displayValue: ''});\n  }\n\n  render () {\n    const {show, displayValue, activeItemIndex} = this.state;\n    const {value, options} = this.props;\n    return (\n      <div className={style.wrapper}>\n        <Input\n          value={displayValue || value}\n          onChange={e => this.handleChange(e.target.value)}\n          onKeyDown={this.handleKeyDown}\n          onFocus={() => this.setState({show: true})}\n          onBlur={() => this.setState({show: false})}\n        />\n        {show && options.length > 0 && (\n          <ul className={style.options} onMouseLeave={this.handleLeave}>\n            {\n              options.map((item, index) => {\n                return (\n                  <li\n                    key={index}\n                    className={index === activeItemIndex ? style.active : ''}\n                    onMouseEnter={() => this.handleEnter(index)}\n                    onClick={() => this.handleChange(getItemValue(item))}\n                  >\n                    {item.text || item}\n                  </li>\n                );\n              })\n            }\n          </ul>\n        )}\n      </div>\n    );\n  }\n}\n\nAutoComplete.propTypes = {\n  value: PropTypes.any,\n  options: PropTypes.array,\n  onChange: PropTypes.func\n};\n\nexport default AutoComplete;\n"
  },
  {
    "path": "src/components/BookEditor.js",
    "content": "import React from 'react';\nimport { Input, InputNumber, Form, Button, message } from 'antd';\nimport AutoComplete from '../components/AutoComplete';\nimport request, { get } from '../utils/request';\n\nconst Option = AutoComplete.Option;\nconst FormItem = Form.Item;\nconst formLayout = {\n  labelCol: {\n    span: 4\n  },\n  wrapperCol: {\n    span: 16\n  }\n};\n\nclass BookEditor extends React.Component {\n  constructor (props) {\n    super(props);\n    this.state = {\n      recommendUsers: []\n    };\n    this.handleSubmit = this.handleSubmit.bind(this);\n    this.handleOwnerIdChange = this.handleOwnerIdChange.bind(this);\n  }\n\n  componentDidMount () {\n    // 在componentWillMount里使用form.setFieldsValue无法设置表单的值\n    // 所以在componentDidMount里进行赋值\n    // see: https://github.com/ant-design/ant-design/issues/4802\n    const {editTarget, form} = this.props;\n    if (editTarget) {\n      form.setFieldsValue(editTarget);\n    }\n  }\n\n  handleSubmit (e) {\n    e.preventDefault();\n\n    const {form, editTarget} = this.props;\n\n    form.validateFields((err, values) => {\n      if (err) {\n        message.warn(err);\n        return;\n      }\n\n      let editType = '添加';\n      let apiUrl = 'http://localhost:3000/book';\n      let method = 'post';\n      if (editTarget) {\n        editType = '编辑';\n        apiUrl += '/' + editTarget.id;\n        method = 'put';\n      }\n\n      request(method, apiUrl, values)\n        .then((res) => {\n          if (res.id) {\n            message.success(editType + '书本成功');\n            this.context.router.push('/book/list');\n          } else {\n            message.error(editType + '失败');\n          }\n        })\n        .catch((err) => console.error(err));\n    });\n  }\n\n  getRecommendUsers (partialUserId) {\n    get('http://localhost:3000/user?id_like=' + partialUserId)\n      .then((res) => {\n        if (res.length === 1 && res[0].id === partialUserId) {\n          return;\n        }\n\n        this.setState({\n          recommendUsers: res.map((user) => {\n            return {\n              text: `${user.id}（${user.name}）`,\n              value: user.id\n            };\n          })\n        });\n      });\n  }\n\n  timer = 0;\n\n  handleOwnerIdChange (value) {\n    this.setState({recommendUsers: []});\n\n    if (this.timer) {\n      clearTimeout(this.timer);\n    }\n\n    if (value) {\n      this.timer = setTimeout(() => {\n        this.getRecommendUsers(value);\n        this.timer = 0;\n      }, 200);\n    }\n  }\n\n  render () {\n    const {recommendUsers} = this.state;\n    const {form} = this.props;\n    const {getFieldDecorator} = form;\n    return (\n      <Form onSubmit={this.handleSubmit} style={{width: '400px'}}>\n        <FormItem label=\"书名：\" {...formLayout}>\n          {getFieldDecorator('name', {\n            rules: [\n              {\n                required: true,\n                message: '请输入书名'\n              }\n            ]\n          })(<Input type=\"text\"/>)}\n        </FormItem>\n\n        <FormItem label=\"价格：\" {...formLayout}>\n          {getFieldDecorator('price', {\n            rules: [\n              {\n                required: true,\n                message: '请输入价格',\n                type: 'number'\n              },\n              {\n                min: 1,\n                max: 99999,\n                type: 'number',\n                message: '请输入1~99999的数字'\n              }\n            ]\n          })(<InputNumber/>)}\n        </FormItem>\n\n        <FormItem label=\"所有者：\" {...formLayout}>\n          {getFieldDecorator('owner_id', {\n            rules: [\n              {\n                required: true,\n                message: '请输入所有者ID'\n              },\n              {\n                pattern: /^\\d*$/,\n                message: '请输入正确的ID'\n              }\n            ]\n          })(\n            <AutoComplete\n              options={recommendUsers}\n              onChange={this.handleOwnerIdChange}\n            />\n          )}\n        </FormItem>\n        <FormItem wrapperCol={{span: formLayout.wrapperCol.span, offset: formLayout.labelCol.span}}>\n          <Button type=\"primary\" htmlType=\"submit\">提交</Button>\n        </FormItem>\n      </Form>\n    );\n  }\n}\n\nBookEditor.contextTypes = {\n  router: React.PropTypes.object.isRequired\n};\n\nBookEditor = Form.create()(BookEditor);\n\nexport default BookEditor;\n"
  },
  {
    "path": "src/components/FormItem.js",
    "content": "import React from 'react';\n\nclass FormItem extends React.Component {\n  render () {\n    const {label, children, valid, error} = this.props;\n    return (\n      <div>\n        <label>{label}</label>\n        {children}\n        {!valid && <span>{error}</span>}\n      </div>\n    );\n  }\n}\n\nexport default FormItem;\n"
  },
  {
    "path": "src/components/UserEditor.js",
    "content": "import React from 'react';\nimport { Form, Input, InputNumber, Select, Button, message } from 'antd';\nimport request from '../utils/request';\n\nconst FormItem = Form.Item;\n\nconst formLayout = {\n  labelCol: {\n    span: 4\n  },\n  wrapperCol: {\n    span: 16\n  }\n};\n\nclass UserEditor extends React.Component {\n  componentDidMount () {\n    // 在componentWillMount里使用form.setFieldsValue无法设置表单的值\n    // 所以在componentDidMount里进行赋值\n    // see: https://github.com/ant-design/ant-design/issues/4802\n    const {editTarget, form} = this.props;\n    if (editTarget) {\n      form.setFieldsValue(editTarget);\n    }\n  }\n\n  handleSubmit (e) {\n    e.preventDefault();\n\n    const {form, editTarget} = this.props;\n\n    form.validateFields((err, values) => {\n      if (!err) {\n        let editType = '添加';\n        let apiUrl = 'http://localhost:3000/user';\n        let method = 'post';\n        if (editTarget) {\n          editType = '编辑';\n          apiUrl += '/' + editTarget.id;\n          method = 'put';\n        }\n\n        request(method, apiUrl, values)\n          .then((res) => {\n            if (res.id) {\n              message.success(editType + '用户成功');\n              this.context.router.push('/user/list');\n            } else {\n              message.error(editType + '失败');\n            }\n          })\n          .catch((err) => console.error(err));\n\n      } else {\n        message.warn(err);\n      }\n    });\n  }\n\n  render () {\n    const {form} = this.props;\n    const {getFieldDecorator} = form;\n    return (\n      <div style={{width: '400px'}}>\n        <Form onSubmit={(e) => this.handleSubmit(e)}>\n          <FormItem label=\"用户名：\" {...formLayout}>\n            {getFieldDecorator('name', {\n              rules: [\n                {\n                  required: true,\n                  message: '请输入用户名'\n                },\n                {\n                  pattern: /^.{1,4}$/,\n                  message: '用户名最多4个字符'\n                }\n              ]\n            })(\n              <Input type=\"text\"/>\n            )}\n          </FormItem>\n          <FormItem label=\"年龄：\" {...formLayout}>\n            {getFieldDecorator('age', {\n              rules: [\n                {\n                  required: true,\n                  message: '请输入年龄',\n                  type: 'number'\n                },\n                {\n                  min: 1,\n                  max: 100,\n                  message: '请输入1~100的年龄',\n                  type: 'number'\n                }\n              ]\n            })(\n              <InputNumber/>\n            )}\n          </FormItem>\n          <FormItem label=\"性别：\" {...formLayout}>\n            {getFieldDecorator('gender', {\n              rules: [\n                {\n                  required: true,\n                  message: '请选择性别'\n                }\n              ]\n            })(\n              <Select placeholder=\"请选择\">\n                <Select.Option value=\"male\">男</Select.Option>\n                <Select.Option value=\"female\">女</Select.Option>\n              </Select>\n            )}\n          </FormItem>\n          <FormItem wrapperCol={{...formLayout.wrapperCol, offset: formLayout.labelCol.span}}>\n            <Button type=\"primary\" htmlType=\"submit\">提交</Button>\n          </FormItem>\n        </Form>\n      </div>\n    );\n  }\n}\n\nUserEditor.contextTypes = {\n  router: React.PropTypes.object.isRequired\n};\n\nUserEditor = Form.create()(UserEditor);\n\nexport default UserEditor;\n"
  },
  {
    "path": "src/createStore.js",
    "content": "import { combineReducers } from 'redux';\n\nexport default function (initialState) {\n\n}\n"
  },
  {
    "path": "src/index.js",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport { Router, Route, hashHistory } from 'react-router';\nimport HomePage from './pages/Home';\nimport UserAddPage from './pages/UserAdd';\nimport UserListPage from './pages/UserList';\nimport UserEditPage from './pages/UserEdit';\nimport BookAddPage from './pages/BookAdd';\nimport BookListPage from './pages/BookList';\nimport BookEditPage from './pages/BookEdit';\nimport LoginPage from './pages/Login';\nimport HomeLayout from './layouts/HomeLayout';\n\nReactDOM.render((\n  <Router history={hashHistory}>\n    <Route component={HomeLayout}>\n      <Route path=\"/\" component={HomePage}/>\n      <Route path=\"/user/add\" component={UserAddPage}/>\n      <Route path=\"/user/list\" component={UserListPage}/>\n      <Route path=\"/user/edit/:id\" component={UserEditPage}/>\n      <Route path=\"/book/add\" component={BookAddPage}/>\n      <Route path=\"/book/list\" component={BookListPage}/>\n      <Route path=\"/book/edit/:id\" component={BookEditPage}/>\n    </Route>\n    <Route path=\"/login\" component={LoginPage}/>\n  </Router>\n), document.getElementById('app'));\n"
  },
  {
    "path": "src/layouts/HomeLayout.js",
    "content": "import React from 'react';\nimport { Link } from 'react-router';\nimport { Menu, Icon } from 'antd';\nimport style from '../styles/home-layout.less';\n\nconst SubMenu = Menu.SubMenu;\nconst MenuItem = Menu.Item;\n\nclass HomeLayout extends React.Component {\n  render () {\n    const {children} = this.props;\n    return (\n      <div>\n        <header className={style.header}>\n          <Link to=\"/\">ReactManager</Link>\n        </header>\n\n        <main className={style.main}>\n          <div className={style.menu}>\n            <Menu mode=\"inline\" theme=\"dark\" style={{width: '240px'}}>\n              <SubMenu key=\"user\" title={<span><Icon type=\"user\"/><span>用户管理</span></span>}>\n                <MenuItem key=\"user-list\">\n                  <Link to=\"/user/list\">用户列表</Link>\n                </MenuItem>\n                <MenuItem key=\"user-add\">\n                  <Link to=\"/user/add\">添加用户</Link>\n                </MenuItem>\n              </SubMenu>\n\n              <SubMenu key=\"book\" title={<span><Icon type=\"book\"/><span>图书管理</span></span>}>\n                <MenuItem key=\"book-list\">\n                  <Link to=\"/book/list\">图书列表</Link>\n                </MenuItem>\n                <MenuItem key=\"book-add\">\n                  <Link to=\"/book/add\">添加图书</Link>\n                </MenuItem>\n              </SubMenu>\n            </Menu>\n          </div>\n\n          <div className={style.content}>\n            {children}\n          </div>\n        </main>\n      </div>\n    );\n  }\n}\n\nexport default HomeLayout;\n"
  },
  {
    "path": "src/pages/BookAdd.js",
    "content": "import React from 'react';\nimport BookEditor from '../components/BookEditor';\n\nclass BookAdd extends React.Component {\n  render () {\n    return (\n      <BookEditor/>\n    );\n  }\n}\n\nexport default BookAdd;\n"
  },
  {
    "path": "src/pages/BookEdit.js",
    "content": "import React from 'react';\nimport BookEditor from '../components/BookEditor';\nimport { get } from '../utils/request';\n\nclass BookEdit extends React.Component {\n  constructor (props) {\n    super(props);\n    this.state = {\n      book: null\n    };\n  }\n\n  componentWillMount () {\n    const bookId = this.context.router.params.id;\n    get('http://localhost:3000/book/' + bookId)\n      .then(res => {\n        this.setState({\n          book: res\n        });\n      });\n  }\n\n  render () {\n    const {book} = this.state;\n    return book ? <BookEditor editTarget={book}/> : <span>加载中...</span>;\n  }\n}\n\nBookEdit.contextTypes = {\n  router: React.PropTypes.object.isRequired\n};\n\nexport default BookEdit;\n"
  },
  {
    "path": "src/pages/BookList.js",
    "content": "import React from 'react';\nimport { message, Table, Button, Popconfirm } from 'antd';\nimport { get, del } from '../utils/request';\n\nclass BookList extends React.Component {\n  constructor (props) {\n    super(props);\n    this.state = {\n      bookList: []\n    };\n  }\n\n  componentWillMount () {\n    get('http://localhost:3000/book')\n      .then(res => {\n        this.setState({\n          bookList: res\n        });\n      });\n  }\n\n  handleEdit (book) {\n    this.context.router.push('/book/edit/' + book.id);\n  }\n\n  handleDel (book) {\n    del('http://localhost:3000/book/' + book.id)\n      .then(res => {\n        this.setState({\n          bookList: this.state.bookList.filter(item => item.id !== book.id)\n        });\n        message.success('删除图书成功');\n      })\n      .catch(err => {\n        console.error(err);\n        message.error('删除图书失败');\n      });\n  }\n\n  render () {\n    const {bookList} = this.state;\n\n    const columns = [\n      {\n        title: '图书ID',\n        dataIndex: 'id'\n      },\n      {\n        title: '书名',\n        dataIndex: 'name'\n      },\n      {\n        title: '价格',\n        dataIndex: 'price',\n        render: (text, record) => <span>&yen;{record.price / 100}</span>\n      },\n      {\n        title: '所有者ID',\n        dataIndex: 'owner_id'\n      },\n      {\n        title: '操作',\n        render: (text, record) => (\n          <Button.Group type=\"ghost\">\n            <Button size=\"small\" onClick={() => this.handleEdit(record)}>编辑</Button>\n            <Popconfirm title=\"确定要删除吗？\" onConfirm={() => this.handleDel(record)}>\n              <Button size=\"small\">删除</Button>\n            </Popconfirm>\n          </Button.Group>\n        )\n      }\n    ];\n\n    return (\n      <Table columns={columns} dataSource={bookList} rowKey={row => row.id}/>\n    );\n  }\n}\n\nBookList.contextTypes = {\n  router: React.PropTypes.object.isRequired\n};\n\nexport default BookList;"
  },
  {
    "path": "src/pages/Home.js",
    "content": "import React from 'react';\nimport style from '../styles/home-page.less';\n\nclass Home extends React.Component {\n  render () {\n    return (\n      <div className={style.welcome}>\n        Welcome\n      </div>\n    );\n  }\n}\n\nexport default Home;\n"
  },
  {
    "path": "src/pages/Login.js",
    "content": "import React from 'react';\nimport { Icon, Form, Input, Button, message } from 'antd';\nimport { post } from '../utils/request';\nimport style from '../styles/login-page.less';\n\nconst FormItem = Form.Item;\n\nclass Login extends React.Component {\n  constructor () {\n    super();\n    this.handleSubmit = this.handleSubmit.bind(this);\n  }\n\n  handleSubmit (e) {\n    e.preventDefault();\n\n    this.props.form.validateFields((err, values) => {\n      if (!err) {\n        post('http://localhost:3000/login', values)\n          .then((res) => {\n            if (res) {\n              message.info('登录成功');\n              this.context.router.push('/');\n            } else {\n              message.info('登录失败，账号或密码错误');\n            }\n          });\n      }\n    });\n  }\n\n  render () {\n    const {form} = this.props;\n    const {getFieldDecorator} = form;\n    return (\n      <div className={style.wrapper}>\n\n        <div className={style.body}>\n          <header className={style.header}>\n            ReactManager\n          </header>\n\n          <section className={style.form}>\n\n            <Form onSubmit={this.handleSubmit}>\n              <FormItem>\n                {getFieldDecorator('account', {\n                  rules: [\n                    {\n                      required: true,\n                      message: '请输入管理员账号',\n                      type: 'string'\n                    }\n                  ]\n                })(\n                  <Input type=\"text\" addonBefore={<Icon type=\"user\"/>}/>\n                )}\n              </FormItem>\n\n              <FormItem>\n                {getFieldDecorator('password', {\n                  rules: [\n                    {\n                      required: true,\n                      message: '请输入密码',\n                      type: 'string'\n                    }\n                  ]\n                })(\n                  <Input type=\"password\" addonBefore={<Icon type=\"lock\"/>}/>\n                )}\n              </FormItem>\n\n              <Button className={style.btn} type=\"primary\" htmlType=\"submit\">Sign In</Button>\n            </Form>\n\n          </section>\n\n        </div>\n\n      </div>\n    );\n  }\n}\n\nLogin.contextTypes = {\n  router: React.PropTypes.object.isRequired\n};\n\nLogin = Form.create()(Login);\n\nexport default Login;\n"
  },
  {
    "path": "src/pages/UserAdd.js",
    "content": "import React from 'react';\nimport UserEditor from '../components/UserEditor';\n\nclass UserAdd extends React.Component {\n  render () {\n    return (\n      <UserEditor/>\n    );\n  }\n}\n\nexport default UserAdd;\n"
  },
  {
    "path": "src/pages/UserEdit.js",
    "content": "import React from 'react';\nimport UserEditor from '../components/UserEditor';\nimport { get } from '../utils/request';\n\nclass UserEdit extends React.Component {\n  constructor (props) {\n    super(props);\n    this.state = {\n      user: null\n    };\n  }\n\n  componentWillMount () {\n    const userId = this.context.router.params.id;\n    get('http://localhost:3000/user/' + userId)\n      .then(res => {\n        this.setState({\n          user: res\n        });\n      });\n  }\n\n  render () {\n    const {user} = this.state;\n    return user ? <UserEditor editTarget={user}/> : <span>加载中...</span>;\n  }\n}\n\nUserEdit.contextTypes = {\n  router: React.PropTypes.object.isRequired\n};\n\nexport default UserEdit;\n"
  },
  {
    "path": "src/pages/UserList.js",
    "content": "import React from 'react';\nimport { message, Table, Button, Popconfirm } from 'antd';\nimport { get, del } from '../utils/request';\n\nclass UserList extends React.Component {\n  constructor (props) {\n    super(props);\n    this.state = {\n      userList: []\n    };\n  }\n\n  componentWillMount () {\n    get('http://localhost:3000/user')\n      .then(res => {\n        this.setState({\n          userList: res\n        });\n      });\n  }\n\n  handleEdit (user) {\n    this.context.router.push('/user/edit/' + user.id);\n  }\n\n  handleDel (user) {\n    del('http://localhost:3000/user/' + user.id)\n      .then(res => {\n        this.setState({\n          userList: this.state.userList.filter(item => item.id !== user.id)\n        });\n        message.success('删除用户成功');\n      })\n      .catch(err => {\n        console.error(err);\n        message.error('删除用户失败');\n      });\n  }\n\n  render () {\n    const {userList} = this.state;\n\n    const columns = [\n      {\n        title: '用户ID',\n        dataIndex: 'id'\n      },\n      {\n        title: '用户名',\n        dataIndex: 'name'\n      },\n      {\n        title: '性别',\n        dataIndex: 'gender'\n      },\n      {\n        title: '年龄',\n        dataIndex: 'age'\n      },\n      {\n        title: '操作',\n        render: (text, record) => {\n          return (\n            <Button.Group type=\"ghost\">\n              <Button size=\"small\" onClick={() => this.handleEdit(record)}>编辑</Button>\n              <Popconfirm title=\"确定要删除吗？\" onConfirm={() => this.handleDel(record)}>\n                <Button size=\"small\">删除</Button>\n              </Popconfirm>\n            </Button.Group>\n          );\n        }\n      }\n    ];\n\n    return (\n      <Table columns={columns} dataSource={userList} rowKey={row => row.id}/>\n    );\n  }\n}\n\nUserList.contextTypes = {\n  router: React.PropTypes.object.isRequired\n};\n\nexport default UserList;\n"
  },
  {
    "path": "src/reducers/index.js",
    "content": ""
  },
  {
    "path": "src/reducers/user.js",
    "content": "export function dataSource (state = [], action) {\n  switch (action.type) {\n\n    default:\n      return state;\n  }\n}\n\nexport function formValues (state = {}, action) {\n  switch (action.type) {\n    default:\n      return state;\n  }\n}\n\nexport function editTarget (state = null, action) {\n  switch (action.type) {\n    default:\n      return state;\n  }\n}\n"
  },
  {
    "path": "src/styles/auto-complete.less",
    "content": ".wrapper {\n  display: inline-block;\n  position: relative;\n}\n\n.options {\n  z-index: 2;\n  margin: 0;\n  padding: 0;\n  top: 110%;\n  left: 0;\n  right: 0;\n  list-style: none;\n  position: absolute;\n  background-color: #fff;\n  box-shadow: 1px 1px 10px 0 rgba(0, 0, 0, .6);\n\n  > li {\n    padding: 3px 6px;\n\n    &.active {\n      background-color: #0094ff;\n      color: white;\n    }\n  }\n}\n"
  },
  {
    "path": "src/styles/home-layout.less",
    "content": ".main {\n  height: 100vh;\n  padding-top: 50px;\n}\n\n.header {\n  position: absolute;\n  top: 0;\n  height: 50px;\n  width: 100%;\n  font-size: 18px;\n  padding: 0 20px;\n  line-height: 50px;\n  background-color: #108ee9;\n  color: #fff;\n\n  a {\n    color: inherit;\n  }\n}\n\n.menu {\n  height: 100%;\n  width: 240px;\n  float: left;\n  background-color: #404040;\n}\n\n.content {\n  height: 100%;\n  padding: 12px;\n  overflow: auto;\n  margin-left: 240px;\n  align-self: stretch;\n}\n"
  },
  {
    "path": "src/styles/home-page.less",
    "content": ".welcome {\n  width: 100%;\n  height: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  font-size: 32px;\n}\n"
  },
  {
    "path": "src/styles/login-page.less",
    "content": ".wrapper {\n  height: 100vh;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.body {\n  width: 360px;\n  box-shadow: 1px 1px 10px 0 rgba(0, 0, 0, .3);\n}\n\n.header {\n  color: #fff;\n  font-size: 24px;\n  padding: 30px 20px;\n  background-color: #108ee9;\n}\n\n.form {\n  margin-top: 12px;\n  padding: 24px;\n}\n\n.btn {\n  width: 100%;\n}\n"
  },
  {
    "path": "src/type.js",
    "content": "export const USER_UPDATE_FORM = 'USER_UPDATE_FORM';\n\nexport const USER_DETAIL_REQUEST = 'USER_DETAIL_REQUEST';\nexport const USER_DETAIL_SUCCESS = 'USER_DETAIL_SUCCESS';\nexport const USER_DETAIL_FAILURE = 'USER_DETAIL_FAILURE';\n\nexport const USER_LIST_REQUEST = 'USER_LIST_REQUEST';\nexport const USER_LIST_SUCCESS = 'USER_LIST_SUCCESS';\nexport const USER_LIST_FAILURE = 'USER_LIST_FAILURE';\n\nexport const USER_ADD_REQUEST = 'USER_ADD_REQUEST';\nexport const USER_ADD_SUCCESS = 'USER_ADD_SUCCESS';\nexport const USER_ADD_FAILURE = 'USER_ADD_FAILURE';\n\nexport const USER_UPDATE_REQUEST = 'USER_UPDATE_REQUEST';\nexport const USER_UPDATE_SUCCESS = 'USER_UPDATE_SUCCESS';\nexport const USER_UPDATE_FAILURE = 'USER_UPDATE_FAILURE';\n\nexport const USER_DEL_REQUEST = 'USER_DEL_REQUEST';\nexport const USER_DEL_SUCCESS = 'USER_DEL_SUCCESS';\nexport const USER_DEL_FAILURE = 'USER_DEL_FAILURE';\n"
  },
  {
    "path": "src/utils/formProvider.js",
    "content": "import React from 'react';\n\nfunction formProvider (fields) {\n  return function (Comp) {\n\n    const initialFormState = {};\n    for (const key in fields) {\n      initialFormState[key] = {\n        value: fields[key].defaultValue,\n        error: ''\n      };\n    }\n\n    class FormComponent extends React.Component {\n      constructor (props) {\n        super(props);\n        this.state = {\n          form: initialFormState,\n          formValid: false\n        };\n\n        this.handleValueChange = this.handleValueChange.bind(this);\n        this.setFormValues = this.setFormValues.bind(this);\n      }\n\n      setFormValues (values) {\n        if (!values) {\n          return;\n        }\n\n        const {form} = this.state;\n        let newForm = {...form};\n        for (const field in form) {\n          if (form.hasOwnProperty(field)) {\n            if (typeof values[field] !== 'undefined') {\n              newForm[field] = {...newForm[field], value: values[field]};\n            }\n            newForm[field].valid = true;\n          }\n        }\n\n        this.setState({form: newForm});\n      }\n\n      handleValueChange (fieldName, value) {\n        const { form } = this.state;\n        const fieldState = form[fieldName];\n\n        const newFieldState = {...fieldState, value, valid: true, error: ''};\n\n        const fieldRules = fields[fieldName].rules;\n\n        for (let i = 0; i < fieldRules.length; i++) {\n          const {pattern, error} = fieldRules[i];\n          let valid = false;\n          if (typeof pattern === 'function') {\n            valid = pattern(value);\n          } else {\n            valid = pattern.test(value);\n          }\n\n          if (!valid) {\n            newFieldState.valid = false;\n            newFieldState.error = error;\n            break;\n          }\n        }\n\n        const newForm = {...form, [fieldName]: newFieldState};\n        const formValid = Object.values(newForm).every(f => f.valid);\n\n        this.setState({\n          form: newForm,\n          formValid\n        });\n      }\n\n      render () {\n        const {form, formValid} = this.state;\n        return (\n          <Comp\n            {...this.props}\n            form={form}\n            formValid={formValid}\n            onFormChange={this.handleValueChange}\n            setFormValues={this.setFormValues}\n          />\n        );\n      }\n    }\n\n    return FormComponent;\n  }\n}\n\nexport default formProvider;\n"
  },
  {
    "path": "src/utils/request.js",
    "content": "import { hashHistory } from 'react-router';\n\nexport default function request (method, url, body) {\n  method = method.toUpperCase();\n  if (method === 'GET') {\n    body = undefined;\n  } else {\n    body = body && JSON.stringify(body);\n  }\n\n  return fetch(url, {\n    method,\n    headers: {\n      'Content-Type': 'application/json',\n      'Accept': 'application/json',\n      'Access-Token': sessionStorage.getItem('access_token') || ''\n    },\n    body\n  })\n    .then((res) => {\n      if (res.status === 401) {\n        hashHistory.push('/login');\n        return Promise.reject('Unauthorized.');\n      } else {\n        const token = res.headers.get('access-token');\n        if (token) {\n          sessionStorage.setItem('access_token', token);\n        }\n        return res.json();\n      }\n    });\n}\n\nexport const get = url => request('GET', url);\nexport const post = (url, body) => request('POST', url, body);\nexport const put = (url, body) => request('PUT', url, body);\nexport const del = (url, body) => request('DELETE', url, body);\n"
  }
]