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, 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;
}

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, editItem, removeItem, 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, 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

结果:
图片描述

图片描述

备注:

代码可精简合并,仅供学习参考。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值