概述:
基于React、Redux,参考官方示例,实现组件状态管理。
文件目录:
│ .babelrc
│ .eslintrc
│ package.json
│
├─config
│ webpack.config.js
│ webpack.production.config.js
│
├─public
└─src
├─company
│ │ index.js
│ │ index.tmpl.html
│ │
│ ├─actions
│ │ items.js
│ │ visible.js
│ │
│ ├─component
│ │ Create.js
│ │ Error.js
│ │ Footer.js
│ │ Header.js
│ │ index.js
│ │ Item.js
│ │ ItemList.js
│ │ Link.js
│ │ RowLink.js
│ │ style.js
│ │ Title.js
│ │
│ ├─container
│ │ CreateItem.js
│ │ FilterLink.js
│ │ VisibleItemList.js
│ │
│ └─reducers
│ filter.js
│ index.js
│ items.js
│
└─static
├─css
│ common.css
│
└─images
180403.png
favicon.png
package.json
{
"name": "demos",
"version": "1.0.0",
"description": "demos",
"main": "index.js",
"scripts": {
"eslint": "eslint --ext .js src",
"eslint-fix": "eslint --fix src",
"deves": "webpack-dev-server --open --mode development --config ./config/webpack.config.js",
"build": "webpack --mode production --progress --config ./config/webpack.production.config.js"
},
"author": "HeJun",
"license": "ISC",
"repository": {
"type": "git",
"url": "git.nsecn.com"
},
"devDependencies": {
"autoprefixer": "^8.4.1",
"babel-core": "^6.26.0",
"babel-loader": "^7.1.4",
"babel-plugin-react-transform": "^3.0.0",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1",
"babel-standalone": "^6.26.0",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-eslint": "^8.2.2",
"babel-polyfill": "^6.26.0",
"clean-webpack-plugin": "^0.1.19",
"css-loader": "^0.28.11",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"file-loader": "^1.1.11",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.1.0",
"lodash": "^4.17.5",
"postcss-loader": "^2.1.4",
"react-transform-hmr": "^1.0.4",
"style-loader": "^0.20.3",
"uglifyjs-webpack-plugin": "^1.2.4",
"url-loader": "^1.0.1",
"webpack": "~4.5.0",
"webpack-cli": "^2.0.13",
"webpack-dev-server": "^3.1.1",
"zip-webpack-plugin": "^3.0.0",
"moment": "^2.22.0",
"eslint": "^4.19.1",
"eslint-plugin-import": "^2.10.0",
"eslint-plugin-react": "^7.7.0"
},
"dependencies": {
"prop-types": "^15.6.1",
"react": "^16.2.0",
"react-dom": "^16.2.0",
"redux": "^4.0.0",
"react-redux": "^5.0.7",
"react-router-dom": "^4.2.2"
}
}
.babelrc
{
presets: ["env", "react"],
"env": {
"development": {
"plugins": [
[
"react-transform", {
"transforms": [
{
"transform": "react-transform-hmr",
"imports": ["react"],
"locals": ["module"]
}
]
}
],
["transform-object-rest-spread", {
"useBuiltIns": true
}]
]
}
}
}
webpack.config.js
const path = require('path');
const webpack = require('webpack');
const autoprefixer = require('autoprefixer');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const base = path.join(__dirname, '..', 'src');
const dist = path.join(__dirname, '..', 'public');
const favicon = path.join(base, 'static', 'images', 'favicon.png');
// 常量
const company = 'company';
module.exports = {
// 入口文件
entry: {
company: ['babel-polyfill', path.join(base, company, 'index.js')]
},
// 抽取公共JS
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'common',
priority: 10,
chunks: 'all'
}
}
}
},
output: {
// 打包后文件路径
path: path.join(dist),
// 打包后输出文件
filename: 'bundle.[name].[hash:8].js'
},
// 发布时设置为null
devtool: 'eval-source-map',
performance: {
hints: false
},
devServer: {
// 本地服务器加载的目录
contentBase: path.join(dist),
port: 8000,
// 不跳转
historyApiFallback: true,
// 实时刷新
inline: true
},
module: {
rules: [
{
test: /(\.jsx|\.js)$/,
use: {
loader: 'babel-loader'
},
exclude: /node_modules/
},
{
test: /\.html$/,
use: {
loader: 'html-loader?minimize=false'
}
},
{
test: /\.(png|jpe?g|gif|svg)$/,
use: {
loader: 'url-loader?limit=1024&name=images/[hash:12].[ext]'
}
},
{
test: /\.css$/,
use: [
{
loader: 'style-loader'
},
{
// 启用CSS模块
loader: 'css-loader',
options: {
module: true
}
},
{
// CSS类自动名称
loader: 'postcss-loader',
options: {
plugins: [
autoprefixer
]
}
}
]
}
]
},
plugins: [
new webpack.BannerPlugin('DEMO COPYRIGHT'),
new HtmlWebpackPlugin({
chunks: ['common', company],
template: path.join(base, company, 'index.tmpl.html'),
filename: 'index.html',
favicon: favicon
}),
// 热加载模块插件
new webpack.HotModuleReplacementPlugin()
]
}
webpack.production.config.js
const path = require('path');
const moment = require('moment');
const webpack = require('webpack');
const autoprefixer = require('autoprefixer');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const CleanWebpackPlugin = require("clean-webpack-plugin");
const ZipPlugin = require('zip-webpack-plugin');
const base = path.join(__dirname, '..', 'src');
const dist = path.join(__dirname, '..', 'public');
const favicon = path.join(base, 'static', 'images', 'favicon.png');
// 常量
const company = 'company';
module.exports = {
// 入口文件
entry: {
company: ['babel-polyfill', path.join(base, company, 'index.js')]
},
// 抽取公共JS
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'common',
priority: 10,
chunks: 'all'
}
}
}
},
output: {
// 打包后文件路径
path: path.join(dist),
// 打包后输出文件
filename: 'bundle.[name].[hash:8].js'
},
// 发布时设置为null
devtool: 'null',
performance: {
hints: false
},
module: {
rules: [
{
test: /(\.jsx|\.js)$/,
use: {
loader: 'babel-loader'
},
exclude: /node_modules/
},
{
test: /\.html$/,
use: {
// 压缩HTML设置true
loader: 'html-loader?minimize=false'
}
},
{
test: /\.(png|jpe?g|gif|svg)$/,
use: {
loader: 'url-loader?limit=1024&name=images/[hash:12].[ext]'
}
},
{
test: /\.css$/,
use: [
{
loader: 'style-loader'
},
{
// 启用CSS模块
loader: 'css-loader',
options: {
module: true
}
},
{
// CSS类自动名称
loader: 'postcss-loader',
options: {
plugins: [
autoprefixer
]
}
}
]
}
]
},
plugins: [
new webpack.BannerPlugin('DEMO COPYRIGHT'),
new HtmlWebpackPlugin({
chunks: ['common', company],
template: path.join(base, company, 'index.tmpl.html'),
filename: 'index.html',
favicon: favicon
}),
// 热加载模块插件
new webpack.HotModuleReplacementPlugin(),
// 为组建分配ID
new webpack.optimize.OccurrenceOrderPlugin(),
// 压缩JS
new UglifyJsPlugin({
uglifyOptions: {
compress: {
drop_console: true
}
}
}),
// 分离CSS[存在BUG]
new ExtractTextPlugin('[name].[hash:10].css'),
// 清除文件
new CleanWebpackPlugin(['*'], {
root: path.join(dist)
}),
// ZIP打包
new ZipPlugin({
path: path.join(dist),
filename: 'Release-' + moment().format('YYHHmmss') + '.zip'
})
]
}
common.css
/*!
* Hon by 2018-05-02
*/
body {
color: #526475;
margin: 0px;
padding: 0px;
font-family: Monospaced Number, Chinese Quote, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif;
font-size: 16px;
font-weight: 300;
width: 100%;
background-color: #ffffff;
}
h1, h2, h3, h4, h5, h6 {
color: #526475;
font-weight: 300;
display: block;
margin-bottom: 20px;
margin-top: 0px;
white-space: nowrap;
}
h1 {
font-size: 36px;
line-height: 50px;
}
h2 {
font-size: 32px;
line-height: 46px;
}
h3 {
font-size: 28px;
line-height: 42px;
}
h4 {
font-size: 24px;
line-height: 38px;
}
h5 {
font-size: 20px;
line-height: 34px;
}
h6 {
font-size: 16px;
line-height: 30px;
}
.btn {
font-family: 'Open Sans';
font-size: 16px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
text-align: center;
text-decoration: none !important;
line-height: 36px;
margin: 5px;
padding: 0 20px;
display: inline-block;
border-radius: 3px;
transition: all 0.3s;
color: #ffffff;
border: 1px solid #09a0f6;
white-space: nowrap;
background-color: #09a0f6;
outline: 0px;
cursor: pointer;
}
.btn:hover {
text-decoration: none;
opacity: 0.8;
}
.btn:active {
background-color: #0077e6;
border-color: #0077e6;
opacity:.8;
-webkit-animation: buttonEffect .4s;
animation: buttonEffect .4s;
}
.disable {
font-family: 'Open Sans';
font-size: 14px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
margin: 5px;
border-radius: 3px;
transition: all 0.3s;
color: #777777;
background-color: #f7f7f7;
white-space: nowrap;
border: 1px solid #d9d9d9;
outline: 0px;
cursor: not-allowed;
}
.btn-small {
font-size: 14px !important;
line-height: 26px !important;
padding: 0 12px !important;
}
.btn-clean {
margin: 0px;
}
.form-input[type="text"], .form-input[type="password"], .form-input[type="number"], .form-input[type="email"] {
font-size: 16px;
display: inline-block;
width: 100%;
transition: all 0.3s;
color: #526475;
padding-left: 10px;
padding-right: 10px;
border: 1px solid #d1e1e8;
border-radius: 3px;
outline: 0px;
box-sizing: border-box;
height: 38px;
}
.form-input[type="text"]:focus, .form-input[type="password"]:focus, .form-input[type="number"]:focus, .form-input[type="email"]:focus {
border: 1px solid #09a0f6;
}
.form-input[type="date"] {
font-size: 16px;
display: inline-block;
width: 100%;
transition: all 0.3s;
color: #526475;
padding: 10px;
border: 1px solid #d1e1e8;
border-radius: 5px;
outline: 0px;
box-sizing: border-box;
width: auto !important;
height: 40px;
}
.form-input[type="date"]:focus {
border: 1px solid #09a0f6;
}
.form-input[disabled] {
font-size: 16px;
display: inline-block;
width: 100%;
transition: all 0.3s;
color: #526475;
padding: 10px;
border: 1px solid #d1e1e8;
border-radius: 5px;
outline: 0px;
box-sizing: border-box;
cursor: not-allowed;
background-color: #d1e1e8;
height: 40px;
}
.form-input[disabled]:focus {
border: 1px solid #09a0f6;
}
.form-input[type="submit"], .form-input[type="button"] {
font-size: 16px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
outline: none;
text-align: center;
text-decoration: none !important;
line-height: 28px;
margin-left: 5px;
margin-right: 5px;
margin: 5px;
padding: 5px 25px;
display: inline-block;
cursor: pointer;
border-radius: 3px;
transition: all 0.3s;
color: #ffffff;
background-color: #09a0f6;
border: 0px;
}
.form-input[type="submit"]:hover, .form-input[type="button"]:hover {
text-decoration: none;
}
.form-input[type="submit"]:hover, .form-input[type="button"]:hover {
opacity: 0.8;
}
.form-select {
font-size: 16px;
display: inline-block;
width: 100%;
transition: all 0.3s;
color: #526475;
padding: 10px;
margin: 5px;
border: 1px solid #d1e1e8;
border-radius: 5px;
outline: 0px;
box-sizing: border-box;
padding-top: 6px;
height: 40px;
background-color: #ffffff;
}
.form-select:focus {
border: 1px solid #09a0f6;
}
.form-textarea {
font-size: 16px;
display: inline-block;
width: 100%;
transition: all 0.3s;
color: #526475;
padding: 10px;
margin: 5px;
border: 1px solid #d1e1e8;
border-radius: 5px;
outline: 0px;
box-sizing: border-box;
resize: vertical;
}
.form-textarea:focus {
border: 1px solid #09a0f6;
}
@media (max-width: 960px) {
.grid {
width: 94%;
}
}
.row {
display: inline-block;
width: 100%;
margin: 10px 0px;
}
.row:after {
content: " ";
clear: both;
display: table;
line-height: 0;
}
.col-1 {
width: 6.33%;
display: inline-block;
vertical-align: top;
float: left;
padding: 1%;
}
.col-2 {
width: 14.66%;
display: inline-block;
vertical-align: top;
float: left;
padding: 1%;
}
.col-3 {
width: 22.99%;
display: inline-block;
vertical-align: top;
float: left;
padding: 1%;
}
.col-4 {
width: 31.33%;
display: inline-block;
vertical-align: top;
float: left;
padding: 1%;
white-space: nowrap;
}
.col-5 {
width: 39.66%;
display: inline-block;
vertical-align: top;
float: left;
padding: 1%;
}
.col-6 {
width: 47.99%;
display: inline-block;
vertical-align: top;
float: left;
padding: 1%;
}
.col-7 {
width: 56.33%;
display: inline-block;
vertical-align: top;
float: left;
padding: 1%;
}
.col-8 {
width: 64.66%;
display: inline-block;
vertical-align: top;
float: left;
padding: 1%;
}
.col-9 {
width: 72.99%;
display: inline-block;
vertical-align: top;
float: left;
padding: 1%;
}
.col-10 {
width: 81.33%;
display: inline-block;
vertical-align: top;
float: left;
padding: 1%;
}
.col-11 {
width: 89.66%;
display: inline-block;
vertical-align: top;
float: left;
padding: 1%;
}
.col-12 {
width: 97.99%;
display: inline-block;
vertical-align: top;
float: left;
padding: 1%;
}
@media (max-width: 400px) {
.col-1 {
width: 98%;
}
.col-2 {
width: 98%;
}
.col-3 {
width: 98%;
}
.col-4 {
width: 98%;
}
.col-5 {
width: 98%;
}
.col-6 {
width: 98%;
}
.col-7 {
width: 98%;
}
.col-8 {
width: 98%;
}
.col-9 {
width: 98%;
}
.col-10 {
width: 98%;
}
.col-11 {
width: 98%;
}
.col-12 {
width: 98%;
}
}
.table {
display: table;
width: 100%;
border-width: 0px;
border-collapse: collapse;
color: #526475;
margin-top: 0px;
margin-bottom: 20px;
}
.table thead tr th {
font-weight: 500;
border: 1px solid #d1e1e8;
padding: 8px 12px;
background-color: #fcfcfc;
border-left: none;
border-right: none;
white-space: nowrap;
text-align: left;
}
.table tr td {
border: 1px solid #d1e1e8;
border-left: none;
border-right: none;
padding: 10px;
white-space: nowrap;
}
.center {
text-align: center;
}
.alert {
display: block;
font-size: 16px;
text-align: left;
padding: 6px 10px;
margin-top: 5px;
border-radius: 2px;
border: 1px solid;
background-color: #E1F5FE;
color: #03A9F4;
border-color: #03A9F4;
}
.alert a {
text-decoration: none;
font-weight: normal;
}
.alert-error {
color: #D32F2F;
background-color: #FFEBEE;
border-color: #FFEBEE;
}
.alert-warning {
background-color: #FFF8E1;
color: #FF8F00;
border-color: #FFC107;
}
.alert-done {
background-color: #E8F5E9;
color: #388E3C;
border-color: #4CAF50;
}
.logo {
background-image: url("../images/180403.png");
background-size: 35px 35px;
background-repeat: no-repeat;
width: 35px;
height: 35px;
display: inline-block;
margin-right: 8px;
margin-bottom: -5px;
overflow: hidden;
}
.footer {
font-size: 12px;
color: #999999;
text-align: center;
line-height: 50px;
height: 50px;
margin: 0px;
overflow: hidden;
position: relative;
}
.footer a {
color: #777777;
text-decoration: none;
}
.footer a:hover {
color: #f54343;
}
.block {
margin: 20px auto;
width: 350px;
padding: 20px 0px;
border: 1px solid #cccccc;
box-shadow: 5px 5px 3px #cccccc;
}
.block .having {
font-size: 20px;
color: #f54343;
font-weight: bold;
padding-right: 10px;
}
.bood {
background-color: #20232a;
width: 56px;
height: 56px;
border-radius: 50%;
display: inline-block;
float: left;
margin-top: -10px;
margin-left: -26px;
}
.gap {
padding-left: 45px;
}
index.tmpl.html
REDUX COMPY
index.js
// 入口
import React from 'react';
import {render} from 'react-dom';
import {createStore} from 'redux';
import {Provider} from 'react-redux';
import Index from './component/index';
import reducer from './reducers';
const store = createStore(reducer);
render(
,
document.querySelector('#root')
);
index.js
import React from 'react';
import style from './style';
import Title from './Title';
import CreateItem from '../container/CreateItem';
import VisibleItemList from '../container/VisibleItemList';
import RowLink from './RowLink';
import Footer from './Footer';
const title = 'COMPANY MANAGEMENT';
// 组装UI组件
const Index = () => (
);
export default Index;
style.js
const style = require('../../static/css/common.css');
// CSS模块
export default style;
Create.js
import React from 'react';
import style from './style';
import Error from './Error';
// 添加组件
const Create = ({createError, addItem, resetCreate}) => {
let input;
return (
)
}
export default Create;
Error.js
import React from 'react';
import style from './style';
// 错误提示
const Error = ({error}) => {
if (error) {
return (
{error}
)
}
return (
)
}
export default Error;
Footer.js
import React from 'react';
import style from './style';
// 页脚组件
const Footer = () => (
@2018 XXX 版权所有 京A2-20186XXX号
);
export default Footer;
Header.js
import React from 'react';
// 表头组件
const Header = () => (
名称 NAME
操作 OPERATION
);
export default Header;
Item.js
import React from 'react';
import style from './style';
import Error from './Error';
import {connect} from 'react-redux';
import {saveItem} from '../actions/items';
const Item = ({toggleItem, editItem, removeItem, cancelEdit, dispatch, ...item}) => {
if (item.isEditing) {
let editInput;
return (
editInput = node} autoFocus="autofocus"
/>
);
}
let itemStyle = {
color: item.isCompleted ? 'green' : 'red',
textDecoration: item.isCompleted ? 'line-through' : 'none',
cursor: 'pointer'
}
return (
{item.text}
);
};
export default connect()(Item);
ItemList.js
import React from 'react';
import style from './style';
import Header from './Header';
import Item from './Item';
// 列表组件
const ItemList = ({data, toggleItem, editItem, removeItem, cancelEdit}) => (
{
data.items.map(item => (
- toggleItem(item.id)}
editItem={() => editItem(item.id)}
removeItem={() => removeItem(item.id)}
cancelEdit={() => cancelEdit(item.id)}
/>
))
}
);
export default ItemList;
Link.js
import React from 'react';
import style from './style';
// UI - 三个参数[是否激活,按钮内容,点击事件]
const Link = ({active, children, onClick}) => {
if (active) {
return (
)
}
return (
)
}
export default Link;
RowLink.js
import React from 'react';
import FilterLink from '../container/FilterLink';
// UI
const RowLink = () => (
);
export default RowLink;
Title.js
import React from 'react';
import style from './style';
// 标题组件
const Title = ({title}) => (
{title}
);
export default Title;
CreateItem.js
import {connect} from 'react-redux';
import {addItem, resetCreate} from '../actions/items';
import Create from '../component/Create';
// 定义输入逻辑 - 将state映射到UI组件的参数
const mapStateToProps = (state) => {
return {
createError: state.data.createError
}
};
// 定义输出逻辑 - UI操作到dispatch的映射
const mapDispatchToProps = dispatch => {
return {
addItem: (text) => {
// 触发Action
dispatch(addItem(text));
},
resetCreate: () => {
dispatch(resetCreate());
}
}
};
// 从UI组件生成容器组件
const CreateItem = connect(
// 不需要映射参数[null或() => ({})]
mapStateToProps,
mapDispatchToProps
)(Create);
export default CreateItem;
FilterLink.js
import {connect} from 'react-redux';
import {visible} from '../actions/visible';
import Link from '../component/Link';
// 定义输入逻辑 - 将state映射到UI组件的参数
const mapStateToProps = (state, props) => {
return {
active: props.filter === state.filter
}
};
// 定义输出逻辑 - UI操作到dispatch的映射
const mapDispatchToProps = (dispatch, props) => {
return {
onClick: () => {
dispatch(visible(props.filter));
}
}
};
// 从UI组件生成容器组件
const FilterLink = connect(
mapStateToProps,
mapDispatchToProps
)(Link);
export default FilterLink;
VisibleItemList.js
import {connect} from 'react-redux';
import {toggleItem, editItem, removeItem, cancelEdit} from '../actions/items';
import ItemList from '../component/ItemList';
// 传入状态[当前数据,当前过滤值]
const getVisibleItems = (data, filter) => {
switch (filter) {
case 'SHOW_COMPLETED':
return {
items: data.items.filter(t => t.isCompleted)
}
case 'SHOW_ACTIVE':
return {
items: data.items.filter(t => !t.isCompleted)
}
case 'SHOW_ALL':
default:
return data
}
};
// 定义输入逻辑 - 将state映射到UI组件的参数
const mapStateToProps = state => {
return {
data: getVisibleItems(state.data, state.filter)
}
};
// 定义输出逻辑 - UI操作到dispatch的映射
const mapDispatchToProps = dispatch => {
return {
toggleItem: id => {
// 触发Action
dispatch(toggleItem(id))
},
editItem: id => {
dispatch(editItem(id))
},
removeItem: id => {
dispatch(removeItem(id))
},
cancelEdit: id => {
dispatch(cancelEdit(id))
}
}
};
// 从UI组件生成容器组件
const VisibleItemList = connect(
mapStateToProps,
mapDispatchToProps
)(ItemList);
export default VisibleItemList;
actions - items.js
export const addItem = text => ({
type: 'ADD_ITEM',
id: new Date().getTime(),
text
});
export const toggleItem = id => ({
type: 'TOGGLE_ITEM',
id
});
export const removeItem = id => ({
type: 'REMOVE_ITEM',
id
});
export const editItem = id => ({
type: 'EDIT_ITEM',
id
});
export const saveItem = item => ({
type: 'SAVA_ITEM',
item
});
export const cancelEdit = id => ({
type: 'CANCEL_EDIT',
id
});
export const resetCreate = () => ({
type: 'RESET_CREATE'
});
actions - visible.js
// Action Creator
export const visible = filter => ({
type: 'SET_VISIBILITY_FILTER',
filter
});
reducers - filter.js
// 把state和action串起来返回新的state
const filter = (state = 'SHOW_ALL', action) => {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter;
default:
return state;
}
}
export default filter;
reducers - items.js
import _ from 'lodash';
// 初始化数据
const def = {
items: [
{
id: new Date().getTime(),
text: "ASKE(北京)信息技术有限公司",
isCompleted: false,
isEditing: false
},
{
id: new Date().getHours(),
text: "SWSN(北京)网络科技有限公司",
isCompleted: true,
isEditing: false
},
{
id: new Date().getMonth(),
text: "SLMI(杭州)网络科技有限公司",
isCompleted: false,
isEditing: false
}
],
createError: ''
};
const items = (state = def, action) => { // state = {},
switch (action.type) {
case 'ADD_ITEM': {
// 非空检查
if (!action.text) {
return {
items: state.items,
createError: '请输入公司名称'
}
}
// 验证重复
let foundItem = _.find(state.items, item =>
(action.text === item.text)
);
if (foundItem) {
return {
items: state.items,
createError: '公司名称已存在'
}
}
// 将新加的数据与原数据合并
return {
items: [
...state.items,
{
id: action.id,
text: action.text,
isCompleted: false,
isEditing: false
}
],
defaultValue: '',
createError: ''
}
}
case 'TOGGLE_ITEM':
// 切换状态数据
return {
items: state.items.map(item =>
(item.id === action.id) ? {
...item, isCompleted: !item.isCompleted
} : item
),
createError: ''
}
case 'REMOVE_ITEM':
// 删除数据[根据ID]
return {
items: _.remove(state.items, item => item.id !== action.id),
createError: ''
}
case 'EDIT_ITEM':
// 编辑数据
return {
items: state.items.map(item =>
(item.id === action.id) ? {...item, isEditing: true} : item
),
createError: ''
}
case 'SAVA_ITEM': {
// 非空检查
if (!action.item.text) {
return {
items: state.items.map(item =>
(item.id === action.item.id) ? {
...item, error: '请输入公司名称'
} : item
),
createError: ''
}
}
// 验证重复
let foundItem = _.find(state.items, item =>
(action.item.text === item.text && action.item.id !== item.id)
);
if (foundItem) {
return {
items: state.items.map(item =>
(item.id === action.item.id) ? {
...item, error: '公司名称已存在'
} : item
),
createError: ''
}
}
// 修改数据
return {
items: state.items.map(item =>
(item.id === action.item.id) ? {
...item, text: action.item.text, isEditing: false, error: null
} : item
),
createError: ''
}
}
case 'CANCEL_EDIT':
// 取消编辑
return {
items: state.items.map(item =>
(item.id === action.id) ? {
...item, isEditing: false, error: null
} : item
),
createError: ''
}
case 'RESET_CREATE':
// 重置添加
return {
items: state.items,
createError: ''
}
default:
return state;
}
}
export default items;
reducers - index.js
import {combineReducers} from 'redux';
import items from './items';
import filter from './filter';
// 生成一个整体的Reducer函数[状态 - Reducer]
export default combineReducers({
data: items,
filter: filter
});
运行:
npm run deves
备注:
代码可精简合并,仅供学习参考。