├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── bin └── react-scripts ├── config ├── env.js ├── jest │ ├── babelTransform.js │ ├── cssTransform.js │ └── fileTransform.js ├── paths.js ├── polyfills.js ├── webpack.config.dev.js ├── webpack.config.prod.js └── webpackDevServer.config.js ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── pxq ├── asset-manifest.json ├── favicon.ico ├── index.html ├── manifest.json ├── service-worker.js └── static │ ├── css │ └── main.fc2ff5ec.css │ ├── js │ ├── 0.c353ab85.chunk.js │ ├── 1.af913d75.chunk.js │ ├── 2.a96eb65f.chunk.js │ ├── 3.ff2875c3.chunk.js │ └── main.83dc4a4d.js │ └── media │ ├── iconfont.6924d946.svg │ ├── iconfont.7e008a77.eot │ └── iconfont.d828102a.ttf ├── screenshot ├── all_redux.png ├── demo1.png ├── diff.png ├── icon_class.png ├── react-lifecycle.png ├── react_props.png └── simple_redux.jpg ├── scripts ├── build.js ├── eject.js ├── init.js ├── start.js ├── test.js └── utils │ └── createJestConfig.js └── src ├── api ├── api.js └── server.js ├── assets └── iconfonts │ ├── iconfont.css │ ├── iconfont.eot │ ├── iconfont.svg │ ├── iconfont.ttf │ └── iconfont.woff ├── components ├── TouchableOpacity │ ├── TouchableOpacity.jsx │ └── TouchableOpacity.less ├── alert │ ├── alert.jsx │ └── alert.less └── header │ ├── header.jsx │ └── header.less ├── envconfig └── envconfig.js ├── index.js ├── pages ├── balance │ ├── balance.jsx │ └── balance.less ├── helpcenter │ ├── helpcenter.jsx │ └── helpcenter.less ├── home │ ├── home.jsx │ └── home.less ├── production │ ├── production.jsx │ └── production.less └── record │ ├── components │ ├── recordList.jsx │ └── recordList.less │ ├── record.jsx │ └── record.less ├── registerServiceWorker.js ├── router └── index.js ├── store ├── home │ ├── action-type.js │ ├── action.js │ └── reducer.js ├── production │ ├── action-type.js │ ├── action.js │ └── reducer.js └── store.js ├── style ├── base.css └── mixin.less └── utils ├── asyncComponent.jsx ├── mixin.js └── setRem.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react"], 3 | "plugins": ["syntax-dynamic-import"] 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | *.idea 19 | *.iml 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | package-lock.json 25 | yarn.lock -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 cangdu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## 技术栈: 3 | react + redux + webpack + react-router + ES6/7/8 + immutable 4 | 5 | 6 | ## 运行项目(nodejs 6.0+) 7 | ``` 8 | git clone https://github.com/bailicangdu/react-pxq.git 9 | 10 | cd react-pxq 11 | 12 | npm i 或者运行 yarn(推荐) 13 | 14 | npm start 15 | 16 | npm run build (发布) 17 | ``` 18 | 19 | 20 | ## 说明 21 | 22 | > 本项目主要用于理解 react 和 redux 的编译方式,以及 react + redux 之间的配合方式 23 | 24 | > 如果觉得不错的话,您可以点右上角 "Star" 支持一下 谢谢! ^_^ 25 | 26 | > 或者您可以 "follow" 一下,我会不断开源更多的有趣的项目 27 | 28 | > 如有问题请直接在 Issues 中提,或者您发现问题并有非常好的解决方案,欢迎 PR 👍 29 | 30 | > 开发环境 macOS 10.13.1 Chrome 63 nodejs 8.9.1 31 | 32 | >  推荐一个 vue2 + vuex 构建的 45 个页面的大型开源项目。[地址在这里](https://github.com/bailicangdu/vue2-elm) 33 | 34 | > 另外一个 vue2 + vuex 的简单项目,非常适合入门练习。[地址在这里](https://github.com/bailicangdu/vue2-happyfri) 35 | 36 | 37 | ## 演示 38 | 39 | [查看演示效果](https://cangdu.org/pxq/)(请用chrome的手机模式预览) 40 | 41 | ### 移动端扫描下方二维码 42 | 43 | 44 | 45 | 46 | 47 | # 个人感悟 48 | 49 | ## 做React需要会什么? 50 | react的功能其实很单一,主要负责渲染的功能,现有的框架,比如angular是一个大而全的框架,用了angular几乎就不需要用其他工具辅助配合,但是react不一样,他只负责ui渲染,想要做好一个项目,往往需要其他库和工具的配合,比如用redux来管理数据,react-router管理路由,react已经全面拥抱es6,所以es6也得掌握,webpack就算是不会配置也要会用,要想提高性能,需要按需加载,immutable.js也得用上,还有单元测试。。。。 51 | 52 | 53 | ## React 是什么 54 | 用脚本进行DOM操作的代价很昂贵。有个贴切的比喻,把DOM和JavaScript各自想象为一个岛屿,它们之间用收费桥梁连接,js每次访问DOM,都要途径这座桥,并交纳“过桥费”,访问DOM的次数越多,费用也就越高。 因此,推荐的做法是尽量减少过桥的次数,努力待在ECMAScript岛上。因为这个原因react的虚拟dom就显得难能可贵了,它创造了虚拟dom并且将它们储存起来,每当状态发生变化的时候就会创造新的虚拟节点和以前的进行对比,让变化的部分进行渲染。整个过程没有对dom进行获取和操作,只有一个渲染的过程,所以react说是一个ui框架。 55 | 56 | 57 | ## React的组件化 58 | 59 | react的一个组件很明显的由dom视图和state数据组成,两个部分泾渭分明。state是数据中心,它的状态决定着视图的状态。这时候发现似乎和我们一直推崇的MVC开发模式有点区别,没了Controller控制器,那用户交互怎么处理,数据变化谁来管理?然而这并不是react所要关心的事情,它只负责ui的渲染。与其他框架监听数据动态改变dom不同,react采用setState来控制视图的更新。setState会自动调用render函数,触发视图的重新渲染,如果仅仅只是state数据的变化而没有调用setState,并不会触发更新。 组件就是拥有独立功能的视图模块,许多小的组件组成一个大的组件,整个页面就是由一个个组件组合而成。它的好处是利于重复利用和维护。 60 | 61 | 62 | ## React的 Diff算法 63 | react的diff算法用在什么地方呢?当组件更新的时候,react会创建一个新的虚拟dom树并且会和之前储存的dom树进行比较,这个比较多过程就用到了diff算法,所以组件初始化的时候是用不到的。react提出了一种假设,相同的节点具有类似的结构,而不同的节点具有不同的结构。在这种假设之上进行逐层的比较,如果发现对应的节点是不同的,那就直接删除旧的节点以及它所包含的所有子节点然后替换成新的节点。如果是相同的节点,则只进行属性的更改。 64 | 65 | 对于列表的diff算法稍有不同,因为列表通常具有相同的结构,在对列表节点进行删除,插入,排序的时候,单个节点的整体操作远比一个个对比一个个替换要好得多,所以在创建列表的时候需要设置key值,这样react才能分清谁是谁。当然不写key值也可以,但这样通常会报出警告,通知我们加上key值以提高react的性能。 66 | 67 | ![](https://github.com/bailicangdu/pxq/blob/master/screenshot/diff.png) 68 | 69 | 70 | 71 | 72 | ## React组件是怎么来的 73 | 74 | 组件的创造方法为React.createClass() ——创造一个类,react系统内部设计了一套类系统,利用它来创造react组件。但这并不是必须的,我们还可以用es6的class类来创造组件,这也是Facebook官方推荐的写法。 75 | 76 | ![](https://github.com/bailicangdu/pxq/blob/master/screenshot/icon_class.png) 77 | 78 | 这两种写法实现的功能一样但是原理却是不同,es6的class类可以看作是构造函数的一个语法糖,可以把它当成构造函数来看,extends实现了类之间的继承 —— 定义一个类Main 继承React.Component所有的属性和方法,组件的生命周期函数就是从这来的。constructor是构造器,在实例化对象时调用,super调用了父类的constructor创造了父类的实例对象this,然后用子类的构造函数进行修改。这和es5的原型继承是不同的,原型继承是先创造一个实例化对象this,然后再继承父级的原型方法。了解了这些之后我们在看组件的时候就清楚很多。 79 | 80 | 当我们使用组件< Main />时,其实是对Main类的实例化——new Main,只不过react对这个过程进行了封装,让它看起来更像是一个标签。 81 | 82 | 有三点值得注意:1、定义类名字的首字母必须大写 2、因为class变成了关键字,类选择器需要用className来代替。 3、类和模块内部默认使用严格模式,所以不需要用use strict指定运行模式。 83 | 84 | 85 | 86 | 87 | ## 组件的生命周期 88 | 89 | ![](https://github.com/bailicangdu/pxq/blob/master/screenshot/react-lifecycle.png) 90 | 91 | **组件在初始化时会触发5个钩子函数:** 92 | 93 | **1、getDefaultProps()** 94 | > 设置默认的props,也可以用dufaultProps设置组件的默认属性。 95 | 96 | 97 | **2、getInitialState()** 98 | > 在使用es6的class语法时是没有这个钩子函数的,可以直接在constructor中定义this.state。此时可以访问this.props。 99 | 100 | 101 | **3、componentWillMount()** 102 | > 组件初始化时只调用,以后组件更新不调用,整个生命周期只调用一次,此时可以修改state。 103 | 104 | 105 | **4、 render()** 106 | > react最重要的步骤,创建虚拟dom,进行diff算法,更新dom树都在此进行。此时就不能更改state了。 107 | 108 | 109 | **5、componentDidMount()** 110 | > 组件渲染之后调用,可以通过this.getDOMNode()获取和操作dom节点,只调用一次。 111 | 112 | 113 | **在更新时也会触发5个钩子函数:** 114 | 115 | **6、componentWillReceivePorps(nextProps)** 116 | > 组件初始化时不调用,组件接受新的props时调用。 117 | 118 | 119 | **7、shouldComponentUpdate(nextProps, nextState)** 120 | > react性能优化非常重要的一环。组件接受新的state或者props时调用,我们可以设置在此对比前后两个props和state是否相同,如果相同则返回false阻止更新,因为相同的属性状态一定会生成相同的dom树,这样就不需要创造新的dom树和旧的dom树进行diff算法对比,节省大量性能,尤其是在dom结构复杂的时候。不过调用this.forceUpdate会跳过此步骤。 121 | 122 | 123 | **8、componentWillUpdate(nextProps, nextState)** 124 | > 组件初始化时不调用,只有在组件将要更新时才调用,此时可以修改state 125 | 126 | 127 | **9、render()** 128 | > 不多说 129 | 130 | 131 | **10、componentDidUpdate()** 132 | > 组件初始化时不调用,组件更新完成后调用,此时可以获取dom节点。 133 | 134 | 135 | 还有一个卸载钩子函数 136 | 137 | **11、componentWillUnmount()** 138 | > 组件将要卸载时调用,一些事件监听和定时器需要在此时清除。 139 | 140 | 141 | 以上可以看出来react总共有10个周期函数(render重复一次),这个10个函数可以满足我们所有对组件操作的需求,利用的好可以提高开发效率和组件性能。 142 | 143 | 144 | ## React-Router路由 145 | 146 | Router就是React的一个组件,它并不会被渲染,只是一个创建内部路由规则的配置对象,根据匹配的路由地址展现相应的组件。Route则对路由地址和组件进行绑定,Route具有嵌套功能,表示路由地址的包涵关系,这和组件之间的嵌套并没有直接联系。Route可以向绑定的组件传递7个属性:children,history,location,params,route,routeParams,routes,每个属性都包涵路由的相关的信息。比较常用的有children(以路由的包涵关系为区分的组件),location(包括地址,参数,地址切换方式,key值,hash值)。react-router提供Link标签,这只是对a标签的封装,值得注意的是,点击链接进行的跳转并不是默认的方式,react-router阻止了a标签的默认行为并用pushState进行hash值的转变。切换页面的过程是在点击Link标签或者后退前进按钮时,会先发生url地址的转变,Router监听到地址的改变根据Route的path属性匹配到对应的组件,将state值改成对应的组件并调用setState触发render函数重新渲染dom。 147 | 148 | 当页面比较多时,项目就会变得越来越大,尤其对于单页面应用来说,初次渲染的速度就会很慢,这时候就需要按需加载,只有切换到页面的时候才去加载对应的js文件。react配合webpack进行按需加载的方法很简单,Route的component改为getComponent,组件用require.ensure的方式获取,并在webpack中配置chunkFilename。 149 | 150 | ```javascript 151 | const chooseProducts = (location, cb) => { 152 | require.ensure([], require => { 153 | cb(null, require('../Component/chooseProducts').default) 154 | },'chooseProducts') 155 | } 156 | 157 | const helpCenter = (location, cb) => { 158 | require.ensure([], require => { 159 | cb(null, require('../Component/helpCenter').default) 160 | },'helpCenter') 161 | } 162 | 163 | const saleRecord = (location, cb) => { 164 | require.ensure([], require => { 165 | cb(null, require('../Component/saleRecord').default) 166 | },'saleRecord') 167 | } 168 | 169 | const RouteConfig = ( 170 | 171 | 172 | //首页 173 | 174 | //帮助中心 175 | //销售记录 176 | 177 | 178 | 179 | ); 180 | 181 | ``` 182 | ## 组件之间的通信 183 | 184 | 185 | react推崇的是单向数据流,自上而下进行数据的传递,但是由下而上或者不在一条数据流上的组件之间的通信就会变的复杂。解决通信问题的方法很多,如果只是父子级关系,父级可以将一个回调函数当作属性传递给子级,子级可以直接调用函数从而和父级通信。 186 | 187 | 组件层级嵌套到比较深,可以使用上下文getChildContext来传递信息,这样在不需要将函数一层层往下传,任何一层的子级都可以通过this.context直接访问。 188 | 189 | 兄弟关系的组件之间无法直接通信,它们只能利用同一层的上级作为中转站。而如果兄弟组件都是最高层的组件,为了能够让它们进行通信,必须在它们外层再套一层组件,这个外层的组件起着保存数据,传递信息的作用,这其实就是redux所做的事情。 190 | 191 | 组件之间的信息还可以通过全局事件来传递。不同页面可以通过参数传递数据,下个页面可以用location.param来获取。其实react本身很简单,难的在于如何优雅高效的实现组件之间数据的交流。 192 | 193 | ## Redux 194 | 195 | 196 | 首先,redux并不是必须的,它的作用相当于在顶层组件之上又加了一个组件,作用是进行逻辑运算、储存数据和实现组件尤其是顶层组件的通信。如果组件之间的交流不多,逻辑不复杂,只是单纯的进行视图的渲染,这时候用回调,context就行,没必要用redux,用了反而影响开发速度。但是如果组件交流特别频繁,逻辑很复杂,那redux的优势就特别明显了。我第一次做react项目的时候并没有用redux,所有的逻辑都是在组件内部实现,当时为了实现一个逻辑比较复杂的购物车,洋洋洒洒居然写了800多行代码,回头一看我自己都不知道写的是啥,画面太感人。 197 | 198 | 先简单说一下redux和react是怎么配合的。react-redux提供了connect和Provider两个好基友,它们一个将组件与redux关联起来,一个将store传给组件。组件通过dispatch发出action,store根据action的type属性调用对应的reducer并传入state和这个action,reducer对state进行处理并返回一个新的state放入store,connect监听到store发生变化,调用setState更新组件,此时组件的props也就跟着变化。 199 | 200 | 201 | 202 | 203 | #### 流程是这个样子的: 204 | 205 | 206 | ![](https://github.com/bailicangdu/pxq/blob/master/screenshot/simple_redux.jpg) 207 | 208 | 值得注意的是connect,Provider,mapStateToProps,mapDispatchToProps是react-redux提供的,redux本身和react没有半毛钱关系,它只是数据处理中心,没有和react产生任何耦合,是react-redux让它们联系在一起。 209 | 210 | 211 | #### 接下来具体分析一下,redux以及react-redux到底是怎么实现的。 212 | 213 | 214 | #### 先上一张图 215 | 216 | ![](https://github.com/bailicangdu/pxq/blob/master/screenshot/all_redux.png) 217 | 218 | 明显比第一张要复杂,其实两张图说的是同一件事。从上而下慢慢分析: 219 | 220 | ### 先说说redux: 221 | 222 | #### redux主要由三部分组成:store,reducer,action。 223 | 224 | 225 | **store**是一个对象,它有四个主要的方法: 226 | 227 | **1、dispatch:** 228 | > 用于action的分发——在createStore中可以用middleware中间件对dispatch进行改造,比如当action传入dispatch会立即触发reducer,有些时候我们不希望它立即触发,而是等待异步操作完成之后再触发,这时候用redux-thunk对dispatch进行改造,以前只能传入一个对象,改造完成后可以传入一个函数,在这个函数里我们手动dispatch一个action对象,这个过程是可控的,就实现了异步。 229 | 230 | **2、subscribe:** 231 | > 监听state的变化——这个函数在store调用dispatch时会注册一个listener监听state变化,当我们需要知道state是否变化时可以调用,它返回一个函数,调用这个返回的函数可以注销监听。 232 | let unsubscribe = store.subscribe(() => {console.log('state发生了变化')}) 233 | 234 | **3、getState:** 235 | > 获取store中的state——当我们用action触发reducer改变了state时,需要再拿到新的state里的数据,毕竟数据才是我们想要的。getState主要在两个地方需要用到,一是在dispatch拿到action后store需要用它来获取state里的数据,并把这个数据传给reducer,这个过程是自动执行的,二是在我们利用subscribe监听到state发生变化后调用它来获取新的state数据,如果做到这一步,说明我们已经成功了。 236 | 237 | **4、replaceReducer:** 238 | > 替换reducer,改变state修改的逻辑。 239 | 240 | store可以通过createStore()方法创建,接受三个参数,经过combineReducers合并的reducer和state的初始状态以及改变dispatch的中间件,后两个参数并不是必须的。store的主要作用是将action和reducer联系起来并改变state。 241 | 242 | 243 | **action:** 244 | >action是一个对象,其中type属性是必须的,同时可以传入一些数据。action可以用actionCreactor进行创造。dispatch就是把action对象发送出去。 245 | 246 | **reducer:** 247 | >reducer是一个函数,它接受一个state和一个action,根据action的type返回一个新的state。根据业务逻辑可以分为很多个reducer,然后通过combineReducers将它们合并,state树中有很多对象,每个state对象对应一个reducer,state对象的名字可以在合并时定义。 248 | 249 | 像这个样子: 250 | ```javascript 251 | const reducer = combineReducers({ 252 | a: doSomethingWithA, 253 | b: processB, 254 | c: c 255 | }) 256 | ``` 257 | **combineReducers:** 258 | >其实它也是一个reducer,它接受整个state和一个action,然后将整个state拆分发送给对应的reducer进行处理,所有的reducer会收到相同的action,不过它们会根据action的type进行判断,有这个type就进行处理然后返回新的state,没有就返回默认值,然后这些分散的state又会整合在一起返回一个新的state树。 259 | 260 | 接下来分析一下整体的流程,首先调用store.dispatch将action作为参数传入,同时用getState获取当前的状态树state并注册subscribe的listener监听state变化,再调用combineReducers并将获取的state和action传入。combineReducers会将传入的state和action传给所有reducer,并根据action的type返回新的state,触发state树的更新,我们调用subscribe监听到state发生变化后用getState获取新的state数据。 261 | 262 | redux的state和react的state两者完全没有关系,除了名字一样。 263 | 264 | **上面分析了redux的主要功能,那么react-redux到底做了什么?** 265 | 266 | 267 | ## React-Redux 268 | 269 | 如果只使用redux,那么流程是这样的: 270 | > component --> dispatch(action) --> reducer --> subscribe --> getState --> component 271 | 272 | 用了react-redux之后流程是这样的: 273 | > component --> actionCreator(data) --> reducer --> component 274 | 275 | store的三大功能:dispatch,subscribe,getState都不需要手动来写了。react-redux帮我们做了这些,同时它提供了两个好基友Provider和connect。 276 | 277 | **Provider**是一个组件,它接受store作为props,然后通过context往下传,这样react中任何组件都可以通过context获取store。也就意味着我们可以在任何一个组件里利用dispatch(action)来触发reducer改变state,并用subscribe监听state的变化,然后用getState获取变化后的值。但是并不推荐这样做,它会让数据流变的混乱,过度的耦合也会影响组件的复用,维护起来也更麻烦。 278 | 279 | __connect --connect(mapStateToProps, mapDispatchToProps, mergeProps, options)__ 是一个函数,它接受四个参数并且再返回一个函数--wrapWithConnect,wrapWithConnect接受一个组件作为参数wrapWithConnect(component),它内部定义一个新组件Connect(容器组件)并将传入的组件(ui组件)作为Connect的子组件然后return出去。 280 | 281 | 所以它的完整写法是这样的:connect(mapStateToProps, mapDispatchToProps, mergeProps, options)(component) 282 | 283 | **mapStateToProps(state, [ownProps]):** 284 | >mapStateToProps 接受两个参数,store的state和自定义的props,并返回一个新的对象,这个对象会作为props的一部分传入ui组件。我们可以根据组件所需要的数据自定义返回一个对象。ownProps的变化也会触发mapStateToProps 285 | 286 | ```javascript 287 | function mapStateToProps(state) { 288 | return { todos: state.todos }; 289 | } 290 | ``` 291 | 292 | **mapDispatchToProps(dispatch, [ownProps]):** 293 | 294 | > mapDispatchToProps如果是对象,那么会和store绑定作为props的一部分传入ui组件。如果是个函数,它接受两个参数,bindActionCreators会将action和dispatch绑定并返回一个对象,这个对象会和ownProps一起作为props的一部分传入ui组件。所以不论mapDispatchToProps是对象还是函数,它最终都会返回一个对象,如果是函数,这个对象的key值是可以自定义的 295 | 296 | ```javascript 297 | function mapDispatchToProps(dispatch) { 298 | return { 299 | todoActions: bindActionCreators(todoActionCreators, dispatch), 300 | counterActions: bindActionCreators(counterActionCreators, dispatch) 301 | }; 302 | } 303 | ``` 304 | 305 | mapDispatchToProps返回的对象其属性其实就是一个个actionCreator,因为已经和dispatch绑定,所以当调用actionCreator时会立即发送action,而不用手动dispatch。ownProps的变化也会触发mapDispatchToProps。 306 | 307 | **mergeProps(stateProps, dispatchProps, ownProps):** 308 | > 将mapStateToProps() 与 mapDispatchToProps()返回的对象和组件自身的props合并成新的props并传入组件。默认返回 Object.assign({}, ownProps, stateProps, dispatchProps) 的结果。 309 | 310 | **options:** 311 | > pure = true 表示Connect容器组件将在shouldComponentUpdate中对store的state和ownProps进行浅对比,判断是否发生变化,优化性能。为false则不对比。 312 | 313 | 其实connect函数并没有做什么,大部分的逻辑都是在它返回的wrapWithConnect函数内实现的,确切的说是在wrapWithConnect内定义的Connect组件里实现的。 314 | 315 | ### 下面是一个完整的 react --> redux --> react 流程: 316 | 317 | 318 | 一、Provider组件接受redux的store作为props,然后通过context往下传。 319 | 320 | 二、connect函数在初始化的时候会将mapDispatchToProps对象绑定到store,如果mapDispatchToProps是函数则在Connect组件获得store后,根据传入的store.dispatch和action通过bindActionCreators进行绑定,再将返回的对象绑定到store,connect函数会返回一个wrapWithConnect函数,同时wrapWithConnect会被调用且传入一个ui组件,wrapWithConnect内部使用class Connect extends Component定义了一个Connect组件,传入的ui组件就是Connect的子组件,然后Connect组件会通过context获得store,并通过store.getState获得完整的state对象,将state传入mapStateToProps返回stateProps对象、mapDispatchToProps对象或mapDispatchToProps函数会返回一个dispatchProps对象,stateProps、dispatchProps以及Connect组件的props三者通过Object.assign(),或者mergeProps合并为props传入ui组件。然后在ComponentDidMount中调用store.subscribe,注册了一个回调函数handleChange监听state的变化。 321 | 322 | 三、此时ui组件就可以在props中找到actionCreator,当我们调用actionCreator时会自动调用dispatch,在dispatch中会调用getState获取整个state,同时注册一个listener监听state的变化,store将获得的state和action传给combineReducers,combineReducers会将state依据state的key值分别传给子reducer,并将action传给全部子reducer,reducer会被依次执行进行action.type的判断,如果有则返回一个新的state,如果没有则返回默认。combineReducers再次将子reducer返回的单个state进行合并成一个新的完整的state。此时state发生了变化。dispatch在state返回新的值之后会调用所有注册的listener函数其中包括handleChange函数,handleChange函数内部首先调用getState获取新的state值并对新旧两个state进行浅对比,如果相同直接return,如果不同则调用mapStateToProps获取stateProps并将新旧两个stateProps进行浅对比,如果相同,直接return结束,不进行后续操作。如果不相同则调用this.setState()触发Connect组件的更新,传入ui组件,触发ui组件的更新,此时ui组件获得新的props,react --> redux --> react 的一次流程结束。 323 | 324 | 325 | **上面的有点复杂,简化版的流程是:** 326 | 327 | 一、Provider组件接受redux的store作为props,然后通过context往下传。 328 | 329 | 二、connect函数收到Provider传出的store,然后接受三个参数mapStateToProps,mapDispatchToProps和组件,并将state和actionCreator以props传入组件,这时组件就可以调用actionCreator函数来触发reducer函数返回新的state,connect监听到state变化调用setState更新组件并将新的state传入组件。 330 | 331 | connect可以写的非常简洁,mapStateToProps,mapDispatchToProps只不过是传入的回调函数,connect函数在必要的时候会调用它们,名字不是固定的,甚至可以不写名字。 332 | 333 | 简化版本: 334 | ```javascript 335 | connect(state => state, action)(Component); 336 | ``` 337 | 338 | ## 项目搭建 339 | 340 | 上面说了react,react-router和redux的知识点。但是怎么样将它们整合起来,搭建一个完整的项目。 341 | 342 | 1、先引用 react.js,redux,react-router 等基本文件,建议用npm安装,直接在文件中引用。 343 | 344 | 2、从 react.js,redux,react-router 中引入所需要的对象和方法。 345 | ```javascript 346 | import React, {Component, PropTypes} from 'react'; 347 | import ReactDOM, {render} from 'react-dom'; 348 | import {Provider, connect} from 'react-redux'; 349 | import {createStore, combineReducers, applyMiddleware} from 'redux'; 350 | import { Router, Route, Redirect, IndexRoute, browserHistory, hashHistory } from 'react-router'; 351 | ``` 352 | 3、根据需求创建顶层ui组件,每个顶层ui组件对应一个页面。 353 | 354 | 4、创建actionCreators和reducers,并用combineReducers将所有的reducer合并成一个大的reduer。利用createStore创建store并引入combineReducers和applyMiddleware。 355 | 356 | 5、利用connect将actionCreator,reuder和顶层的ui组件进行关联并返回一个新的组件。 357 | 358 | 6、利用connect返回的新的组件配合react-router进行路由的部署,返回一个路由组件Router。 359 | 360 | 7、将Router放入最顶层组件Provider,引入store作为Provider的属性。 361 | 362 | 8、调用render渲染Provider组件且放入页面的标签中。 363 | 364 | 可以看到顶层的ui组件其实被套了四层组件,Provider,Router,Route,Connect,这四个组件并不会在视图上改变react,它们只是功能性的。 365 | 366 | 通常我们在顶层的ui组件打印props时可以看到一堆属性: 367 | 368 | ![](https://github.com/bailicangdu/pxq/blob/master/screenshot/react_props.png) 369 | 370 | 上图的顶层ui组件属性总共有18个,如果刚刚接触react,可能对这些属性怎么来的感到困惑,其实这些属性来自五个地方: 371 | 372 | 组件自定义属性1个,actionCreator返回的对象6个,reducer返回的state4个,Connect组件属性0个,以及Router注入的属性7个。 373 | -------------------------------------------------------------------------------- /bin/react-scripts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Copyright (c) 2015-present, Facebook, Inc. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | const spawn = require('react-dev-utils/crossSpawn'); 12 | const args = process.argv.slice(2); 13 | 14 | const scriptIndex = args.findIndex( 15 | x => x === 'build' || x === 'eject' || x === 'start' || x === 'test' 16 | ); 17 | const script = scriptIndex === -1 ? args[0] : args[scriptIndex]; 18 | const nodeArgs = scriptIndex > 0 ? args.slice(0, scriptIndex) : []; 19 | 20 | switch (script) { 21 | case 'build': 22 | case 'eject': 23 | case 'start': 24 | case 'test': { 25 | const result = spawn.sync( 26 | 'node', 27 | nodeArgs 28 | .concat(require.resolve('../scripts/' + script)) 29 | .concat(args.slice(scriptIndex + 1)), 30 | { stdio: 'inherit' } 31 | ); 32 | if (result.signal) { 33 | if (result.signal === 'SIGKILL') { 34 | console.log( 35 | 'The build failed because the process exited too early. ' + 36 | 'This probably means the system ran out of memory or someone called ' + 37 | '`kill -9` on the process.' 38 | ); 39 | } else if (result.signal === 'SIGTERM') { 40 | console.log( 41 | 'The build failed because the process exited too early. ' + 42 | 'Someone might have called `kill` or `killall`, or the system could ' + 43 | 'be shutting down.' 44 | ); 45 | } 46 | process.exit(1); 47 | } 48 | process.exit(result.status); 49 | break; 50 | } 51 | default: 52 | console.log('Unknown script "' + script + '".'); 53 | console.log('Perhaps you need to update react-scripts?'); 54 | console.log( 55 | 'See: https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#updating-to-new-releases' 56 | ); 57 | break; 58 | } 59 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | // @remove-on-eject-begin 2 | /** 3 | * Copyright (c) 2015-present, Facebook, Inc. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | // @remove-on-eject-end 9 | 'use strict'; 10 | 11 | const fs = require('fs'); 12 | const path = require('path'); 13 | const paths = require('./paths'); 14 | 15 | // Make sure that including paths.js after env.js will read .env variables. 16 | delete require.cache[require.resolve('./paths')]; 17 | 18 | const NODE_ENV = process.env.NODE_ENV; 19 | if (!NODE_ENV) { 20 | throw new Error( 21 | 'The NODE_ENV environment variable is required but was not specified.' 22 | ); 23 | } 24 | 25 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 26 | var dotenvFiles = [ 27 | `${paths.dotenv}.${NODE_ENV}.local`, 28 | `${paths.dotenv}.${NODE_ENV}`, 29 | // Don't include `.env.local` for `test` environment 30 | // since normally you expect tests to produce the same 31 | // results for everyone 32 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 33 | paths.dotenv, 34 | ].filter(Boolean); 35 | 36 | // Load environment variables from .env* files. Suppress warnings using silent 37 | // if this file is missing. dotenv will never modify any environment variables 38 | // that have already been set. 39 | // https://github.com/motdotla/dotenv 40 | dotenvFiles.forEach(dotenvFile => { 41 | if (fs.existsSync(dotenvFile)) { 42 | require('dotenv').config({ 43 | path: dotenvFile, 44 | }); 45 | } 46 | }); 47 | 48 | // We support resolving modules according to `NODE_PATH`. 49 | // This lets you use absolute paths in imports inside large monorepos: 50 | // https://github.com/facebookincubator/create-react-app/issues/253. 51 | // It works similar to `NODE_PATH` in Node itself: 52 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 53 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 54 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 55 | // https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 56 | // We also resolve them to make sure all tools using them work consistently. 57 | const appDirectory = fs.realpathSync(process.cwd()); 58 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 59 | .split(path.delimiter) 60 | .filter(folder => folder && !path.isAbsolute(folder)) 61 | .map(folder => path.resolve(appDirectory, folder)) 62 | .join(path.delimiter); 63 | 64 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 65 | // injected into the application via DefinePlugin in Webpack configuration. 66 | const REACT_APP = /^REACT_APP_/i; 67 | 68 | function getClientEnvironment(publicUrl) { 69 | const raw = Object.keys(process.env) 70 | .filter(key => REACT_APP.test(key)) 71 | .reduce( 72 | (env, key) => { 73 | env[key] = process.env[key]; 74 | return env; 75 | }, 76 | { 77 | // Useful for determining whether we’re running in production mode. 78 | // Most importantly, it switches React into the correct mode. 79 | NODE_ENV: process.env.NODE_ENV || 'development', 80 | // Useful for resolving the correct path to static assets in `public`. 81 | // For example, . 82 | // This should only be used as an escape hatch. Normally you would put 83 | // images into the `src` and `import` them in code to get their paths. 84 | PUBLIC_URL: publicUrl, 85 | STATIC_ENV: process.env.STATIC_ENV || 'development', 86 | } 87 | ); 88 | // Stringify all values so we can feed into Webpack DefinePlugin 89 | const stringified = { 90 | 'process.env': Object.keys(raw).reduce((env, key) => { 91 | env[key] = JSON.stringify(raw[key]); 92 | return env; 93 | }, {}), 94 | }; 95 | 96 | return { raw, stringified }; 97 | } 98 | 99 | module.exports = getClientEnvironment; 100 | -------------------------------------------------------------------------------- /config/jest/babelTransform.js: -------------------------------------------------------------------------------- 1 | // @remove-file-on-eject 2 | /** 3 | * Copyright (c) 2014-present, Facebook, Inc. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 'use strict'; 9 | 10 | const babelJest = require('babel-jest'); 11 | 12 | module.exports = babelJest.createTransformer({ 13 | presets: [require.resolve('babel-preset-react-app')], 14 | babelrc: false, 15 | }); 16 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | // @remove-on-eject-begin 2 | /** 3 | * Copyright (c) 2014-present, Facebook, Inc. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | // @remove-on-eject-end 9 | 'use strict'; 10 | 11 | // This is a custom Jest transformer turning style imports into empty objects. 12 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 13 | 14 | module.exports = { 15 | process() { 16 | return 'module.exports = {};'; 17 | }, 18 | getCacheKey() { 19 | // The output is always the same. 20 | return 'cssTransform'; 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | // @remove-on-eject-begin 2 | /** 3 | * Copyright (c) 2014-present, Facebook, Inc. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | // @remove-on-eject-end 9 | 'use strict'; 10 | 11 | const path = require('path'); 12 | 13 | // This is a custom Jest transformer turning file imports into filenames. 14 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 15 | 16 | module.exports = { 17 | process(src, filename) { 18 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | // @remove-on-eject-begin 2 | /** 3 | * Copyright (c) 2015-present, Facebook, Inc. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | // @remove-on-eject-end 9 | 'use strict'; 10 | 11 | const path = require('path'); 12 | const fs = require('fs'); 13 | const url = require('url'); 14 | 15 | // Make sure any symlinks in the project folder are resolved: 16 | // https://github.com/facebookincubator/create-react-app/issues/637 17 | const appDirectory = fs.realpathSync(process.cwd()); 18 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 19 | 20 | const envPublicUrl = process.env.PUBLIC_URL; 21 | const appName = envPublicUrl&&envPublicUrl.split('/').reverse()[0]; 22 | 23 | function ensureSlash(path, needsSlash) { 24 | const hasSlash = path.endsWith('/'); 25 | if (hasSlash && !needsSlash) { 26 | return path.substr(path, path.length - 1); 27 | } else if (!hasSlash && needsSlash) { 28 | return `${path}/`; 29 | } else { 30 | return path; 31 | } 32 | } 33 | 34 | const getPublicUrl = appPackageJson => 35 | envPublicUrl || require(appPackageJson).homepage; 36 | 37 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 38 | // "public path" at which the app is served. 39 | // Webpack needs to know it to put the right -------------------------------------------------------------------------------- /pxq/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /pxq/service-worker.js: -------------------------------------------------------------------------------- 1 | "use strict";var precacheConfig=[["/pxq/index.html","875215ec8d140e525fd454d2c022057b"],["/pxq/static/css/main.fc2ff5ec.css","fc2ff5ec6f5dc4032dabfcd7f2aaa7f7"],["/pxq/static/js/0.c353ab85.chunk.js","e7c50c13f6ea3fcedaa90f4b338e8435"],["/pxq/static/js/1.af913d75.chunk.js","6940f47e358dd059edaeadc24ccdf564"],["/pxq/static/js/2.a96eb65f.chunk.js","6a1675b658706d0ff2c0a3d23f5e1153"],["/pxq/static/js/3.ff2875c3.chunk.js","8ff44e7d84be779d753b578d97f93419"],["/pxq/static/js/main.83dc4a4d.js","c00f625a5e31f8b88f4b4930a59a1f2c"],["/pxq/static/media/iconfont.6924d946.svg","6924d946d064c8e9e3e13857d0b17c92"],["/pxq/static/media/iconfont.7e008a77.eot","7e008a771d5079e5051b632adcff1243"],["/pxq/static/media/iconfont.d828102a.ttf","d828102abc66b0a0a7cd5eb3f9b4e2b4"]],cacheName="sw-precache-v3-sw-precache-webpack-plugin-"+(self.registration?self.registration.scope:""),ignoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var n=new URL(e);return"/"===n.pathname.slice(-1)&&(n.pathname+=t),n.toString()},cleanResponse=function(t){return t.redirected?("body"in t?Promise.resolve(t.body):t.blob()).then(function(e){return new Response(e,{headers:t.headers,status:t.status,statusText:t.statusText})}):Promise.resolve(t)},createCacheKey=function(e,t,n,r){var a=new URL(e);return r&&a.pathname.match(r)||(a.search+=(a.search?"&":"")+encodeURIComponent(t)+"="+encodeURIComponent(n)),a.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var n=new URL(t).pathname;return e.some(function(e){return n.match(e)})},stripIgnoredUrlParameters=function(e,n){var t=new URL(e);return t.hash="",t.search=t.search.slice(1).split("&").map(function(e){return e.split("=")}).filter(function(t){return n.every(function(e){return!e.test(t[0])})}).map(function(e){return e.join("=")}).join("&"),t.toString()},hashParamName="_sw-precache",urlsToCacheKeys=new Map(precacheConfig.map(function(e){var t=e[0],n=e[1],r=new URL(t,self.location),a=createCacheKey(r,hashParamName,n,/\.\w{8}\./);return[r.toString(),a]}));function setOfCachedUrls(e){return e.keys().then(function(e){return e.map(function(e){return e.url})}).then(function(e){return new Set(e)})}self.addEventListener("install",function(e){e.waitUntil(caches.open(cacheName).then(function(r){return setOfCachedUrls(r).then(function(n){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(t){if(!n.has(t)){var e=new Request(t,{credentials:"same-origin"});return fetch(e).then(function(e){if(!e.ok)throw new Error("Request for "+t+" returned a response with status "+e.status);return cleanResponse(e).then(function(e){return r.put(t,e)})})}}))})}).then(function(){return self.skipWaiting()}))}),self.addEventListener("activate",function(e){var n=new Set(urlsToCacheKeys.values());e.waitUntil(caches.open(cacheName).then(function(t){return t.keys().then(function(e){return Promise.all(e.map(function(e){if(!n.has(e.url))return t.delete(e)}))})}).then(function(){return self.clients.claim()}))}),self.addEventListener("fetch",function(t){if("GET"===t.request.method){var e,n=stripIgnoredUrlParameters(t.request.url,ignoreUrlParametersMatching),r="index.html";(e=urlsToCacheKeys.has(n))||(n=addDirectoryIndex(n,r),e=urlsToCacheKeys.has(n));var a="/pxq/index.html";!e&&"navigate"===t.request.mode&&isPathWhitelisted(["^(?!\\/__).*"],t.request.url)&&(n=new URL(a,self.location).toString(),e=urlsToCacheKeys.has(n)),e&&t.respondWith(caches.open(cacheName).then(function(e){return e.match(urlsToCacheKeys.get(n)).then(function(e){if(e)return e;throw Error("The cached response that was expected is missing.")})}).catch(function(e){return console.warn('Couldn\'t serve response for "%s" from cache: %O',t.request.url,e),fetch(t.request)}))}}); -------------------------------------------------------------------------------- /pxq/static/css/main.fc2ff5ec.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:iconfont;src:url(/pxq/static/media/iconfont.7e008a77.eot);src:url(/pxq/static/media/iconfont.7e008a77.eot#iefix) format("embedded-opentype"),url("data:application/x-font-woff;charset=utf-8;base64,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") format("woff"),url(/pxq/static/media/iconfont.d828102a.ttf) format("truetype"),url(/pxq/static/media/iconfont.6924d946.svg#iconfont) format("svg")}.iconfont{font-family:iconfont!important;font-size:.213333rem;font-style:normal;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon-yinliangda:before{content:"\E600"}.icon-yinliangxiao:before{content:"\E602"}.icon-sanjiaoxing:before{content:"\E656"}.icon-xuanze:before{content:"\E636"}.icon-xuanze1:before{content:"\E696"}.icon-zanting:before{content:"\E672"}.icon-jiantou-copy-copy:before{content:"\E679"}.icon-catalog:before{content:"\E716"}.icon-jingyin:before{content:"\E674"}.icon-quanping:before{content:"\E601"}.icon-jilu:before{content:"\E8D7"}.icon-jian:before{content:"\E711"}.icon-yinliang:before{content:"\EA1B"}.icon-tuichuquanping:before{content:"\E60D"}.icon-icon-test:before{content:"\E610"}.icon-jia:before{content:"\E6D9"}.icon-guanbi:before{content:"\E624"}.header-container{background-color:#975ec9;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center;position:fixed;top:0;left:0;z-index:10;width:100%;height:1.173333rem}.header-container .header-slide-icon{font-size:.48rem;color:#fff;text-align:left;position:absolute;top:50%;left:.4rem;-webkit-transform:translate3d(0,-50%,0);transform:translate3d(0,-50%,0)}.header-container .header-link,.header-container .header-title{font-size:.533333rem;color:#fff;text-align:left}.header-container .header-link{position:absolute;top:50%;right:.4rem;-webkit-transform:translate3d(0,-50%,0);transform:translate3d(0,-50%,0)}.header-container .header-link-confim{font-size:.426667rem}.header-container .nav-slide-list{position:fixed;top:0;left:0;right:0;bottom:0;width:100%;height:100%;background-color:#fff;top:1.173333rem;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.header-container .nav-slide-list .nav-link{font-size:.426667rem;color:#333;text-align:left;border-bottom:1px solid #ddd;margin:0 .4rem;padding:.466667rem .266667rem;position:relative}.header-container .nav-slide-list .nav-link:before{position:absolute;right:.133333rem;top:50%;-webkit-transform:translate3d(0,-50%,0);transform:translate3d(0,-50%,0);color:#666}.header-container .nav-enter{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.header-container .nav-enter.nav-enter-active{-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;-o-transition:transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s}.header-container .nav-enter.nav-enter-active,.header-container .nav-leave{-webkit-transform:translateZ(0);transform:translateZ(0)}.header-container .nav-leave.nav-leave-active{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;-o-transition:transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s}.alert-con{position:fixed;top:0;left:0;right:0;bottom:0;width:100%;height:100%;background-color:rgba(0,0,0,.3);z-index:11}.alert-context{position:absolute;top:50%;left:50%;-webkit-transform:translate3d(-50%,-50%,0);transform:translate3d(-50%,-50%,0);z-index:12;width:8rem;height:4.8rem;background-color:#fff;border-radius:.213333rem;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:stretch;align-items:stretch;-ms-flex-direction:column;flex-direction:column}.alert-context .alert-content-detail{-ms-flex:5 1;flex:5 1;-ms-flex-pack:center;-ms-flex-align:center}.alert-context .alert-content-detail,.alert-context .confirm-btn{display:-ms-flexbox;display:flex;justify-content:center;align-items:center;font-size:.533333rem;color:#333;text-align:left}.alert-context .confirm-btn{-ms-flex:2 1;flex:2 1;-ms-flex-pack:center;-ms-flex-align:center;border-top:1px solid #eee}.alert-enter{opacity:0}.alert-enter.alert-enter-active{-webkit-transition:all .3s;-o-transition:all .3s;transition:all .3s;opacity:1}.alert-leave{opacity:1}.alert-leave.alert-leave-active{-webkit-transition:all .3s;-o-transition:all .3s;transition:all .3s;opacity:0}.home-container{padding-top:1.173333rem}.home-container .common-title{font-size:.48rem;color:#975ec9;text-align:left;padding:.4rem}.home-container .home-form{background-color:#fff}.home-container .home-form .home-form-tiem{height:1.6rem;border-bottom:1px solid #eee}.home-container .home-form .home-form-tiem span{font-size:.426667rem;color:#555;text-align:left;margin-left:.4rem;margin-right:.266667rem}.home-container .home-form .home-form-tiem input{border:none;font-size:.4rem}.home-container .home-form .home-form-tiem ::-webkit-input-placeholder{color:#ccc}.home-container .common-select-btn{background-color:#fff;display:block;min-height:1.6rem;font-size:.56rem;color:#ccc;text-align:center;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center}.home-container .common-select-btn .selected-pro-list{display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center;-ms-flex-wrap:wrap;flex-wrap:wrap}.home-container .common-select-btn .selected-pro-list .selected-pro-item{width:4.533333rem;margin:.133333rem;line-height:.666667rem;font-size:.426667rem;color:#333;text-align:center}.home-container .upload-img-con{text-align:center}.home-container .upload-img-con .file-lable{position:relative}.home-container .upload-img-con .file-lable input[type=file]{position:absolute;width:100%;height:100%;background-color:red;top:0;right:0;opacity:0}.home-container .upload-img-con .select-img{margin-top:.133333rem}.home-container .submit-btn{width:90%;background-color:#975ec9;margin:.666667rem auto 0;border-radius:.133333rem;line-height:1.333333rem;font-size:.533333rem;color:#fff;text-align:center}*{margin:0;padding:0;-webkit-tap-highlight-color:transparent;-webkit-text-size-adjust:none;font-family:Helvetica Neue,Helvetica,STHeiTi,sans-serif;-webkit-font-smoothing:antialiased;-webkit-box-sizing:border-box;box-sizing:border-box;font-weight:400}img{vertical-align:top;border:none}button:focus,input:focus,select:focus,textarea:focus{outline:none}input[type=email],input[type=file],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=url],textarea{-webkit-appearance:none}input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none}[class*=" icon-"],[class^=icon-]{font-family:iconfont;font-size:.213333rem;font-style:normal;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}a{text-decoration:none;-webkit-touch-callout:none}body{background-color:#f5f5f5}em,i{font-style:normal}li{list-style:none}body,html{height:100%}.common-con-top{margin-top:1.173333rem}.ellipsis{overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis;white-space:nowrap} -------------------------------------------------------------------------------- /pxq/static/js/0.c353ab85.chunk.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([0],{514:function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!==typeof t&&"function"!==typeof t?e:t}function a(e,t){if("function"!==typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var c=n(5),i=n.n(c),s=n(51),l=(n.n(s),n(75)),A=n(184),m=n(518),p=n(521),f=(n.n(p),function(){function e(e,t){for(var n=0;n0?"edit-active":""),onClick:e.handleEdit.bind(e,n,-1)}),a.a.createElement("span",{className:"pro-num"},t.selectNum),a.a.createElement("span",{className:"icon-jia",onClick:e.handleEdit.bind(e,n,1)})))}))))}}]),t}(c.Component),l.propTypes={proData:f.a.object.isRequired,getProData:f.a.func.isRequired,togSelectPro:f.a.func.isRequired,editPro:f.a.func.isRequired},s);t.default=Object(A.b)(function(e){return{proData:e.proData}},{getProData:m.c,togSelectPro:m.d,editPro:m.b})(g)},525:function(e,t,n){var o=n(526);"string"===typeof o&&(o=[[e.i,o,""]]);var r={hmr:!0};r.transform=void 0;n(512)(o,r);o.locals&&(e.exports=o.locals)},526:function(e,t,n){t=e.exports=n(183)(!0),t.i(n(513),""),t.push([e.i,".pro-list-con{padding-top:.4rem}.pro-list-ul{background-color:#fff}.pro-list-ul .pro-item{min-height:1.866667rem;padding:.4rem;border-bottom:1px solid #eee;-ms-flex-pack:justify;justify-content:space-between}.pro-list-ul .pro-item,.pro-list-ul .pro-item .pro-item-select{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.pro-list-ul .pro-item .pro-item-select{-ms-flex-pack:center;justify-content:center}.pro-list-ul .pro-item .pro-item-select .pro-select-status{font-size:.6rem;color:#ccc;text-align:left}.pro-list-ul .pro-item .pro-item-select .pro-selected{color:#975ec9}.pro-list-ul .pro-item .pro-item-select .pro-name{font-size:.48rem;color:#333;text-align:left;margin-left:.266667rem;margin-top:.16rem}.pro-list-ul .pro-item .pro-item-edit{display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center}.pro-list-ul .pro-item .pro-item-edit .icon-jian{font-size:.666667rem;color:#ccc;text-align:left}.pro-list-ul .pro-item .pro-item-edit .pro-num{font-size:.4rem;color:#333;text-align:center;min-width:.8rem}.pro-list-ul .pro-item .pro-item-edit .icon-jia{font-size:.666667rem;color:#975ec9;text-align:left}.pro-list-ul .pro-item .pro-item-edit .edit-active{color:#975ec9}","",{version:3,sources:["/mygit/react-pxq/src/pages/production/production.less"],names:[],mappings:"AAEA,cACE,iBAAoB,CACrB,AAED,aACE,qBAAuB,CACxB,AAED,uBACE,uBAAwB,AACxB,cAAgB,AAChB,6BAA8B,AAG9B,sBAAuB,AACvB,6BAA+B,CAGhC,AAED,+DARE,oBAAqB,AACrB,aAAc,AAGd,sBAAuB,AACvB,kBAAoB,CAUrB,AAPD,wCAGE,qBAAsB,AACtB,sBAAwB,CAGzB,AAED,2DACE,gBAAkB,AAClB,WAAY,AACZ,eAAiB,CAClB,AAED,sDACE,aAAe,CAChB,AAED,kDACE,iBAAmB,AACnB,WAAY,AACZ,gBAAiB,AACjB,uBAAyB,AACzB,iBAAoB,CACrB,AAED,sCACE,oBAAqB,AACrB,aAAc,AACd,qBAAsB,AACtB,uBAAwB,AACxB,sBAAuB,AACvB,kBAAoB,CACrB,AAED,iDACE,qBAAuB,AACvB,WAAY,AACZ,eAAiB,CAClB,AAED,+CACE,gBAAkB,AAClB,WAAY,AACZ,kBAAmB,AACnB,eAAkB,CACnB,AAED,gDACE,qBAAuB,AACvB,cAAe,AACf,eAAiB,CAClB,AAED,mDACE,aAAe,CAChB",file:"production.less",sourcesContent:['@import "../../assets/iconfonts/iconfont.css";\n\n.pro-list-con {\n padding-top: 0.4rem;\n}\n\n.pro-list-ul {\n background-color: #fff;\n}\n\n.pro-list-ul .pro-item {\n min-height: 1.866667rem;\n padding: 0.4rem;\n border-bottom: 1PX solid #eee;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-pack: justify;\n justify-content: space-between;\n -ms-flex-align: center;\n align-items: center;\n}\n\n.pro-list-ul .pro-item .pro-item-select {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-pack: center;\n justify-content: center;\n -ms-flex-align: center;\n align-items: center;\n}\n\n.pro-list-ul .pro-item .pro-item-select .pro-select-status {\n font-size: 0.6rem;\n color: #ccc;\n text-align: left;\n}\n\n.pro-list-ul .pro-item .pro-item-select .pro-selected {\n color: #975ec9;\n}\n\n.pro-list-ul .pro-item .pro-item-select .pro-name {\n font-size: 0.48rem;\n color: #333;\n text-align: left;\n margin-left: 0.266667rem;\n margin-top: 0.16rem;\n}\n\n.pro-list-ul .pro-item .pro-item-edit {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-pack: center;\n justify-content: center;\n -ms-flex-align: center;\n align-items: center;\n}\n\n.pro-list-ul .pro-item .pro-item-edit .icon-jian {\n font-size: 0.666667rem;\n color: #ccc;\n text-align: left;\n}\n\n.pro-list-ul .pro-item .pro-item-edit .pro-num {\n font-size: 0.4rem;\n color: #333;\n text-align: center;\n min-width: 0.8rem;\n}\n\n.pro-list-ul .pro-item .pro-item-edit .icon-jia {\n font-size: 0.666667rem;\n color: #975ec9;\n text-align: left;\n}\n\n.pro-list-ul .pro-item .pro-item-edit .edit-active {\n color: #975ec9;\n}'],sourceRoot:""}])}}); -------------------------------------------------------------------------------- /pxq/static/js/2.a96eb65f.chunk.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([2],{515:function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!==typeof t&&"function"!==typeof t?e:t}function a(e,t){if("function"!==typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0}),n.d(t,"default",function(){return s});var c=n(5),l=n.n(c),i=n(184),u=n(51),p=(n.n(u),n(523)),m=(n.n(p),function(){function e(e,t){for(var n=0;n200&&(t="200.00"),i.setState({applyNum:t}))},i.sumitForm=function(){var e=void 0;e=i.state.applyNum?parseFloat(i.state.applyNum)>i.state.balance.balance?"\u7533\u8bf7\u63d0\u73b0\u91d1\u989d\u4e0d\u80fd\u5927\u4e8e\u4f59\u989d":"\u7533\u8bf7\u63d0\u73b0\u6210\u529f":"\u8bf7\u8f93\u5165\u63d0\u73b0\u91d1\u989d",i.setState({alertStatus:!0,alertTip:e,applyNum:""})},i.closeAlert=function(){i.setState({alertStatus:!1,alertTip:""})},c=n,a(i,c)}return i(t,e),h(t,[{key:"shouldComponentUpdate",value:function(e,t){return!Object(m.is)(Object(m.fromJS)(this.props),Object(m.fromJS)(e))||!Object(m.is)(Object(m.fromJS)(this.state),Object(m.fromJS)(t))}},{key:"componentDidMount",value:function(){this.initData()}},{key:"render",value:function(){return A.a.createElement("main",{className:"home-container"},A.a.createElement(p.a,{title:"\u63d0\u73b0",record:!0}),A.a.createElement("section",{className:"broke-main-content"},A.a.createElement("p",{className:"broke-header"},"\u60a8\u7684\u53ef\u63d0\u73b0\u91d1\u989d\u4e3a\uff1a\xa5 ",this.state.balance.balance),A.a.createElement("form",{className:"broke-form"},A.a.createElement("p",null,"\u8bf7\u8f93\u5165\u63d0\u73b0\u91d1\u989d\uff08\u5143\uff09"),A.a.createElement("p",null,"\xa5 ",A.a.createElement("input",{type:"text",value:this.state.applyNum,placeholder:"0.00",onInput:this.handleInput,maxLength:"5"}))),A.a.createElement(f.a,{className:"submit-btn",clickCallBack:this.sumitForm,text:"\u7533\u8bf7\u63d0\u73b0"})),A.a.createElement(u.a,{closeAlert:this.closeAlert,alertTip:this.state.alertTip,alertStatus:this.state.alertStatus}))}}]),t}(s.Component);t.default=C},527:function(e,t,n){var r=n(528);"string"===typeof r&&(r=[[e.i,r,""]]);var o={hmr:!0};o.transform=void 0;n(512)(r,o);r.locals&&(e.exports=r.locals)},528:function(e,t,n){t=e.exports=n(183)(!0),t.push([e.i,".broke-main-content{padding-top:.4rem}.broke-main-content .broke-header{font-size:.426667rem;color:#999;text-align:left;padding:0 .4rem}.broke-main-content .broke-form{background-color:#fff;min-height:3.333333rem;margin-top:.4rem;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center;-ms-flex-direction:column;flex-direction:column}.broke-main-content .broke-form p:first-of-type{font-size:.533333rem;color:#333;text-align:left}.broke-main-content .broke-form p:nth-of-type(2){font-size:.533333rem;color:#333;text-align:left;margin-top:.4rem}.broke-main-content .broke-form p:nth-of-type(2) ::-webkit-input-placeholder{color:#ccc}.broke-main-content .broke-form p:nth-of-type(2) input{border:none;font-size:.533333rem;color:#333;text-align:left;width:2.4rem}","",{version:3,sources:["/mygit/react-pxq/src/pages/balance/balance.less"],names:[],mappings:"AAAA,oBACE,iBAAoB,CACrB,AAED,kCACE,qBAAuB,AACvB,WAAY,AACZ,gBAAiB,AACjB,eAAkB,CACnB,AAED,gCACE,sBAAuB,AACvB,uBAAwB,AACxB,iBAAmB,AACnB,oBAAqB,AACrB,aAAc,AACd,qBAAsB,AACtB,uBAAwB,AACxB,sBAAuB,AACvB,mBAAoB,AACpB,0BAA2B,AAC3B,qBAAuB,CACxB,AAED,gDACE,qBAAuB,AACvB,WAAY,AACZ,eAAiB,CAClB,AAED,iDACE,qBAAuB,AACvB,WAAY,AACZ,gBAAiB,AACjB,gBAAmB,CACpB,AAED,6EACE,UAAY,CACb,AAED,uDACE,YAAa,AACb,qBAAuB,AACvB,WAAY,AACZ,gBAAiB,AACjB,YAAc,CACf",file:"balance.less",sourcesContent:[".broke-main-content {\n padding-top: 0.4rem;\n}\n\n.broke-main-content .broke-header {\n font-size: 0.426667rem;\n color: #999;\n text-align: left;\n padding: 0 0.4rem;\n}\n\n.broke-main-content .broke-form {\n background-color: #fff;\n min-height: 3.333333rem;\n margin-top: 0.4rem;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-pack: center;\n justify-content: center;\n -ms-flex-align: center;\n align-items: center;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n\n.broke-main-content .broke-form p:nth-of-type(1) {\n font-size: 0.533333rem;\n color: #333;\n text-align: left;\n}\n\n.broke-main-content .broke-form p:nth-of-type(2) {\n font-size: 0.533333rem;\n color: #333;\n text-align: left;\n margin-top: 0.4rem;\n}\n\n.broke-main-content .broke-form p:nth-of-type(2) ::-webkit-input-placeholder {\n color: #ccc;\n}\n\n.broke-main-content .broke-form p:nth-of-type(2) input {\n border: none;\n font-size: 0.533333rem;\n color: #333;\n text-align: left;\n width: 2.4rem;\n}"],sourceRoot:""}])}}); -------------------------------------------------------------------------------- /pxq/static/media/iconfont.6924d946.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Created by iconfont 9 | 10 | 11 | 12 | 13 | 21 | 22 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /pxq/static/media/iconfont.7e008a77.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bailicangdu/react-pxq/553e42f41b9ed4ad0d30216aedf7e3fb7c06fc3e/pxq/static/media/iconfont.7e008a77.eot -------------------------------------------------------------------------------- /pxq/static/media/iconfont.d828102a.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bailicangdu/react-pxq/553e42f41b9ed4ad0d30216aedf7e3fb7c06fc3e/pxq/static/media/iconfont.d828102a.ttf -------------------------------------------------------------------------------- /screenshot/all_redux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bailicangdu/react-pxq/553e42f41b9ed4ad0d30216aedf7e3fb7c06fc3e/screenshot/all_redux.png -------------------------------------------------------------------------------- /screenshot/demo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bailicangdu/react-pxq/553e42f41b9ed4ad0d30216aedf7e3fb7c06fc3e/screenshot/demo1.png -------------------------------------------------------------------------------- /screenshot/diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bailicangdu/react-pxq/553e42f41b9ed4ad0d30216aedf7e3fb7c06fc3e/screenshot/diff.png -------------------------------------------------------------------------------- /screenshot/icon_class.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bailicangdu/react-pxq/553e42f41b9ed4ad0d30216aedf7e3fb7c06fc3e/screenshot/icon_class.png -------------------------------------------------------------------------------- /screenshot/react-lifecycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bailicangdu/react-pxq/553e42f41b9ed4ad0d30216aedf7e3fb7c06fc3e/screenshot/react-lifecycle.png -------------------------------------------------------------------------------- /screenshot/react_props.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bailicangdu/react-pxq/553e42f41b9ed4ad0d30216aedf7e3fb7c06fc3e/screenshot/react_props.png -------------------------------------------------------------------------------- /screenshot/simple_redux.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bailicangdu/react-pxq/553e42f41b9ed4ad0d30216aedf7e3fb7c06fc3e/screenshot/simple_redux.jpg -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | // @remove-on-eject-begin 2 | /** 3 | * Copyright (c) 2015-present, Facebook, Inc. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | // @remove-on-eject-end 9 | 'use strict'; 10 | 11 | // Do this as the first thing so that any code reading it knows the right env. 12 | process.env.BABEL_ENV = 'production'; 13 | process.env.NODE_ENV = 'production'; 14 | process.env.STATIC_ENV = 'production'; 15 | if(process.env.STATIC_ENV === 'production'){ 16 | process.env.PUBLIC_URL = '/pxq'; 17 | } 18 | 19 | // Makes the script crash on unhandled rejections instead of silently 20 | // ignoring them. In the future, promise rejections that are not handled will 21 | // terminate the Node.js process with a non-zero exit code. 22 | process.on('unhandledRejection', err => { 23 | throw err; 24 | }); 25 | 26 | // Ensure environment variables are read. 27 | require('../config/env'); 28 | 29 | const path = require('path'); 30 | const chalk = require('chalk'); 31 | const fs = require('fs-extra'); 32 | const webpack = require('webpack'); 33 | const config = require('../config/webpack.config.prod'); 34 | const paths = require('../config/paths'); 35 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 36 | const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); 37 | const printHostingInstructions = require('react-dev-utils/printHostingInstructions'); 38 | const FileSizeReporter = require('react-dev-utils/FileSizeReporter'); 39 | const printBuildError = require('react-dev-utils/printBuildError'); 40 | 41 | const measureFileSizesBeforeBuild = 42 | FileSizeReporter.measureFileSizesBeforeBuild; 43 | const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild; 44 | const useYarn = fs.existsSync(paths.yarnLockFile); 45 | 46 | // These sizes are pretty large. We'll warn for bundles exceeding them. 47 | const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; 48 | const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; 49 | 50 | // Warn and crash if required files are missing 51 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 52 | process.exit(1); 53 | } 54 | 55 | // First, read the current file sizes in build directory. 56 | // This lets us display how much they changed later. 57 | measureFileSizesBeforeBuild(paths.appBuild) 58 | .then(previousFileSizes => { 59 | // Remove all content but keep the directory so that 60 | // if you're in it, you don't end up in Trash 61 | fs.emptyDirSync(paths.appBuild); 62 | // Merge with the public folder 63 | copyPublicFolder(); 64 | // Start the webpack build 65 | return build(previousFileSizes); 66 | }) 67 | .then( 68 | ({ stats, previousFileSizes, warnings }) => { 69 | if (warnings.length) { 70 | console.log(chalk.yellow('Compiled with warnings.\n')); 71 | console.log(warnings.join('\n\n')); 72 | console.log( 73 | '\nSearch for the ' + 74 | chalk.underline(chalk.yellow('keywords')) + 75 | ' to learn more about each warning.' 76 | ); 77 | console.log( 78 | 'To ignore, add ' + 79 | chalk.cyan('// eslint-disable-next-line') + 80 | ' to the line before.\n' 81 | ); 82 | } else { 83 | console.log(chalk.green('Compiled successfully.\n')); 84 | } 85 | 86 | console.log('File sizes after gzip:\n'); 87 | printFileSizesAfterBuild( 88 | stats, 89 | previousFileSizes, 90 | paths.appBuild, 91 | WARN_AFTER_BUNDLE_GZIP_SIZE, 92 | WARN_AFTER_CHUNK_GZIP_SIZE 93 | ); 94 | console.log(); 95 | 96 | const appPackage = require(paths.appPackageJson); 97 | const publicUrl = paths.publicUrl; 98 | const publicPath = config.output.publicPath; 99 | const buildFolder = path.relative(process.cwd(), paths.appBuild); 100 | printHostingInstructions( 101 | appPackage, 102 | publicUrl, 103 | publicPath, 104 | buildFolder, 105 | useYarn 106 | ); 107 | }, 108 | err => { 109 | console.log(chalk.red('Failed to compile.\n')); 110 | printBuildError(err); 111 | process.exit(1); 112 | } 113 | ); 114 | 115 | // Create the production build and print the deployment instructions. 116 | function build(previousFileSizes) { 117 | console.log('Creating an optimized production build...'); 118 | 119 | let compiler = webpack(config); 120 | return new Promise((resolve, reject) => { 121 | compiler.run((err, stats) => { 122 | if (err) { 123 | return reject(err); 124 | } 125 | const messages = formatWebpackMessages(stats.toJson({}, true)); 126 | if (messages.errors.length) { 127 | // Only keep the first error. Others are often indicative 128 | // of the same problem, but confuse the reader with noise. 129 | if (messages.errors.length > 1) { 130 | messages.errors.length = 1; 131 | } 132 | return reject(new Error(messages.errors.join('\n\n'))); 133 | } 134 | if ( 135 | process.env.CI && 136 | (typeof process.env.CI !== 'string' || 137 | process.env.CI.toLowerCase() !== 'false') && 138 | messages.warnings.length 139 | ) { 140 | console.log( 141 | chalk.yellow( 142 | '\nTreating warnings as errors because process.env.CI = true.\n' + 143 | 'Most CI servers set it automatically.\n' 144 | ) 145 | ); 146 | return reject(new Error(messages.warnings.join('\n\n'))); 147 | } 148 | return resolve({ 149 | stats, 150 | previousFileSizes, 151 | warnings: messages.warnings, 152 | }); 153 | }); 154 | }); 155 | } 156 | 157 | function copyPublicFolder() { 158 | fs.copySync(paths.appPublic, paths.appBuild, { 159 | dereference: true, 160 | filter: file => file !== paths.appHtml, 161 | }); 162 | } 163 | -------------------------------------------------------------------------------- /scripts/eject.js: -------------------------------------------------------------------------------- 1 | // @remove-file-on-eject 2 | /** 3 | * Copyright (c) 2015-present, Facebook, Inc. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 'use strict'; 9 | 10 | // Makes the script crash on unhandled rejections instead of silently 11 | // ignoring them. In the future, promise rejections that are not handled will 12 | // terminate the Node.js process with a non-zero exit code. 13 | process.on('unhandledRejection', err => { 14 | throw err; 15 | }); 16 | 17 | const fs = require('fs-extra'); 18 | const path = require('path'); 19 | const execSync = require('child_process').execSync; 20 | const chalk = require('chalk'); 21 | const paths = require('../config/paths'); 22 | const createJestConfig = require('./utils/createJestConfig'); 23 | const inquirer = require('react-dev-utils/inquirer'); 24 | const spawnSync = require('react-dev-utils/crossSpawn').sync; 25 | 26 | const green = chalk.green; 27 | const cyan = chalk.cyan; 28 | 29 | function getGitStatus() { 30 | try { 31 | let stdout = execSync(`git status --porcelain`, { 32 | stdio: ['pipe', 'pipe', 'ignore'], 33 | }).toString(); 34 | return stdout.trim(); 35 | } catch (e) { 36 | return ''; 37 | } 38 | } 39 | 40 | inquirer 41 | .prompt({ 42 | type: 'confirm', 43 | name: 'shouldEject', 44 | message: 'Are you sure you want to eject? This action is permanent.', 45 | default: false, 46 | }) 47 | .then(answer => { 48 | if (!answer.shouldEject) { 49 | console.log(cyan('Close one! Eject aborted.')); 50 | return; 51 | } 52 | 53 | const gitStatus = getGitStatus(); 54 | if (gitStatus) { 55 | console.error( 56 | chalk.red( 57 | `This git repository has untracked files or uncommitted changes:\n\n` + 58 | gitStatus.split('\n').map(line => ' ' + line) + 59 | '\n\n' + 60 | 'Remove untracked files, stash or commit any changes, and try again.' 61 | ) 62 | ); 63 | process.exit(1); 64 | } 65 | 66 | console.log('Ejecting...'); 67 | 68 | const ownPath = paths.ownPath; 69 | const appPath = paths.appPath; 70 | 71 | function verifyAbsent(file) { 72 | if (fs.existsSync(path.join(appPath, file))) { 73 | console.error( 74 | `\`${file}\` already exists in your app folder. We cannot ` + 75 | 'continue as you would lose all the changes in that file or directory. ' + 76 | 'Please move or delete it (maybe make a copy for backup) and run this ' + 77 | 'command again.' 78 | ); 79 | process.exit(1); 80 | } 81 | } 82 | 83 | const folders = ['config', 'config/jest', 'scripts']; 84 | 85 | // Make shallow array of files paths 86 | const files = folders.reduce((files, folder) => { 87 | return files.concat( 88 | fs 89 | .readdirSync(path.join(ownPath, folder)) 90 | // set full path 91 | .map(file => path.join(ownPath, folder, file)) 92 | // omit dirs from file list 93 | .filter(file => fs.lstatSync(file).isFile()) 94 | ); 95 | }, []); 96 | 97 | // Ensure that the app folder is clean and we won't override any files 98 | folders.forEach(verifyAbsent); 99 | files.forEach(verifyAbsent); 100 | 101 | // Prepare Jest config early in case it throws 102 | const jestConfig = createJestConfig( 103 | filePath => path.posix.join('', filePath), 104 | null, 105 | true 106 | ); 107 | 108 | console.log(); 109 | console.log(cyan(`Copying files into ${appPath}`)); 110 | 111 | folders.forEach(folder => { 112 | fs.mkdirSync(path.join(appPath, folder)); 113 | }); 114 | 115 | files.forEach(file => { 116 | let content = fs.readFileSync(file, 'utf8'); 117 | 118 | // Skip flagged files 119 | if (content.match(/\/\/ @remove-file-on-eject/)) { 120 | return; 121 | } 122 | content = 123 | content 124 | // Remove dead code from .js files on eject 125 | .replace( 126 | /\/\/ @remove-on-eject-begin([\s\S]*?)\/\/ @remove-on-eject-end/gm, 127 | '' 128 | ) 129 | // Remove dead code from .applescript files on eject 130 | .replace( 131 | /-- @remove-on-eject-begin([\s\S]*?)-- @remove-on-eject-end/gm, 132 | '' 133 | ) 134 | .trim() + '\n'; 135 | console.log(` Adding ${cyan(file.replace(ownPath, ''))} to the project`); 136 | fs.writeFileSync(file.replace(ownPath, appPath), content); 137 | }); 138 | console.log(); 139 | 140 | const ownPackage = require(path.join(ownPath, 'package.json')); 141 | const appPackage = require(path.join(appPath, 'package.json')); 142 | 143 | console.log(cyan('Updating the dependencies')); 144 | const ownPackageName = ownPackage.name; 145 | if (appPackage.devDependencies) { 146 | // We used to put react-scripts in devDependencies 147 | if (appPackage.devDependencies[ownPackageName]) { 148 | console.log(` Removing ${cyan(ownPackageName)} from devDependencies`); 149 | delete appPackage.devDependencies[ownPackageName]; 150 | } 151 | } 152 | appPackage.dependencies = appPackage.dependencies || {}; 153 | if (appPackage.dependencies[ownPackageName]) { 154 | console.log(` Removing ${cyan(ownPackageName)} from dependencies`); 155 | delete appPackage.dependencies[ownPackageName]; 156 | } 157 | Object.keys(ownPackage.dependencies).forEach(key => { 158 | // For some reason optionalDependencies end up in dependencies after install 159 | if (ownPackage.optionalDependencies[key]) { 160 | return; 161 | } 162 | console.log(` Adding ${cyan(key)} to dependencies`); 163 | appPackage.dependencies[key] = ownPackage.dependencies[key]; 164 | }); 165 | // Sort the deps 166 | const unsortedDependencies = appPackage.dependencies; 167 | appPackage.dependencies = {}; 168 | Object.keys(unsortedDependencies) 169 | .sort() 170 | .forEach(key => { 171 | appPackage.dependencies[key] = unsortedDependencies[key]; 172 | }); 173 | console.log(); 174 | 175 | console.log(cyan('Updating the scripts')); 176 | delete appPackage.scripts['eject']; 177 | Object.keys(appPackage.scripts).forEach(key => { 178 | Object.keys(ownPackage.bin).forEach(binKey => { 179 | const regex = new RegExp(binKey + ' (\\w+)', 'g'); 180 | if (!regex.test(appPackage.scripts[key])) { 181 | return; 182 | } 183 | appPackage.scripts[key] = appPackage.scripts[key].replace( 184 | regex, 185 | 'node scripts/$1.js' 186 | ); 187 | console.log( 188 | ` Replacing ${cyan(`"${binKey} ${key}"`)} with ${cyan( 189 | `"node scripts/${key}.js"` 190 | )}` 191 | ); 192 | }); 193 | }); 194 | 195 | console.log(); 196 | console.log(cyan('Configuring package.json')); 197 | // Add Jest config 198 | console.log(` Adding ${cyan('Jest')} configuration`); 199 | appPackage.jest = jestConfig; 200 | 201 | // Add Babel config 202 | console.log(` Adding ${cyan('Babel')} preset`); 203 | appPackage.babel = { 204 | presets: ['react-app'], 205 | }; 206 | 207 | // Add ESlint config 208 | console.log(` Adding ${cyan('ESLint')} configuration`); 209 | appPackage.eslintConfig = { 210 | extends: 'react-app', 211 | }; 212 | 213 | fs.writeFileSync( 214 | path.join(appPath, 'package.json'), 215 | JSON.stringify(appPackage, null, 2) + '\n' 216 | ); 217 | console.log(); 218 | 219 | // "Don't destroy what isn't ours" 220 | if (ownPath.indexOf(appPath) === 0) { 221 | try { 222 | // remove react-scripts and react-scripts binaries from app node_modules 223 | Object.keys(ownPackage.bin).forEach(binKey => { 224 | fs.removeSync(path.join(appPath, 'node_modules', '.bin', binKey)); 225 | }); 226 | fs.removeSync(ownPath); 227 | } catch (e) { 228 | // It's not essential that this succeeds 229 | } 230 | } 231 | 232 | if (fs.existsSync(paths.yarnLockFile)) { 233 | // TODO: this is disabled for three reasons. 234 | // 235 | // 1. It produces garbage warnings on Windows on some systems: 236 | // https://github.com/facebookincubator/create-react-app/issues/2030 237 | // 238 | // 2. For the above reason, it breaks Windows CI: 239 | // https://github.com/facebookincubator/create-react-app/issues/2624 240 | // 241 | // 3. It is wrong anyway: re-running yarn will respect the lockfile 242 | // rather than package.json we just updated. Instead we should have 243 | // updated the lockfile. So we might as well not do it while it's broken. 244 | // https://github.com/facebookincubator/create-react-app/issues/2627 245 | // 246 | // console.log(cyan('Running yarn...')); 247 | // spawnSync('yarnpkg', [], { stdio: 'inherit' }); 248 | } else { 249 | console.log(cyan('Running npm install...')); 250 | spawnSync('npm', ['install', '--loglevel', 'error'], { 251 | stdio: 'inherit', 252 | }); 253 | } 254 | console.log(green('Ejected successfully!')); 255 | console.log(); 256 | 257 | console.log( 258 | green('Please consider sharing why you ejected in this survey:') 259 | ); 260 | console.log(green(' http://goo.gl/forms/Bi6CZjk1EqsdelXk1')); 261 | console.log(); 262 | }); 263 | -------------------------------------------------------------------------------- /scripts/init.js: -------------------------------------------------------------------------------- 1 | // @remove-file-on-eject 2 | /** 3 | * Copyright (c) 2015-present, Facebook, Inc. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 'use strict'; 9 | 10 | // Makes the script crash on unhandled rejections instead of silently 11 | // ignoring them. In the future, promise rejections that are not handled will 12 | // terminate the Node.js process with a non-zero exit code. 13 | process.on('unhandledRejection', err => { 14 | throw err; 15 | }); 16 | 17 | const fs = require('fs-extra'); 18 | const path = require('path'); 19 | const chalk = require('chalk'); 20 | const spawn = require('react-dev-utils/crossSpawn'); 21 | 22 | module.exports = function( 23 | appPath, 24 | appName, 25 | verbose, 26 | originalDirectory, 27 | template 28 | ) { 29 | const ownPackageName = require(path.join(__dirname, '..', 'package.json')) 30 | .name; 31 | const ownPath = path.join(appPath, 'node_modules', ownPackageName); 32 | const appPackage = require(path.join(appPath, 'package.json')); 33 | const useYarn = fs.existsSync(path.join(appPath, 'yarn.lock')); 34 | 35 | // Copy over some of the devDependencies 36 | appPackage.dependencies = appPackage.dependencies || {}; 37 | 38 | // Setup the script rules 39 | appPackage.scripts = { 40 | start: 'react-scripts start', 41 | build: 'react-scripts build', 42 | test: 'react-scripts test --env=jsdom', 43 | eject: 'react-scripts eject', 44 | }; 45 | 46 | fs.writeFileSync( 47 | path.join(appPath, 'package.json'), 48 | JSON.stringify(appPackage, null, 2) 49 | ); 50 | 51 | const readmeExists = fs.existsSync(path.join(appPath, 'README.md')); 52 | if (readmeExists) { 53 | fs.renameSync( 54 | path.join(appPath, 'README.md'), 55 | path.join(appPath, 'README.old.md') 56 | ); 57 | } 58 | 59 | // Copy the files for the user 60 | const templatePath = template 61 | ? path.resolve(originalDirectory, template) 62 | : path.join(ownPath, 'template'); 63 | if (fs.existsSync(templatePath)) { 64 | fs.copySync(templatePath, appPath); 65 | } else { 66 | console.error( 67 | `Could not locate supplied template: ${chalk.green(templatePath)}` 68 | ); 69 | return; 70 | } 71 | 72 | // Rename gitignore after the fact to prevent npm from renaming it to .npmignore 73 | // See: https://github.com/npm/npm/issues/1862 74 | fs.move( 75 | path.join(appPath, 'gitignore'), 76 | path.join(appPath, '.gitignore'), 77 | [], 78 | err => { 79 | if (err) { 80 | // Append if there's already a `.gitignore` file there 81 | if (err.code === 'EEXIST') { 82 | const data = fs.readFileSync(path.join(appPath, 'gitignore')); 83 | fs.appendFileSync(path.join(appPath, '.gitignore'), data); 84 | fs.unlinkSync(path.join(appPath, 'gitignore')); 85 | } else { 86 | throw err; 87 | } 88 | } 89 | } 90 | ); 91 | 92 | let command; 93 | let args; 94 | 95 | if (useYarn) { 96 | command = 'yarnpkg'; 97 | args = ['add']; 98 | } else { 99 | command = 'npm'; 100 | args = ['install', '--save', verbose && '--verbose'].filter(e => e); 101 | } 102 | args.push('react', 'react-dom'); 103 | 104 | // Install additional template dependencies, if present 105 | const templateDependenciesPath = path.join( 106 | appPath, 107 | '.template.dependencies.json' 108 | ); 109 | if (fs.existsSync(templateDependenciesPath)) { 110 | const templateDependencies = require(templateDependenciesPath).dependencies; 111 | args = args.concat( 112 | Object.keys(templateDependencies).map(key => { 113 | return `${key}@${templateDependencies[key]}`; 114 | }) 115 | ); 116 | fs.unlinkSync(templateDependenciesPath); 117 | } 118 | 119 | // Install react and react-dom for backward compatibility with old CRA cli 120 | // which doesn't install react and react-dom along with react-scripts 121 | // or template is presetend (via --internal-testing-template) 122 | if (!isReactInstalled(appPackage) || template) { 123 | console.log(`Installing react and react-dom using ${command}...`); 124 | console.log(); 125 | 126 | const proc = spawn.sync(command, args, { stdio: 'inherit' }); 127 | if (proc.status !== 0) { 128 | console.error(`\`${command} ${args.join(' ')}\` failed`); 129 | return; 130 | } 131 | } 132 | 133 | // Display the most elegant way to cd. 134 | // This needs to handle an undefined originalDirectory for 135 | // backward compatibility with old global-cli's. 136 | let cdpath; 137 | if (originalDirectory && path.join(originalDirectory, appName) === appPath) { 138 | cdpath = appName; 139 | } else { 140 | cdpath = appPath; 141 | } 142 | 143 | // Change displayed command to yarn instead of yarnpkg 144 | const displayedCommand = useYarn ? 'yarn' : 'npm'; 145 | 146 | console.log(); 147 | console.log(`Success! Created ${appName} at ${appPath}`); 148 | console.log('Inside that directory, you can run several commands:'); 149 | console.log(); 150 | console.log(chalk.cyan(` ${displayedCommand} start`)); 151 | console.log(' Starts the development server.'); 152 | console.log(); 153 | console.log( 154 | chalk.cyan(` ${displayedCommand} ${useYarn ? '' : 'run '}build`) 155 | ); 156 | console.log(' Bundles the app into static files for production.'); 157 | console.log(); 158 | console.log(chalk.cyan(` ${displayedCommand} test`)); 159 | console.log(' Starts the test runner.'); 160 | console.log(); 161 | console.log( 162 | chalk.cyan(` ${displayedCommand} ${useYarn ? '' : 'run '}eject`) 163 | ); 164 | console.log( 165 | ' Removes this tool and copies build dependencies, configuration files' 166 | ); 167 | console.log( 168 | ' and scripts into the app directory. If you do this, you can’t go back!' 169 | ); 170 | console.log(); 171 | console.log('We suggest that you begin by typing:'); 172 | console.log(); 173 | console.log(chalk.cyan(' cd'), cdpath); 174 | console.log(` ${chalk.cyan(`${displayedCommand} start`)}`); 175 | if (readmeExists) { 176 | console.log(); 177 | console.log( 178 | chalk.yellow( 179 | 'You had a `README.md` file, we renamed it to `README.old.md`' 180 | ) 181 | ); 182 | } 183 | console.log(); 184 | console.log('Happy hacking!'); 185 | }; 186 | 187 | function isReactInstalled(appPackage) { 188 | const dependencies = appPackage.dependencies || {}; 189 | 190 | return ( 191 | typeof dependencies.react !== 'undefined' && 192 | typeof dependencies['react-dom'] !== 'undefined' 193 | ); 194 | } 195 | -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | // @remove-on-eject-begin 2 | /** 3 | * Copyright (c) 2015-present, Facebook, Inc. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | // @remove-on-eject-end 9 | 'use strict'; 10 | 11 | // Do this as the first thing so that any code reading it knows the right env. 12 | process.env.BABEL_ENV = 'development'; 13 | process.env.NODE_ENV = 'development'; 14 | process.env.STATIC_ENV = 'development'; 15 | 16 | // Makes the script crash on unhandled rejections instead of silently 17 | // ignoring them. In the future, promise rejections that are not handled will 18 | // terminate the Node.js process with a non-zero exit code. 19 | process.on('unhandledRejection', err => { 20 | throw err; 21 | }); 22 | 23 | // Ensure environment variables are read. 24 | require('../config/env'); 25 | 26 | const fs = require('fs'); 27 | const chalk = require('chalk'); 28 | const webpack = require('webpack'); 29 | const WebpackDevServer = require('webpack-dev-server'); 30 | const clearConsole = require('react-dev-utils/clearConsole'); 31 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 32 | const { 33 | choosePort, 34 | createCompiler, 35 | prepareProxy, 36 | prepareUrls, 37 | } = require('react-dev-utils/WebpackDevServerUtils'); 38 | const openBrowser = require('react-dev-utils/openBrowser'); 39 | const paths = require('../config/paths'); 40 | const config = require('../config/webpack.config.dev'); 41 | const createDevServerConfig = require('../config/webpackDevServer.config'); 42 | 43 | const useYarn = fs.existsSync(paths.yarnLockFile); 44 | const isInteractive = process.stdout.isTTY; 45 | 46 | // Warn and crash if required files are missing 47 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 48 | process.exit(1); 49 | } 50 | 51 | // Tools like Cloud9 rely on this. 52 | const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; 53 | const HOST = process.env.HOST || '0.0.0.0'; 54 | 55 | // We attempt to use the default port but if it is busy, we offer the user to 56 | // run on a different port. `detect()` Promise resolves to the next free port. 57 | choosePort(HOST, DEFAULT_PORT) 58 | .then(port => { 59 | if (port == null) { 60 | // We have not found a port. 61 | return; 62 | } 63 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; 64 | const appName = require(paths.appPackageJson).name; 65 | const urls = prepareUrls(protocol, HOST, port); 66 | // Create a webpack compiler that is configured with custom messages. 67 | const compiler = createCompiler(webpack, config, appName, urls, useYarn); 68 | // Load proxy config 69 | const proxySetting = require(paths.appPackageJson).proxy; 70 | const proxyConfig = prepareProxy(proxySetting, paths.appPublic); 71 | // Serve webpack assets generated by the compiler over a web sever. 72 | const serverConfig = createDevServerConfig( 73 | proxyConfig, 74 | urls.lanUrlForConfig 75 | ); 76 | const devServer = new WebpackDevServer(compiler, serverConfig); 77 | // Launch WebpackDevServer. 78 | devServer.listen(port, HOST, err => { 79 | if (err) { 80 | return console.log(err); 81 | } 82 | if (isInteractive) { 83 | clearConsole(); 84 | } 85 | console.log(chalk.cyan('Starting the development server...\n')); 86 | openBrowser(urls.localUrlForBrowser); 87 | }); 88 | 89 | ['SIGINT', 'SIGTERM'].forEach(function(sig) { 90 | process.on(sig, function() { 91 | devServer.close(); 92 | process.exit(); 93 | }); 94 | }); 95 | }) 96 | .catch(err => { 97 | if (err && err.message) { 98 | console.log(err.message); 99 | } 100 | process.exit(1); 101 | }); 102 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | // @remove-on-eject-begin 2 | /** 3 | * Copyright (c) 2015-present, Facebook, Inc. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | // @remove-on-eject-end 9 | 'use strict'; 10 | 11 | // Do this as the first thing so that any code reading it knows the right env. 12 | process.env.BABEL_ENV = 'test'; 13 | process.env.NODE_ENV = 'test'; 14 | process.env.PUBLIC_URL = ''; 15 | 16 | // Makes the script crash on unhandled rejections instead of silently 17 | // ignoring them. In the future, promise rejections that are not handled will 18 | // terminate the Node.js process with a non-zero exit code. 19 | process.on('unhandledRejection', err => { 20 | throw err; 21 | }); 22 | 23 | // Ensure environment variables are read. 24 | require('../config/env'); 25 | 26 | const jest = require('jest'); 27 | const argv = process.argv.slice(2); 28 | 29 | // Watch unless on CI or in coverage mode 30 | if (!process.env.CI && argv.indexOf('--coverage') < 0) { 31 | argv.push('--watch'); 32 | } 33 | 34 | // @remove-on-eject-begin 35 | // This is not necessary after eject because we embed config into package.json. 36 | const createJestConfig = require('./utils/createJestConfig'); 37 | const path = require('path'); 38 | const paths = require('../config/paths'); 39 | argv.push( 40 | '--config', 41 | JSON.stringify( 42 | createJestConfig( 43 | relativePath => path.resolve(__dirname, '..', relativePath), 44 | path.resolve(paths.appSrc, '..'), 45 | false 46 | ) 47 | ) 48 | ); 49 | // @remove-on-eject-end 50 | jest.run(argv); 51 | -------------------------------------------------------------------------------- /scripts/utils/createJestConfig.js: -------------------------------------------------------------------------------- 1 | // @remove-file-on-eject 2 | /** 3 | * Copyright (c) 2015-present, Facebook, Inc. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 'use strict'; 9 | 10 | const fs = require('fs'); 11 | const chalk = require('chalk'); 12 | const paths = require('../../config/paths'); 13 | 14 | module.exports = (resolve, rootDir, isEjecting) => { 15 | // Use this instead of `paths.testsSetup` to avoid putting 16 | // an absolute filename into configuration after ejecting. 17 | const setupTestsFile = fs.existsSync(paths.testsSetup) 18 | ? '/src/setupTests.js' 19 | : undefined; 20 | 21 | // TODO: I don't know if it's safe or not to just use / as path separator 22 | // in Jest configs. We need help from somebody with Windows to determine this. 23 | const config = { 24 | collectCoverageFrom: ['src/**/*.{js,jsx,mjs}'], 25 | setupFiles: [resolve('config/polyfills.js')], 26 | setupTestFrameworkScriptFile: setupTestsFile, 27 | testMatch: [ 28 | '/src/**/__tests__/**/*.{js,jsx,mjs}', 29 | '/src/**/?(*.)(spec|test).{js,jsx,mjs}', 30 | ], 31 | testEnvironment: 'node', 32 | testURL: 'http://localhost', 33 | transform: { 34 | '^.+\\.(js|jsx|mjs)$': isEjecting 35 | ? '/node_modules/babel-jest' 36 | : resolve('config/jest/babelTransform.js'), 37 | '^.+\\.css$': resolve('config/jest/cssTransform.js'), 38 | '^(?!.*\\.(js|jsx|mjs|css|json)$)': resolve( 39 | 'config/jest/fileTransform.js' 40 | ), 41 | }, 42 | transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs)$'], 43 | moduleNameMapper: { 44 | '^react-native$': 'react-native-web', 45 | }, 46 | moduleFileExtensions: [ 47 | 'web.js', 48 | 'mjs', 49 | 'js', 50 | 'json', 51 | 'web.jsx', 52 | 'jsx', 53 | 'node', 54 | ], 55 | }; 56 | if (rootDir) { 57 | config.rootDir = rootDir; 58 | } 59 | const overrides = Object.assign({}, require(paths.appPackageJson).jest); 60 | const supportedKeys = [ 61 | 'collectCoverageFrom', 62 | 'coverageReporters', 63 | 'coverageThreshold', 64 | 'snapshotSerializers', 65 | ]; 66 | if (overrides) { 67 | supportedKeys.forEach(key => { 68 | if (overrides.hasOwnProperty(key)) { 69 | config[key] = overrides[key]; 70 | delete overrides[key]; 71 | } 72 | }); 73 | const unsupportedKeys = Object.keys(overrides); 74 | if (unsupportedKeys.length) { 75 | console.error( 76 | chalk.red( 77 | 'Out of the box, Create React App only supports overriding ' + 78 | 'these Jest options:\n\n' + 79 | supportedKeys.map(key => chalk.bold(' \u2022 ' + key)).join('\n') + 80 | '.\n\n' + 81 | 'These options in your package.json Jest configuration ' + 82 | 'are not currently supported by Create React App:\n\n' + 83 | unsupportedKeys 84 | .map(key => chalk.bold(' \u2022 ' + key)) 85 | .join('\n') + 86 | '\n\nIf you wish to override other Jest options, you need to ' + 87 | 'eject from the default setup. You can do so by running ' + 88 | chalk.bold('npm run eject') + 89 | ' but remember that this is a one-way operation. ' + 90 | 'You may also file an issue with Create React App to discuss ' + 91 | 'supporting more options out of the box.\n' 92 | ) 93 | ); 94 | process.exit(1); 95 | } 96 | } 97 | return config; 98 | }; 99 | -------------------------------------------------------------------------------- /src/api/api.js: -------------------------------------------------------------------------------- 1 | import Server from './server'; 2 | 3 | class API extends Server{ 4 | /** 5 | * 用途:上传图片 6 | * @url https://elm.cangdu.org/v1/addimg/shop 7 | * 返回status为1表示成功 8 | * @method post 9 | * @return {promise} 10 | */ 11 | async uploadImg(params = {}){ 12 | try{ 13 | let result = await this.axios('post', '//elm.cangdu.org/v1/addimg/shop', params); 14 | if(result && result.status === 1){ 15 | return result; 16 | }else{ 17 | let err = { 18 | tip: '上传图片失败', 19 | response: result, 20 | data: params, 21 | url: '//elm.cangdu.org/v1/addimg/shop', 22 | } 23 | throw err; 24 | } 25 | }catch(err){ 26 | throw err; 27 | } 28 | } 29 | 30 | /** 31 | * 用途:获取记录数据 32 | * @url https://api.cangdu.org/shopro/data/record 33 | * 返回http_code为200表示成功 34 | * @method get 35 | * @return {promise} 36 | */ 37 | async getRecord(params = {}){ 38 | try{ 39 | let result = await this.axios('get', `/shopro/data/record/${params.type}`); 40 | if(result && (result.data instanceof Object) && result.http_code === 200){ 41 | return result.data; 42 | }else{ 43 | let err = { 44 | tip: '获取记录数据失败', 45 | response: result, 46 | data: params, 47 | url: 'https://api.cangdu.org/shopro/data/record', 48 | } 49 | throw err; 50 | } 51 | }catch(err){ 52 | throw err; 53 | } 54 | } 55 | 56 | /** 57 | * 用途:获取商品数据 58 | * @url https://api.cangdu.org/shopro/data/products 59 | * 返回http_code为200表示成功 60 | * @method get 61 | * @return {promise} 62 | */ 63 | async getProduction(params = {}){ 64 | try{ 65 | let result = await this.axios('get', '/shopro/data/products', params); 66 | if(result && (result.data instanceof Object) && result.http_code === 200){ 67 | return result.data.data||[]; 68 | }else{ 69 | let err = { 70 | tip: '获取商品数据失败', 71 | response: result, 72 | data: params, 73 | url: 'https://api.cangdu.org/shopro/data/products', 74 | } 75 | throw err; 76 | } 77 | }catch(err){ 78 | throw err; 79 | } 80 | } 81 | 82 | /** 83 | * 用途:获取佣金数据 84 | * @url https://api.cangdu.org/shopro/data/balance 85 | * 返回http_code为200表示成功 86 | * @method get 87 | * @return {promise} 88 | */ 89 | async getBalance(params = {}){ 90 | try{ 91 | let result = await this.axios('get', '/shopro/data/balance', params); 92 | if(result && (result.data instanceof Object) && result.http_code === 200){ 93 | return result.data.data||{}; 94 | }else{ 95 | let err = { 96 | tip: '获取佣金数据失败', 97 | response: result, 98 | data: params, 99 | url: 'https://api.cangdu.org/shopro/data/balance', 100 | } 101 | throw err; 102 | } 103 | }catch(err){ 104 | throw err; 105 | } 106 | } 107 | } 108 | 109 | export default new API(); -------------------------------------------------------------------------------- /src/api/server.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import envconfig from '@/envconfig/envconfig'; 3 | /** 4 | * 主要params参数 5 | * @params method {string} 方法名 6 | * @params url {string} 请求地址 例如:/login 配合baseURL组成完整请求地址 7 | * @params baseURL {string} 请求地址统一前缀 ***需要提前指定*** 例如:http://cangdu.org 8 | * @params timeout {number} 请求超时时间 默认 30000 9 | * @params params {object} get方式传参key值 10 | * @params headers {string} 指定请求头信息 11 | * @params withCredentials {boolean} 请求是否携带本地cookies信息默认开启 12 | * @params validateStatus {func} 默认判断请求成功的范围 200 - 300 13 | * @return {Promise} 14 | * 其他更多拓展参看axios文档后 自行拓展 15 | * 注意:params中的数据会覆盖method url 参数,所以如果指定了这2个参数则不需要在params中带入 16 | */ 17 | 18 | export default class Server { 19 | axios(method, url, params){ 20 | return new Promise((resolve, reject) => { 21 | if(typeof params !== 'object') params = {}; 22 | let _option = params; 23 | _option = { 24 | method, 25 | url, 26 | baseURL: envconfig.baseURL, 27 | timeout: 30000, 28 | params: null, 29 | data: null, 30 | headers: null, 31 | withCredentials: true, //是否携带cookies发起请求 32 | validateStatus:(status)=>{ 33 | return status >= 200 && status < 300; 34 | }, 35 | ...params, 36 | } 37 | axios.request(_option).then(res => { 38 | resolve(typeof res.data === 'object' ? res.data : JSON.parse(res.data)) 39 | },error => { 40 | if(error.response){ 41 | reject(error.response.data) 42 | }else{ 43 | reject(error) 44 | } 45 | }) 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/assets/iconfonts/iconfont.css: -------------------------------------------------------------------------------- 1 | 2 | @font-face {font-family: "iconfont"; 3 | src: url('iconfont.eot?t=1515590359802'); /* IE9*/ 4 | src: url('iconfont.eot?t=1515590359802#iefix') format('embedded-opentype'), /* IE6-IE8 */ 5 | url('data:application/x-font-woff;charset=utf-8;base64,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') format('woff'), 6 | url('iconfont.ttf?t=1515590359802') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ 7 | url('iconfont.svg?t=1515590359802#iconfont') format('svg'); /* iOS 4.1- */ 8 | } 9 | 10 | .iconfont { 11 | font-family:"iconfont" !important; 12 | font-size:16px; 13 | font-style:normal; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | } 17 | 18 | .icon-yinliangda:before { content: "\e600"; } 19 | 20 | .icon-yinliangxiao:before { content: "\e602"; } 21 | 22 | .icon-sanjiaoxing:before { content: "\e656"; } 23 | 24 | .icon-xuanze:before { content: "\e636"; } 25 | 26 | .icon-xuanze1:before { content: "\e696"; } 27 | 28 | .icon-zanting:before { content: "\e672"; } 29 | 30 | .icon-jiantou-copy-copy:before { content: "\e679"; } 31 | 32 | .icon-catalog:before { content: "\e716"; } 33 | 34 | .icon-jingyin:before { content: "\e674"; } 35 | 36 | .icon-quanping:before { content: "\e601"; } 37 | 38 | .icon-jilu:before { content: "\e8d7"; } 39 | 40 | .icon-jian:before { content: "\e711"; } 41 | 42 | .icon-yinliang:before { content: "\ea1b"; } 43 | 44 | .icon-tuichuquanping:before { content: "\e60d"; } 45 | 46 | .icon-icon-test:before { content: "\e610"; } 47 | 48 | .icon-jia:before { content: "\e6d9"; } 49 | 50 | .icon-guanbi:before { content: "\e624"; } 51 | 52 | -------------------------------------------------------------------------------- /src/assets/iconfonts/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bailicangdu/react-pxq/553e42f41b9ed4ad0d30216aedf7e3fb7c06fc3e/src/assets/iconfonts/iconfont.eot -------------------------------------------------------------------------------- /src/assets/iconfonts/iconfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Created by iconfont 9 | 10 | 11 | 12 | 13 | 21 | 22 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/assets/iconfonts/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bailicangdu/react-pxq/553e42f41b9ed4ad0d30216aedf7e3fb7c06fc3e/src/assets/iconfonts/iconfont.ttf -------------------------------------------------------------------------------- /src/assets/iconfonts/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bailicangdu/react-pxq/553e42f41b9ed4ad0d30216aedf7e3fb7c06fc3e/src/assets/iconfonts/iconfont.woff -------------------------------------------------------------------------------- /src/components/TouchableOpacity/TouchableOpacity.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { is, fromJS } from 'immutable'; 4 | import './TouchableOpacity.less'; 5 | /** 6 | * 点击状态组件 7 | */ 8 | export default class TouchableOpacity extends Component{ 9 | static propTypes = { 10 | clickCallBack: PropTypes.func, 11 | text: PropTypes.string, 12 | className: PropTypes.string, 13 | } 14 | 15 | handleTouchStart = () => { 16 | this.refs.btn.style.opacity = '0.3'; 17 | } 18 | 19 | handleTouchEnd = () => { 20 | this.refs.btn.style.opacity = '1'; 21 | this.props.clickCallBack(); 22 | } 23 | 24 | shouldComponentUpdate(nextProps, nextState){ 25 | return !is(fromJS(this.props), fromJS(nextProps)) || !is(fromJS(this.state), fromJS(nextState)) 26 | } 27 | 28 | render(){ 29 | return ( 30 |
{this.props.text||'确认'}
31 | ); 32 | } 33 | } -------------------------------------------------------------------------------- /src/components/TouchableOpacity/TouchableOpacity.less: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: cangdu 3 | * @Date: 2018-05-10 19:03:13 4 | * @Last Modified by: cangdu 5 | * @Last Modified time: 2018-05-10 19:03:13 6 | */ 7 | -------------------------------------------------------------------------------- /src/components/alert/alert.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { is, fromJS } from 'immutable'; 4 | import TouchableOpacity from '@/components/TouchableOpacity/TouchableOpacity'; 5 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; 6 | import './alert.less'; 7 | 8 | export default class Alert extends Component{ 9 | static propTypes = { 10 | closeAlert: PropTypes.func.isRequired, 11 | alertTip: PropTypes.string.isRequired, 12 | alertStatus: PropTypes.bool.isRequired, 13 | } 14 | // css动画组件设置为目标组件 15 | FirstChild = props => { 16 | const childrenArray = React.Children.toArray(props.children); 17 | return childrenArray[0] || null; 18 | } 19 | // 关闭弹框 20 | confirm = () => { 21 | this.props.closeAlert(); 22 | } 23 | 24 | shouldComponentUpdate(nextProps, nextState){ 25 | return !is(fromJS(this.props), fromJS(nextProps)) || !is(fromJS(this.state), fromJS(nextState)) 26 | } 27 | 28 | render(){ 29 | return ( 30 | 35 | { 36 | this.props.alertStatus&&
37 |
38 |
{this.props.alertTip}
39 | 40 |
41 |
42 | } 43 |
44 | ); 45 | } 46 | } -------------------------------------------------------------------------------- /src/components/alert/alert.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../../style/mixin.less"; 2 | 3 | .alert-con{ 4 | .coverScreen(fixed, rgba(0, 0, 0, 0.3)); 5 | z-index: 11; 6 | } 7 | .alert-context{ 8 | .positionCenter; 9 | z-index: 12; 10 | .wh(600px, 360px); 11 | background-color: #fff; 12 | border-radius: 16px; 13 | .flex(@align-items: stretch); 14 | flex-direction: column; 15 | .alert-content-detail{ 16 | flex: 5; 17 | .flex; 18 | .font(40px); 19 | } 20 | .confirm-btn{ 21 | flex: 2; 22 | .flex; 23 | .font(40px); 24 | border-top: 1PX solid #eee; 25 | } 26 | } 27 | .alert-enter{ 28 | opacity: 0; 29 | } 30 | .alert-enter.alert-enter-active{ 31 | transition: all 300ms; 32 | opacity: 1; 33 | } 34 | 35 | .alert-leave{ 36 | opacity: 1; 37 | } 38 | .alert-leave.alert-leave-active{ 39 | transition: all 300ms; 40 | opacity: 0; 41 | } -------------------------------------------------------------------------------- /src/components/header/header.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { is, fromJS } from 'immutable'; 3 | import { NavLink } from 'react-router-dom'; 4 | import PropTypes from 'prop-types'; 5 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; 6 | import './header.less'; 7 | 8 | export default class PublicHeader extends Component{ 9 | static propTypes = { 10 | record: PropTypes.any, 11 | title: PropTypes.string.isRequired, 12 | confirm: PropTypes.any, 13 | } 14 | 15 | state = { 16 | navState: false, //导航栏是否显示 17 | }; 18 | 19 | // 切换左侧导航栏状态 20 | toggleNav = () => { 21 | this.setState({navState: !this.state.navState}); 22 | } 23 | // css动画组件设置为目标组件 24 | FirstChild = props => { 25 | const childrenArray = React.Children.toArray(props.children); 26 | return childrenArray[0] || null; 27 | } 28 | shouldComponentUpdate(nextProps, nextState) { 29 | return !is(fromJS(this.props), fromJS(nextProps))|| !is(fromJS(this.state),fromJS(nextState)) 30 | } 31 | 32 | render(){ 33 | return( 34 |
35 | 36 | {this.props.title} 37 | { 38 | this.props.record&& 39 | } 40 | { 41 | this.props.confirm&&确定 42 | } 43 | 48 | { 49 | this.state.navState && 54 | } 55 | 56 | 57 |
58 | ); 59 | } 60 | } -------------------------------------------------------------------------------- /src/components/header/header.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../../style/mixin.less"; 2 | @import "../../assets/iconfonts/iconfont.css"; 3 | 4 | .header-container{ 5 | background-color: @base-color; 6 | .flex; 7 | position: fixed; 8 | top: 0; 9 | left: 0; 10 | z-index: 10; 11 | .wh(100%, 88px); 12 | .header-slide-icon{ 13 | .font(36px, #fff); 14 | .positionLeft; 15 | } 16 | .header-title{ 17 | .font(40px, #fff); 18 | } 19 | .header-link{ 20 | .font(40px, #fff); 21 | .positionRight; 22 | } 23 | .header-link-confim{ 24 | font-size: 32px; 25 | } 26 | .nav-slide-list{ 27 | .coverScreen(fixed); 28 | top: 88px; 29 | display: flex; 30 | flex-direction: column; 31 | .nav-link{ 32 | .font(32px); 33 | border-bottom: 1Px solid #ddd; 34 | margin: 0 @common-wd; 35 | padding: 35px 20px; 36 | position: relative; 37 | &:before{ 38 | position: absolute; 39 | right: 10px; 40 | top: 50%; 41 | transform: translate3d(0, -50%, 0); 42 | color: #666; 43 | }; 44 | } 45 | } 46 | .nav-enter{ 47 | transform: translate3d(-100%, 0, 0); 48 | } 49 | 50 | .nav-enter.nav-enter-active{ 51 | transform: translate3d(0, 0, 0); 52 | transition: transform 300ms; 53 | } 54 | 55 | .nav-leave{ 56 | transform: translate3d(0, 0, 0); 57 | } 58 | 59 | .nav-leave.nav-leave-active{ 60 | transform: translate3d(-100%, 0, 0); 61 | transition: transform 300ms; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/envconfig/envconfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 全局配置文件 3 | */ 4 | let baseURL; 5 | let imgUrl = '//elm.cangdu.org/img/'; 6 | if(process.env.NODE_ENV === 'development'){ 7 | baseURL = '//api.cangdu.org'; 8 | }else{ 9 | baseURL = '//api.cangdu.org'; 10 | } 11 | 12 | 13 | export default {imgUrl, baseURL} -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Route from './router/'; 4 | import FastClick from 'fastclick'; 5 | import registerServiceWorker from './registerServiceWorker'; 6 | import { AppContainer } from 'react-hot-loader'; 7 | import {Provider} from 'react-redux'; 8 | import store from '@/store/store'; 9 | import './utils/setRem'; 10 | import './style/base.css'; 11 | 12 | FastClick.attach(document.body); 13 | 14 | // 监听state变化 15 | // store.subscribe(() => { 16 | // console.log('store发生了变化'); 17 | // }); 18 | 19 | const render = Component => { 20 | ReactDOM.render( 21 | //绑定redux、热加载 22 | 23 | 24 | 25 | 26 | , 27 | document.getElementById('root'), 28 | ) 29 | } 30 | 31 | render(Route); 32 | 33 | // Webpack Hot Module Replacement API 34 | if (module.hot) { 35 | module.hot.accept('./router/', () => { 36 | render(Route); 37 | }) 38 | } 39 | 40 | registerServiceWorker(); 41 | -------------------------------------------------------------------------------- /src/pages/balance/balance.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { is, fromJS } from 'immutable'; 3 | import PublicHeader from '@/components/header/header'; 4 | import TouchableOpacity from '@/components/TouchableOpacity/TouchableOpacity'; 5 | import PublicAlert from '@/components/alert/alert'; 6 | import API from '@/api/api'; 7 | import './balance.less'; 8 | 9 | class BrokeRage extends Component{ 10 | state = { 11 | applyNum: '', //输入值 12 | alertStatus: false, //弹框状态 13 | alertTip: '', //弹框提示文字 14 | balance: { //可提现金额 15 | balance: 0, 16 | }, 17 | } 18 | // 初始化数据 19 | initData = async () => { 20 | try{ 21 | let result = await API.getBalance(); 22 | console.log(result); 23 | this.setState({balance: result}); 24 | }catch(err){ 25 | console.error(err); 26 | } 27 | } 28 | 29 | /** 30 | * 格式化输入数据 31 | * 格式为微信红包格式:最大 200.00 32 | * @param {object} event 事件对象 33 | */ 34 | handleInput = event => { 35 | let value = event.target.value; 36 | if((/^\d*?\.?\d{0,2}?$/gi).test(value)){ 37 | if((/^0+[1-9]+/).test(value)) { 38 | value = value.replace(/^0+/,''); 39 | } 40 | if((/^0{2}\./).test(value)) { 41 | value = value.replace(/^0+/,'0'); 42 | } 43 | value = value.replace(/^\./gi,'0.'); 44 | if(parseFloat(value) > 200){ 45 | value = '200.00'; 46 | } 47 | this.setState({applyNum: value}); 48 | } 49 | } 50 | 51 | /** 52 | * 提交判断条件 53 | */ 54 | sumitForm = () => { 55 | let alertTip; 56 | if(!this.state.applyNum){ 57 | alertTip = '请输入提现金额'; 58 | }else if(parseFloat(this.state.applyNum) > this.state.balance.balance){ 59 | alertTip = '申请提现金额不能大于余额'; 60 | }else{ 61 | alertTip = '申请提现成功'; 62 | } 63 | 64 | this.setState({ 65 | alertStatus: true, 66 | alertTip, 67 | applyNum: '', 68 | }) 69 | } 70 | 71 | /* 72 | 关闭弹框 73 | */ 74 | closeAlert = () => { 75 | this.setState({ 76 | alertStatus: false, 77 | alertTip: '', 78 | }) 79 | } 80 | 81 | shouldComponentUpdate(nextProps, nextState) { 82 | return !is(fromJS(this.props), fromJS(nextProps)) || !is(fromJS(this.state),fromJS(nextState)) 83 | } 84 | 85 | componentDidMount(){ 86 | this.initData(); 87 | } 88 | 89 | render(){ 90 | return ( 91 |
92 | 93 |
94 |

