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