概述:
基于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,'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,// 发布时设置为null devtool: 'null',use: { // 压缩HTML设置true loader: 'html-loader?minimize=false' } },// 热加载模块插件 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; }
images
index.tmpl.html
<!DOCTYPE html> <html> <head> <Meta charset="UTF-8"> <title>REDUX COMPY</title> </head> <body> <div id="root"></div> </body> </html>
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( <Provider store={store}> <Index/> </Provider>,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 = () => ( <div className={style.row}> <div className={style["col-3"]}></div> <div className={style["col-6"]}> <Title title={title}/> <CreateItem/> <VisibleItemList/> <RowLink/> </div> <div className={style["col-3"]}></div> <div className={style["col-12"]}> <Footer/> </div> </div> ); 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 ( <div className={style.row}> <form onSubmit={(e) => { e.preventDefault(); input.focus(); addItem(input.value.trim()); }}> <div className={style["col-8"]}> <input type={'text'} className={style["form-input"]} placeholder="请输入公司名称" ref={node => { input = node }} /> <Error error={createError}/> </div> <div className={style["col-4"]} style={{marginTop: '-5px'}}> <button type={'submit'} className={`${style.btn} ${style["btn-clean"]}`}> 添加 </button> <button type={'button'} className={`${style.btn}`} onClick={(e) => { input.value = ''; resetCreate(); }} >重置 </button> </div> </form> </div> ) } export default Create;
Error.js
import React from 'react'; import style from './style'; // 错误提示 const Error = ({error}) => { if (error) { return ( <div className={`${style.alert} ${style["alert-error"]}`}> {error} </div> ) } return ( <span></span> ) } export default Error;
Footer.js
import React from 'react'; import style from './style'; // 页脚组件 const Footer = () => ( <div> <div className={style.footer}>@2018 <a href="/">XXX</a> 版权所有 京A2-20186XXX号</div> </div> ); export default Footer;
Header.js
import React from 'react'; // 表头组件 const Header = () => ( <thead> <tr> <th>名称 NAME</th> <th style={{textAlign: 'center'}}>操作 OPERATION</th> </tr> </thead> ); 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 ( <tr> <td> <input className={style["form-input"]} type="text" defaultValue={item.text} ref={node => editInput = node} autoFocus="autofocus" /> <Error error={item.error}/> </td> <td className={style.center}> <button className={`${style.btn} ${style["btn-small"]}`} onClick={(e) => { e.preventDefault(); editInput.focus(); const it = Object.assign({},{...item},{text: editInput.value.trim()}); // 调用 dispatch dispatch(saveItem(it)); }} >保存 </button> <button className={`${style.btn} ${style["btn-small"]}`} onClick={(e) => { e.preventDefault(); cancelEdit(item.id); }} >取消 </button> </td> </tr> ); } let itemStyle = { color: item.isCompleted ? 'green' : 'red',textDecoration: item.isCompleted ? 'line-through' : 'none',cursor: 'pointer' } return ( <tr> <td onClick={toggleItem} style={itemStyle}> {item.text} </td> <td className={style.center}> <button className={`${style.btn} ${style["btn-small"]}`} onClick={(e) => { e.preventDefault(); editItem(item.id); }} >编辑 </button> <button className={`${style.btn} ${style["btn-small"]}`} onClick={(e) => { e.preventDefault(); removeItem(item.id); }} >删除 </button> </td> </tr> ); }; 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,cancelEdit}) => ( <table className={style.table}> <Header/> <tbody> { data.items.map(item => ( <Item key={item.id} {...item} toggleItem={() => toggleItem(item.id)} editItem={() => editItem(item.id)} removeItem={() => removeItem(item.id)} cancelEdit={() => cancelEdit(item.id)} /> )) } </tbody> </table> ); export default ItemList;
Link.js
import React from 'react'; import style from './style'; // UI - 三个参数[是否激活,按钮内容,点击事件] const Link = ({active,children,onClick}) => { if (active) { return ( <button className={`${style.disable} ${style["btn-small"]}`}> {children} </button> ) } return ( <button className={`${style.btn} ${style["btn-small"]}`} onClick={e => { e.preventDefault(); onClick(); }}> {children} </button> ) } export default Link;
RowLink.js
import React from 'react'; import FilterLink from '../container/FilterLink'; // UI const RowLink = () => ( <div> <span style={{marginLeft: '5px'}}></span> <FilterLink filter="SHOW_ALL"> 全部 </FilterLink> <FilterLink filter="SHOW_ACTIVE"> 激活 </FilterLink> <FilterLink filter="SHOW_COMPLETED"> 完成 </FilterLink> <a href={'counter.html'} style={{textDecoration: 'none',fontSize: '14px',marginLeft: '30px',whiteSpace: 'nowrap',color: '#8B668B'}}> 计数器 </a> </div> ); export default RowLink;
Title.js
import React from 'react'; import style from './style'; // 标题组件 const Title = ({title}) => ( <h2><span className={style.logo}></span>{title}</h2> ); 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,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,{ id: new Date().getMonth(),text: "SLMI(杭州)网络科技有限公司",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,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,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
结果:
备注:
代码可精简合并,仅供学习参考。