您的可提现金额为:¥ {this.state.balance.balance}

95 |
96 |

请输入提现金额(元)

97 |

¥

98 |
99 | 100 |
101 | 102 |
103 | ); 104 | } 105 | } 106 | 107 | export default BrokeRage; 108 | -------------------------------------------------------------------------------- /src/pages/balance/balance.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../../style/mixin.less"; 2 | 3 | 4 | .broke-main-content{ 5 | padding-top: @common-wd; 6 | .broke-header{ 7 | .font(32px, #999); 8 | padding: 0 @common-wd; 9 | } 10 | .broke-form{ 11 | background-color: #fff; 12 | min-height: 250px; 13 | margin-top: @common-wd; 14 | .flex; 15 | flex-direction: column; 16 | p:nth-of-type(1){ 17 | .font(40px); 18 | } 19 | p:nth-of-type(2){ 20 | .font(40px); 21 | ::-webkit-input-placeholder{ 22 | color: #ccc; 23 | } 24 | margin-top: @common-wd; 25 | input{ 26 | border: none; 27 | .font(40px); 28 | width: 180px; 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/pages/helpcenter/helpcenter.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PublicHeader from '@/components/header/header'; 3 | import { is, fromJS } from 'immutable'; 4 | import './helpcenter.less'; 5 | 6 | export default class HelpCenter extends Component { 7 | 8 | shouldComponentUpdate(nextProps, nextState){ 9 | return !is(fromJS(this.props), fromJS(nextProps)) || !is(fromJS(this.state), fromJS(nextState)) 10 | } 11 | 12 | render(){ 13 | return ( 14 |
15 | 16 |
17 |

