概述:css
基於React、Redux,參考官方示例,實現組件狀態管理。
圖示:html
文件目錄:node
│ .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.jsonreact
{ "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" } }
.babelrcwebpack
{ 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.jsgit
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.jsweb
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.cssnpm
/*! * 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; }
imagesjson
index.tmpl.htmlredux
<!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, 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
結果:
備註:
代碼可精簡合併,僅供學習參考。