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 (
);
}
}
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 (
);
}
}
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 (
);
}
}
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 (
{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 (
);
}
}
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);