介绍

18 |

本项目主要用于理解 react 和 redux 的编译方式,以及 react + redux 之间的配合方式

19 |

技术要点

20 |

react:v16.2

21 |

redux:v3.7

22 |

webpack:v3.8

23 |

react-router:v4.2

24 |

ES 6/7/8

25 |

code split

26 |

hot loader

27 |

axios:v0.17

28 |

less:v2.7

29 |

immutable:v3.8

30 |

项目地址 github

31 |
32 |
33 | ) 34 | } 35 | } -------------------------------------------------------------------------------- /src/pages/helpcenter/helpcenter.less: -------------------------------------------------------------------------------- 1 | .context-con{ 2 | margin-top: 100px; 3 | padding: 20px; 4 | h2{ 5 | font-size: 45px; 6 | text-align: center; 7 | margin-bottom: 10px; 8 | padding-top: 10px; 9 | } 10 | p{ 11 | font-size: 32px; 12 | text-indent: 40px; 13 | margin-bottom: 20px; 14 | } 15 | } -------------------------------------------------------------------------------- /src/pages/home/home.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { connect } from 'react-redux'; 4 | import { is, fromJS } from 'immutable'; 5 | import PropTypes from 'prop-types'; 6 | import API from '@/api/api'; 7 | import envconfig from '@/envconfig/envconfig'; 8 | import { saveFormData, saveImg, clearData } from '@/store/home/action'; 9 | import { clearSelected } from '@/store/production/action'; 10 | import PublicHeader from '@/components/header/header'; 11 | import PublicAlert from '@/components/alert/alert'; 12 | import TouchableOpacity from '@/components/TouchableOpacity/TouchableOpacity'; 13 | import mixin, { padStr } from '@/utils/mixin'; 14 | import './home.less'; 15 | 16 | @mixin({padStr}) 17 | class Home extends Component { 18 | static propTypes = { 19 | formData: PropTypes.object.isRequired, 20 | saveFormData: PropTypes.func.isRequired, 21 | saveImg: PropTypes.func.isRequired, 22 | clearData: PropTypes.func.isRequired, 23 | clearSelected: PropTypes.func.isRequired, 24 | } 25 | 26 | state = { 27 | alertStatus: false, //弹框状态 28 | alertTip: '', //弹框提示文字 29 | } 30 | /** 31 | * 已选择的商品数据 32 | * @type {Array} 33 | */ 34 | selectedProList = []; 35 | 36 | /** 37 | * 将表单数据保存至redux,保留状态 38 | * @param {string} type 数据类型 orderSum||name||phoneNo 39 | * @param {object} event 事件对象 40 | */ 41 | handleInput = (type, event) => { 42 | let value = event.target.value; 43 | switch(type){ 44 | case 'orderSum': 45 | value = value.replace(/\D/g, ''); 46 | break; 47 | case 'name': 48 | break; 49 | case 'phoneNo': 50 | value = this.padStr(value.replace(/\D/g, ''), [3, 7], ' ', event.target); 51 | break; 52 | default:; 53 | } 54 | this.props.saveFormData(value, type); 55 | } 56 | 57 | /* 58 | 上传图片,并将图片地址存到redux,保留状态 59 | */ 60 | uploadImg = async event => { 61 | try{ 62 | let formdata = new FormData(); 63 | formdata.append('file', event.target.files[0]); 64 | let result = await API.uploadImg({data: formdata}); 65 | this.props.saveImg(envconfig.imgUrl + result.image_path); 66 | console.log(result); 67 | }catch(err){ 68 | console.error(err); 69 | } 70 | } 71 | 72 | // 提交表单 73 | sumitForm = () => { 74 | const {orderSum, name, phoneNo} = this.props.formData; 75 | let alertTip = ''; 76 | if(!orderSum.toString().length){ 77 | alertTip = '请填写金额'; 78 | }else if(!name.toString().length){ 79 | alertTip = '请填写姓名'; 80 | }else if(!phoneNo.toString().length){ 81 | alertTip = '请填写正确的手机号'; 82 | }else{ 83 | alertTip = '添加数据成功'; 84 | this.props.clearSelected(); 85 | this.props.clearData(); 86 | } 87 | this.setState({ 88 | alertStatus: true, 89 | alertTip, 90 | }) 91 | } 92 | 93 | // 关闭弹款 94 | closeAlert = () => { 95 | this.setState({ 96 | alertStatus: false, 97 | alertTip: '', 98 | }) 99 | } 100 | 101 | // 初始化数据,获取已选择的商品 102 | initData = props => { 103 | this.selectedProList = []; 104 | props.proData.dataList.forEach(item => { 105 | if(item.selectStatus && item.selectNum){ 106 | this.selectedProList.push(item); 107 | } 108 | }) 109 | } 110 | 111 | componentWillReceiveProps(nextProps){ 112 | if(!is(fromJS(this.props.proData), fromJS(nextProps.proData))){ 113 | this.initData(nextProps); 114 | } 115 | } 116 | 117 | shouldComponentUpdate(nextProps, nextState) { 118 | return !is(fromJS(this.props), fromJS(nextProps)) || !is(fromJS(this.state),fromJS(nextState)) 119 | } 120 | 121 | componentWillMount(){ 122 | this.initData(this.props); 123 | } 124 | 125 | 126 | render() { 127 | 128 | return ( 129 |
130 | 131 |

请录入您的信息

132 |
133 |
134 | 销售金额: 135 | 136 |
137 |
138 | 客户姓名: 139 | 140 |
141 |
142 | 客户电话: 143 | 144 |
145 |
146 |
147 |

请选择销售的产品

148 | 149 | { 150 | this.selectedProList.length ?
    151 | { 152 | this.selectedProList.map((item, index) => { 153 | return
  • {item.product_name}x{item.selectNum}
  • 154 | }) 155 | } 156 |
:'选择产品' 157 | } 158 | 159 |
160 |
161 |

请上传发票凭证

162 |
163 | 上传图片 164 | 165 |
166 | 167 |
168 | 169 | 170 |
171 | ); 172 | } 173 | } 174 | 175 | export default connect(state => ({ 176 | formData: state.formData, 177 | proData: state.proData, 178 | }), { 179 | saveFormData, 180 | saveImg, 181 | clearData, 182 | clearSelected, 183 | })(Home); 184 | -------------------------------------------------------------------------------- /src/pages/home/home.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../../style/mixin.less"; 2 | 3 | .home-container{ 4 | padding-top: 88px; 5 | .common-title{ 6 | .font(36px, @base-color); 7 | padding: 30px; 8 | } 9 | .home-form{ 10 | background-color: #fff; 11 | .home-form-tiem{ 12 | height: 120px; 13 | border-bottom: 1PX solid #eee; 14 | span{ 15 | .font(32px, #555); 16 | margin-left: 30px; 17 | margin-right: 20px; 18 | } 19 | input{ 20 | border: none; 21 | font-size: 30px; 22 | } 23 | ::-webkit-input-placeholder{ 24 | color: #ccc; 25 | } 26 | } 27 | } 28 | .common-select-btn{ 29 | background-color: #fff; 30 | display: block; 31 | min-height: 120px; 32 | .font(42px, #ccc, center); 33 | .flex; 34 | .selected-pro-list{ 35 | .flex(); 36 | flex-wrap: wrap; 37 | .selected-pro-item{ 38 | width: 340px; 39 | margin: 10px; 40 | line-height: 50px; 41 | .font(32px, @text-align:center); 42 | } 43 | } 44 | } 45 | .upload-img-con{ 46 | text-align: center; 47 | .file-lable{ 48 | position: relative; 49 | input[type='file']{ 50 | position: absolute; 51 | .wh(100%, 100%); 52 | background-color: red; 53 | top: 0; 54 | right: 0; 55 | opacity: 0; 56 | } 57 | } 58 | .select-img{ 59 | margin-top: 10px; 60 | } 61 | } 62 | 63 | .submit-btn{ 64 | width: 90%; 65 | background-color: @base-color; 66 | margin: 50px auto 0; 67 | border-radius: 10px; 68 | line-height: 100px; 69 | .font(40px, #fff, center); 70 | } 71 | } -------------------------------------------------------------------------------- /src/pages/production/production.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { is, fromJS } from 'immutable'; 3 | import { connect } from 'react-redux'; 4 | import { getProData, togSelectPro, editPro } from '@/store/production/action'; 5 | import PropTypes from 'prop-types'; 6 | import PublicHeader from '@/components/header/header'; 7 | import './production.less'; 8 | 9 | class Production extends Component{ 10 | static propTypes = { 11 | proData: PropTypes.object.isRequired, 12 | getProData: PropTypes.func.isRequired, 13 | togSelectPro: PropTypes.func.isRequired, 14 | editPro: PropTypes.func.isRequired, 15 | } 16 | 17 | /** 18 | * 添加或删减商品,交由redux进行数据处理,作为全局变量 19 | * @param {int} index 编辑的商品索引 20 | * @param {int} num 添加||删减的商品数量 21 | */ 22 | handleEdit = (index, num) => { 23 | let currentNum = this.props.proData.dataList[index].selectNum + num; 24 | if(currentNum < 0){ 25 | return 26 | } 27 | this.props.editPro(index, currentNum); 28 | } 29 | 30 | // 选择商品,交由redux进行数据处理,作为全局变量 31 | togSelect = index => { 32 | this.props.togSelectPro(index); 33 | } 34 | 35 | shouldComponentUpdate(nextProps, nextState) { 36 | return !is(fromJS(this.props), fromJS(nextProps)) || !is(fromJS(this.state), fromJS(nextState)) 37 | } 38 | 39 | componentDidMount(){ 40 | if(!this.props.proData.dataList.length){ 41 | this.props.getProData(); 42 | } 43 | } 44 | 45 | render(){ 46 | return ( 47 |
48 | 49 |
50 |
    51 | { 52 | this.props.proData.dataList.map((item, index) => { 53 | return
  • 54 |
    55 | 56 | {item.product_name} 57 |
    58 |
    59 | 0? 'edit-active':''}`} onClick={this.handleEdit.bind(this, index, -1)}> 60 | {item.selectNum} 61 | 62 |
    63 |
  • 64 | }) 65 | } 66 |
67 |
68 |
69 | ) 70 | } 71 | } 72 | 73 | 74 | export default connect(state => ({ 75 | proData: state.proData, 76 | }), { 77 | getProData, 78 | togSelectPro, 79 | editPro 80 | })(Production); -------------------------------------------------------------------------------- /src/pages/production/production.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../../style/mixin.less"; 2 | @import "../../assets/iconfonts/iconfont.css"; 3 | 4 | .pro-list-con{ 5 | padding-top: @common-wd; 6 | } 7 | .pro-list-ul{ 8 | background-color: #fff; 9 | .pro-item{ 10 | min-height: 140px; 11 | padding: @common-wd; 12 | border-bottom: 1PX solid #eee; 13 | .flex(space-between); 14 | .pro-item-select{ 15 | .flex; 16 | .pro-select-status{ 17 | .font(45px, #ccc); 18 | } 19 | .pro-selected{ 20 | color: @base-color; 21 | } 22 | .pro-name{ 23 | .font(36px); 24 | margin-left: 20px; 25 | margin-top: 12px; 26 | } 27 | } 28 | .pro-item-edit{ 29 | .flex; 30 | .icon-jian{ 31 | .font(50px, #ccc); 32 | } 33 | .pro-num{ 34 | .font(30px, @text-align: center); 35 | min-width: 60px; 36 | } 37 | .icon-jia{ 38 | .font(50px, @base-color); 39 | } 40 | .edit-active{ 41 | color: @base-color; 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/pages/record/components/recordList.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { is, fromJS } from 'immutable'; 3 | import API from '@/api/api'; 4 | import './recordList.less'; 5 | 6 | class RecordList extends Component{ 7 | 8 | state = { 9 | recordData: [], 10 | } 11 | 12 | /** 13 | * 初始化获取数据 14 | * @param {string} type 数据类型 15 | */ 16 | getRecord = async type => { 17 | try{ 18 | let result = await API.getRecord({type}); 19 | this.setState({recordData: result.data||[]}) 20 | }catch(err){ 21 | console.error(err); 22 | } 23 | } 24 | 25 | componentWillReceiveProps(nextProps){ 26 | // 判断类型是否重复 27 | let currenType = this.props.location.pathname.split('/')[2]; 28 | let type = nextProps.location.pathname.split('/')[2]; 29 | if(currenType !== type){ 30 | this.getRecord(type); 31 | } 32 | } 33 | 34 | shouldComponentUpdate(nextProps, nextState){ 35 | return !is(fromJS(this.props), fromJS(nextProps)) || !is(fromJS(this.state), fromJS(nextState)) 36 | } 37 | 38 | componentWillMount(){ 39 | let type = this.props.location.pathname.split('/')[2]; 40 | this.getRecord(type); 41 | } 42 | 43 | render(){ 44 | return ( 45 |
46 |
    47 | { 48 | this.state.recordData.map((item, index) => { 49 | return
  • 50 |
    51 | 创建时间:{item.created_at} 52 | {item.type_name} 53 |
    54 |
    55 |

    用户名:{item.customers_name}   {item.customers_phone}

    56 |

    商 品:{item.product[0].product_name}

    57 |

    金 额:{item.sales_money}   佣金:{item.commission}

    58 |
    59 |

    等待管理员审核,审核通过后,佣金将结算至账户

    60 |
  • 61 | }) 62 | } 63 |
64 |
65 | ); 66 | } 67 | 68 | } 69 | 70 | export default RecordList; -------------------------------------------------------------------------------- /src/pages/record/components/recordList.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../../../style/mixin.less"; 2 | 3 | .record-list-con{ 4 | .record-item{ 5 | background-color: #fff; 6 | margin-top: 20px; 7 | .record-item-header{ 8 | .flex(@justify-content: space-between); 9 | height: 100px; 10 | padding: 0 20px; 11 | border-bottom: 1PX solid #eee; 12 | span:nth-of-type(1){ 13 | .font(30px, #999); 14 | } 15 | span:nth-of-type(2){ 16 | .font(36px, @base-red); 17 | } 18 | } 19 | .record-item-content{ 20 | padding: 20px; 21 | p{ 22 | .font(30px, #000); 23 | line-height: 50px; 24 | } 25 | } 26 | .record-item-footer{ 27 | padding: 20px; 28 | .font(28px, #000); 29 | border-top: 1PX solid #eee; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/pages/record/record.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { is, fromJS } from 'immutable'; 3 | import { NavLink, Switch, Route, Redirect } from 'react-router-dom'; 4 | import PublicHeader from '@/components/header/header'; 5 | import RecordList from './components/recordList'; 6 | import './record.less'; 7 | 8 | class Record extends Component { 9 | state = { 10 | flagBarPos: '17%', 11 | } 12 | /** 13 | * 设置头部底部标签位置 14 | * @param {string} type 数据类型 15 | */ 16 | setFlagBarPos = type => { 17 | let flagBarPos; 18 | switch(type){ 19 | case 'passed': 20 | flagBarPos = '17%'; 21 | break; 22 | case 'audited': 23 | flagBarPos = '50%'; 24 | break; 25 | case 'failed': 26 | flagBarPos = '83%'; 27 | break; 28 | default: 29 | flagBarPos = '17%'; 30 | } 31 | this.setState({flagBarPos}) 32 | } 33 | 34 | componentWillReceiveProps(nextProps){ 35 | // 属性变化时设置头部底部标签位置 36 | let currenType = this.props.location.pathname.split('/')[2]; 37 | let type = nextProps.location.pathname.split('/')[2]; 38 | if(currenType !== type){ 39 | this.setFlagBarPos(type); 40 | } 41 | } 42 | 43 | shouldComponentUpdate(nextProps, nextState){ 44 | return !is(fromJS(this.props), fromJS(nextProps)) || !is(fromJS(this.state), fromJS(nextState)) 45 | } 46 | 47 | componentWillMount(){ 48 | // 初始化设置头部底部标签位置 49 | let type = this.props.location.pathname.split('/')[2]; 50 | this.setFlagBarPos(type); 51 | } 52 | render() { 53 | return ( 54 |
55 | 56 |
57 | 62 | 63 |
64 | {/* 子路由在父级配置,react-router4新特性,更加灵活 */} 65 | 66 | 67 | 68 | 69 |
70 | ); 71 | } 72 | } 73 | 74 | export default Record; 75 | -------------------------------------------------------------------------------- /src/pages/record/record.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../../style/mixin.less"; 2 | 3 | .record-nav-con{ 4 | height: 110px; 5 | position: relative; 6 | .record-nav{ 7 | height: 100%; 8 | .flex; 9 | background-color: #fff; 10 | .nav-link{ 11 | .font(@font-size:36px, @text-align:center); 12 | flex: 1; 13 | height: 60px; 14 | line-height: 60px; 15 | transition: color 300ms; 16 | } 17 | a:nth-of-type(1), a:nth-of-type(2){ 18 | border-right: 1Px solid #ddd; 19 | } 20 | .active{ 21 | color: @base-color; 22 | } 23 | } 24 | 25 | .nav-flag-bar{ 26 | .wh(130px, 2PX); 27 | border-radius: 1px; 28 | background-color: @base-color; 29 | position: absolute; 30 | bottom: 0; 31 | left: 17%; 32 | margin-left: -65px; 33 | transition: left 300ms; 34 | } 35 | } 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | } else { 39 | // Is not local host. Just register service worker 40 | registerValidSW(swUrl); 41 | } 42 | }); 43 | } 44 | } 45 | 46 | function registerValidSW(swUrl) { 47 | navigator.serviceWorker 48 | .register(swUrl) 49 | .then(registration => { 50 | registration.onupdatefound = () => { 51 | const installingWorker = registration.installing; 52 | installingWorker.onstatechange = () => { 53 | if (installingWorker.state === 'installed') { 54 | if (navigator.serviceWorker.controller) { 55 | // At this point, the old content will have been purged and 56 | // the fresh content will have been added to the cache. 57 | // It's the perfect time to display a "New content is 58 | // available; please refresh." message in your web app. 59 | console.log('New content is available; please refresh.'); 60 | } else { 61 | // At this point, everything has been precached. 62 | // It's the perfect time to display a 63 | // "Content is cached for offline use." message. 64 | console.log('Content is cached for offline use.'); 65 | } 66 | } 67 | }; 68 | }; 69 | }) 70 | .catch(error => { 71 | console.error('Error during service worker registration:', error); 72 | }); 73 | } 74 | 75 | function checkValidServiceWorker(swUrl) { 76 | // Check if the service worker can be found. If it can't reload the page. 77 | fetch(swUrl) 78 | .then(response => { 79 | // Ensure service worker exists, and that we really are getting a JS file. 80 | if ( 81 | response.status === 404 || 82 | response.headers.get('content-type').indexOf('javascript') === -1 83 | ) { 84 | // No service worker found. Probably a different app. Reload the page. 85 | navigator.serviceWorker.ready.then(registration => { 86 | registration.unregister().then(() => { 87 | window.location.reload(); 88 | }); 89 | }); 90 | } else { 91 | // Service worker found. Proceed as normal. 92 | registerValidSW(swUrl); 93 | } 94 | }) 95 | .catch(() => { 96 | console.log( 97 | 'No internet connection found. App is running in offline mode.' 98 | ); 99 | }); 100 | } 101 | 102 | export function unregister() { 103 | if ('serviceWorker' in navigator) { 104 | navigator.serviceWorker.ready.then(registration => { 105 | registration.unregister(); 106 | }); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { HashRouter, Switch, Route, Redirect } from 'react-router-dom'; 3 | import asyncComponent from '@/utils/asyncComponent'; 4 | 5 | import home from "@/pages/home/home"; 6 | const record = asyncComponent(() => import("@/pages/record/record")); 7 | const helpcenter = asyncComponent(() => import("@/pages/helpcenter/helpcenter")); 8 | const production = asyncComponent(() => import("@/pages/production/production")); 9 | const balance = asyncComponent(() => import("@/pages/balance/balance")); 10 | 11 | // react-router4 不再推荐将所有路由规则放在同一个地方集中式路由,子路由应该由父组件动态配置,组件在哪里匹配就在哪里渲染,更加灵活 12 | export default class RouteConfig extends Component{ 13 | render(){ 14 | return( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/store/home/action-type.js: -------------------------------------------------------------------------------- 1 | // 保存表单数据 2 | export const SAVEFORMDATA = 'SAVEFORMDATA'; 3 | // 保存图片 4 | export const SAVEIMG = 'SAVEIMG'; 5 | // 清空数据 6 | export const CLEARDATA = 'CLEARDATA'; -------------------------------------------------------------------------------- /src/store/home/action.js: -------------------------------------------------------------------------------- 1 | import * as home from './action-type'; 2 | 3 | // 保存表单数据 4 | export const saveFormData = (value, datatype) => { 5 | return { 6 | type: home.SAVEFORMDATA, 7 | value, 8 | datatype, 9 | } 10 | } 11 | 12 | // 保存图片地址 13 | export const saveImg = path => { 14 | return { 15 | type: home.SAVEIMG, 16 | path, 17 | } 18 | } 19 | 20 | // 保存图片地址 21 | export const clearData = () => { 22 | return { 23 | type: home.CLEARDATA, 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /src/store/home/reducer.js: -------------------------------------------------------------------------------- 1 | import * as home from './action-type'; 2 | 3 | let defaultState = { 4 | orderSum: '', //金额 5 | name: '', //姓名 6 | phoneNo: '', //手机号 7 | imgpath: '', //图片地址 8 | } 9 | // 首页表单数据 10 | export const formData = (state = defaultState , action = {}) => { 11 | switch(action.type){ 12 | case home.SAVEFORMDATA: 13 | return {...state, ...{[action.datatype]: action.value}}; 14 | case home.SAVEIMG: 15 | return {...state, ...{imgpath: action.path}}; 16 | case home.CLEARDATA: 17 | return {...state, ...defaultState}; 18 | default: 19 | return state; 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /src/store/production/action-type.js: -------------------------------------------------------------------------------- 1 | // 保存商品数据 2 | export const GETPRODUCTION = 'GETPRODUCTION'; 3 | // 选择商品 4 | export const TOGGLESELECT = 'TOGGLESELECT'; 5 | // 编辑商品 6 | export const EDITPRODUCTION = 'EDITPRODUCTION'; 7 | // 清空选择 8 | export const CLEARSELECTED = 'CLEARSELECTED'; -------------------------------------------------------------------------------- /src/store/production/action.js: -------------------------------------------------------------------------------- 1 | import * as pro from './action-type'; 2 | import API from '@/api/api'; 3 | 4 | // 初始化获取商品数据,保存至redux 5 | export const getProData = () => { 6 | // 返回函数,异步dispatch 7 | return async dispatch => { 8 | try{ 9 | let result = await API.getProduction(); 10 | result.map(item => { 11 | item.selectStatus = true; 12 | item.selectNum = 0; 13 | return item; 14 | }) 15 | dispatch({ 16 | type: pro.GETPRODUCTION, 17 | dataList: result, 18 | }) 19 | }catch(err){ 20 | console.error(err); 21 | } 22 | } 23 | } 24 | 25 | // 选择商品 26 | export const togSelectPro = index => { 27 | return { 28 | type: pro.TOGGLESELECT, 29 | index, 30 | } 31 | } 32 | 33 | // 编辑商品 34 | export const editPro = (index, selectNum) => { 35 | return { 36 | type: pro.EDITPRODUCTION, 37 | index, 38 | selectNum, 39 | } 40 | } 41 | 42 | // 清空选择 43 | export const clearSelected = () => { 44 | return { 45 | type: pro.CLEARSELECTED, 46 | } 47 | } 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/store/production/reducer.js: -------------------------------------------------------------------------------- 1 | import * as pro from './action-type'; 2 | import Immutable from 'immutable'; 3 | 4 | let defaultState = { 5 | /** 6 | * 商品数据 7 | * @type {Array} 8 | * example: [{ 9 | * product_id: 1, 商品ID 10 | * product_name: "PaiBot(2G/32G)", 商品名称 11 | * product_price: 2999, 商品价格 12 | * commission: 200, 佣金 13 | * selectStatus: false, 是否选择 14 | * selectNum: 0, 选择数量 15 | * }] 16 | */ 17 | dataList: [], 18 | } 19 | 20 | export const proData = (state = defaultState, action) => { 21 | let imuDataList; 22 | let imuItem; 23 | switch(action.type){ 24 | case pro.GETPRODUCTION: 25 | return {...state, ...action} 26 | case pro.TOGGLESELECT: 27 | //避免引用类型数据,使用immutable进行数据转换 28 | imuDataList = Immutable.List(state.dataList); 29 | imuItem = Immutable.Map(state.dataList[action.index]); 30 | imuItem = imuItem.set('selectStatus', !imuItem.get('selectStatus')); 31 | imuDataList = imuDataList.set(action.index, imuItem); 32 | // redux必须返回一个新的state 33 | return {...state, ...{dataList: imuDataList.toJS()}}; 34 | case pro.EDITPRODUCTION: 35 | //避免引用类型数据,使用immutable进行数据转换 36 | imuDataList = Immutable.List(state.dataList); 37 | imuItem = Immutable.Map(state.dataList[action.index]); 38 | imuItem = imuItem.set('selectNum', action.selectNum); 39 | imuDataList = imuDataList.set(action.index, imuItem); 40 | // redux必须返回一个新的state 41 | return {...state, ...{dataList: imuDataList.toJS()}}; 42 | // 清空数据 43 | case pro.CLEARSELECTED: 44 | imuDataList = Immutable.fromJS(state.dataList); 45 | for (let i = 0; i < state.dataList.length; i++) { 46 | imuDataList = imuDataList.update(i, item => { 47 | item = item.set('selectStatus', false); 48 | item = item.set('selectNum', 0); 49 | return item 50 | }) 51 | } 52 | return {...state, ...{dataList: imuDataList.toJS()}}; 53 | default: 54 | return state; 55 | } 56 | } -------------------------------------------------------------------------------- /src/store/store.js: -------------------------------------------------------------------------------- 1 | import {createStore, combineReducers, applyMiddleware} from 'redux'; 2 | import * as home from './home/reducer'; 3 | import * as production from './production/reducer'; 4 | import thunk from 'redux-thunk'; 5 | 6 | let store = createStore( 7 | combineReducers({...home, ...production}), 8 | applyMiddleware(thunk) 9 | ); 10 | 11 | export default store; -------------------------------------------------------------------------------- /src/style/base.css: -------------------------------------------------------------------------------- 1 | *{ 2 | margin: 0; 3 | padding: 0; 4 | /*防止点击闪烁*/ 5 | -webkit-tap-highlight-color: transparent; 6 | /*缩放网页,文字大小不变*/ 7 | -webkit-text-size-adjust: none; 8 | font-family: "Helvetica Neue",Helvetica,STHeiTi,sans-serif; 9 | -webkit-font-smoothing: antialiased; 10 | box-sizing: border-box; 11 | font-weight: normal; 12 | } 13 | /*修复img默认位置和IE边框*/ 14 | img{ 15 | vertical-align: top; 16 | border:none; 17 | } 18 | 19 | /*去除虚线*/ 20 | input:focus,select:focus,textarea:focus,button:focus{ 21 | outline: none; 22 | } 23 | /*重置部分表单圆角和内阴影*/ 24 | input[type="text"],input[type="file"],input[type="password"],input[type="search"],input[type="tel"],input[type="number"],input[type="email"],input[type="url"],textarea{ 25 | -webkit-appearance:none; 26 | } 27 | /*去除文本框上下箭头*/ 28 | input[type="number"]::-webkit-inner-spin-button{ 29 | -webkit-appearance:none; 30 | } 31 | 32 | /*设置iconfont标签font-famile*/ 33 | [class^="icon-"], [class*=" icon-"] { 34 | font-family:"iconfont"; 35 | font-size:16px; 36 | font-style:normal; 37 | -webkit-font-smoothing: antialiased; 38 | -moz-osx-font-smoothing: grayscale; 39 | } 40 | a{ 41 | text-decoration: none; 42 | /*ios禁止页面在新窗口打开*/ 43 | -webkit-touch-callout:none; 44 | } 45 | body{ 46 | background-color: #f5f5f5; 47 | } 48 | em,i{ 49 | font-style: normal; 50 | } 51 | li{ 52 | list-style: none; 53 | } 54 | html,body{ 55 | height: 100%; 56 | } 57 | .common-con-top{ 58 | margin-top: 88px; 59 | } 60 | 61 | .ellipsis{ 62 | overflow:hidden; 63 | text-overflow:ellipsis; 64 | white-space:nowrap 65 | } -------------------------------------------------------------------------------- /src/style/mixin.less: -------------------------------------------------------------------------------- 1 | @base-color: #975ec9; 2 | @common-wd: 30px; 3 | @assets-img: '../assets/images/'; 4 | @base-red: #F20500; 5 | 6 | .flex(@justify-content: center, @align-items: center) when (iskeyword(@justify-content)), (iskeyword(@align-items)){ 7 | display: flex; 8 | justify-content: @justify-content; 9 | align-items: @align-items; 10 | } 11 | 12 | .commonBack(@url) when (isstring(@url)){ 13 | background: url("@{assets-img}@{url}") no-repeat center; 14 | background-size: 100%; 15 | } 16 | 17 | .coverScreen(@position: absolute, @backColor: #fff) when (iskeyword(@position)), (iscolor(@backColor)){ 18 | position: @position; 19 | top: 0; 20 | left: 0; 21 | right: 0; 22 | bottom: 0; 23 | width: 100%; 24 | height: 100%; 25 | background-color: @backColor; 26 | } 27 | 28 | .positionCenter(@position: absolute) when (iskeyword(@position)){ 29 | position: @position; 30 | top: 50%; 31 | left: 50%; 32 | transform: translate3d(-50%, -50%, 0); 33 | } 34 | 35 | .positionLeft(@position: absolute, @left: @common-wd) when (iskeyword(@position)), (ispixel(@left)){ 36 | position: @position; 37 | top: 50%; 38 | left: @left; 39 | transform: translate3d(0, -50%, 0); 40 | } 41 | 42 | .positionRight(@position: absolute, @right: @common-wd) when (iskeyword(@position)), (ispixel(@right)){ 43 | position: @position; 44 | top: 50%; 45 | right: @right; 46 | transform: translate3d(0, -50%, 0); 47 | } 48 | 49 | .font(@font-size, @color: #333, @text-align: left) when (ispixel(@font-size)), (iscolor(@color)), (iskeyword(@text-align)) { 50 | font-size: @font-size; 51 | color: @color; 52 | text-align: @text-align; 53 | } 54 | 55 | .wh(@w, @h) when (ispixel(@w))or(ispercentage(@w)), (ispixel(@h))or(ispercentage(@h)){ 56 | width: @w; 57 | height: @h; 58 | } -------------------------------------------------------------------------------- /src/utils/asyncComponent.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | 3 | export default function asyncComponent(importComponent) { 4 | class AsyncComponent extends Component { 5 | constructor(props) { 6 | super(props); 7 | 8 | this.state = { 9 | component: null 10 | }; 11 | } 12 | 13 | async componentDidMount() { 14 | const { default: component } = await importComponent(); 15 | 16 | this.setState({component}); 17 | } 18 | 19 | render() { 20 | const C = this.state.component; 21 | 22 | return C ? : null; 23 | } 24 | } 25 | 26 | return AsyncComponent; 27 | } -------------------------------------------------------------------------------- /src/utils/mixin.js: -------------------------------------------------------------------------------- 1 | export default methods => { 2 | return target => { 3 | Object.assign(target.prototype, methods); 4 | } 5 | } 6 | 7 | /** 8 | * 字符串填充函数 9 | * @param {string} value 目标字符串 10 | * @param {array} position 需要填充的位置 11 | * @param {string} padstr 填充字符串 12 | * @return {string} 返回目标字符串 13 | */ 14 | export const padStr = (value, position, padstr, inputElement) => { 15 | position.forEach((item, index) => { 16 | if (value.length > item + index) { 17 | value = value.substring(0, item + index) + padstr + value.substring(item + index) 18 | } 19 | }) 20 | value = value.trim(); 21 | // 解决安卓部分浏览器插入空格后光标错位问题 22 | requestAnimationFrame(() => { 23 | inputElement.setSelectionRange(value.length, value.length); 24 | }) 25 | return value; 26 | } 27 | 28 | -------------------------------------------------------------------------------- /src/utils/setRem.js: -------------------------------------------------------------------------------- 1 | (function(psdw){ 2 | var dpr=0 , rem=0 , scale=0; 3 | var htmlDOM=document.documentElement; 4 | dpr=window.devicePixelRatio; 5 | var currentWidth=htmlDOM.clientWidth; 6 | scale=currentWidth/psdw; 7 | rem=psdw/10; 8 | rem=rem*scale; 9 | htmlDOM.style.fontSize=rem+'px'; 10 | htmlDOM.setAttribute('data-dpr',dpr) 11 | })(750) --------------------------------------------------------------------------------