React-Redux类似任务管理示例

前端之家收集整理的这篇文章主要介绍了React-Redux类似任务管理示例前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

概述:

基于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&nbsp;<a href="/">XXX</a>&nbsp;版权所有&nbsp;京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

结果:

备注:

代码可精简合并,仅供学习参考。

猜你在找的React相关文章