├── .DS_Store ├── .gitignore ├── LICENSE ├── README.md ├── config ├── config.js ├── scss.template.handlebars └── utils.js ├── gulpfile.js ├── node ├── README.md ├── app.js ├── asset │ ├── comment.js │ ├── detail.js │ └── index.js ├── common │ ├── nodeUtils.js │ └── requestSync.js ├── config │ ├── cgiPath.js │ ├── mongo.js │ └── router.js ├── controller │ └── controller.js ├── main.js ├── model │ ├── db.js │ └── model.js ├── package.json └── view │ ├── index.html │ └── layout.html ├── package.json ├── src ├── .DS_Store ├── css │ ├── common │ │ ├── common.scss │ │ ├── icon.scss │ │ └── reset.scss │ └── sprites │ │ ├── list_s.png │ │ └── list_s.scss ├── favicon.ico ├── img │ ├── .DS_Store │ └── sprites │ │ ├── .DS_Store │ │ └── list_s │ │ ├── .DS_Store │ │ ├── icon.png │ │ └── logo_news.png ├── index.html ├── js │ └── common │ │ ├── immutable-pure-render-decorator.js │ │ ├── net.js │ │ ├── pure-render-decorator.js │ │ ├── spin.js │ │ └── utils.js ├── libs │ ├── .DS_Store │ ├── react-dom.js │ └── react.js ├── page │ ├── .DS_Store │ ├── common │ │ ├── actions │ │ │ └── actions.js │ │ ├── components │ │ │ ├── scroll │ │ │ │ ├── index.js │ │ │ │ └── index.scss │ │ │ ├── spinner │ │ │ │ ├── index.js │ │ │ │ ├── index.scss │ │ │ │ └── spinnerComp.js │ │ │ └── touch │ │ │ │ ├── index.js │ │ │ │ └── touchComp.js │ │ ├── constants │ │ │ ├── cgiPath.js │ │ │ └── constants.js │ │ ├── devtools │ │ │ └── DevTools.js │ │ └── middleware │ │ │ ├── api.js │ │ │ └── logger.js │ ├── index │ │ ├── actions │ │ │ └── actions.js │ │ ├── components │ │ │ ├── list │ │ │ │ ├── index.js │ │ │ │ └── index.scss │ │ │ ├── loading │ │ │ │ ├── index.js │ │ │ │ └── index.scss │ │ │ ├── scroll │ │ │ │ └── index.js │ │ │ └── tab │ │ │ │ ├── index.js │ │ │ │ └── index.scss │ │ ├── connect │ │ │ └── connect.js │ │ ├── constants │ │ │ └── constants.js │ │ ├── container │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── main.js │ │ ├── reducers │ │ │ └── reducers.js │ │ ├── root │ │ │ ├── Root.dev.js │ │ │ ├── Root.js │ │ │ └── Root.prod.js │ │ └── stores │ │ │ ├── configureStore.dev.js │ │ │ ├── configureStore.js │ │ │ ├── configureStore.prod.js │ │ │ └── stores.js │ └── spa │ │ ├── actions │ │ └── actions.js │ │ ├── components │ │ ├── list │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── loading │ │ │ ├── index.js │ │ │ └── index.scss │ │ └── tab │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── connect │ │ └── connect.js │ │ ├── constants │ │ └── constants.js │ │ ├── container │ │ ├── app.js │ │ ├── comment.js │ │ ├── comment.scss │ │ ├── detail.js │ │ ├── detail.scss │ │ ├── index.js │ │ └── index.scss │ │ ├── main.js │ │ ├── reducers │ │ └── reducers.js │ │ ├── root │ │ ├── Root.dev.js │ │ ├── Root.dev_browser.js │ │ ├── Root.dev_hash.js │ │ ├── Root.js │ │ ├── Root.prod.js │ │ ├── Root.prod_browser.js │ │ ├── route.js │ │ └── route_server.js │ │ └── stores │ │ ├── configureStore.dev.js │ │ ├── configureStore.dev_browser.js │ │ ├── configureStore.dev_hash.js │ │ ├── configureStore.js │ │ ├── configureStore.prod.js │ │ ├── configureStore.prod_browser.js │ │ └── stores.js └── spa.html ├── webpack.config.js ├── webpack.dev.js ├── webpack.node.js ├── webpack.prod.js └── webpack.server.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcxfs1991/steamer-react/ad7d574bd66b56e7ac8ff9cfe4e0e230338b48f9/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .svn 3 | pub 4 | dist 5 | 4.6.85.31.flags.json 6 | npm-debug.log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 heyli 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # web开发 2 | 3 | ## 端口占用 4 | * 9000 webpack开发时占用,用于hot reload,以及做proxy,可指向服务端 5 | * 3001 koa服务器端占用 6 | 7 | ## 命令环境 8 | package.json中的scripts,若是Windows,设置环境请用set,若是Mac,设置环境请使用export,如: 9 | * Mac => export NODE_ENV=__DEV__ 10 | * Window => set NODE_ENV=__DEV__ 11 | 12 | ## 开发环境 13 | * react文件夹下启动:npm run dev 14 | * react/node文件夹下启动: npm start 15 | 16 | 腾讯新闻主页: 17 | * localhost:9000/index.html 18 | * localhost:9000/news/index.html (webpack.server.js里映射路径到news) 19 | 20 | 腾讯新闻spa页: 21 | * localhost:9000/spa.html 22 | * localhost:9000/news/spa.html 23 | 24 | ## 生产环境 25 | * react文件夹下启动: npm run pub 26 | * react/node文件夹下启动npm start 27 | 28 | 使用Fiddler(Window) / Charles(Mac) 配置以下代理 29 | 30 | ### Charles: 31 | ### Map Local: 32 | * localhost:9000 => /react/pub/ 匹配本地html资源 33 | * localhost:8000 => /react/pub/ 匹配本地除cdn资源 34 | 35 | ### Map Remote: 36 | * localhost:9000/api/* => localhost:3000/api/ 37 | 38 | ### Fiddler: 39 | ### Rule 40 | 41 | ### Host/Extension 42 | 43 | 44 | # 直出 45 | ## 开发环境 46 | * react文件夹下启动: npm run dev-node => 后台服务相关 47 | * react文件夹下启动: npm run dev-node-static => cdn资源 48 | * recat/node文件夹下启动: npm run start 49 | * react直出后台逻辑主要在react/node/asset/index.js中,生成文件在react/pub/node/app.js。 50 | cdn资源生成在react/dist/中。 51 | * 列表页、详情页、留言页都可以以spa或者直出的形式访问 52 | 53 | 腾讯新闻spa页: 54 | * http://localhost:3001/spa 55 | 56 | 使用Fiddler(Window) / Charles(Mac) 配置以下代理 57 | * localhost:3001 => /react/dist/ 匹配本地除cdn资源 58 | 59 | ## 生产环境 60 | * react文件夹下启动: npm run pub-node 61 | * react/node文件夹下启动: npm run start 62 | * 生成内容都在react/pub/中 63 | * 列表页、详情页、留言页都可以以spa或者直出的形式访问 64 | 65 | 66 | # 多个页面的开发 67 | 添加html到src/目录下就可以了,现在steamer-react会自动识别 68 | 69 | 70 | # Devtools 71 | * ctrl + h进行切换 72 | * ctrl + q切换位置 73 | 74 | 其它命令可以参考src/page/common/DevTools。可以调defaultSize设置自己喜欢的大小。目前默认设置在底部,占30%的屏幕大小。 75 | 76 | # 文件目录 77 | * 单页面文件可参考 src/page/index 78 | * 单页应用可参考 src/page/spa 79 | 80 | 81 | # 合图代码 82 | 请统一放在 src/page/xxx/container/xxx.scss中,可参考src/page/index里面的做法。 83 | 这里的问题囿于插件局限性,之后建议找更好的插件,或者我们自己写一个。 84 | 85 | 目前构建已经支持多个合图。只需要在src/img/sprites/下面新建文件夹,然后放在需要合的图,就会自动在src/css/文件夹下生成sprites/文件夹,里面包含了对应的合图和scss。 86 | 87 | # Windows下node-sass的安装 88 | 在node版本大于4.0的环境下,调用“npm rebuild node-sass ”时会自动安装“node-gyp”模块。window下的“node-gyp”模块需要以下配置: 89 | 90 | * python(v2.7) (https://www.python.org/ftp/python/2.7.9/python-2.7.9.amd64.msi) 91 | * Microsoft Visual Studio C++ 2013 (https://www.visualstudio.com/downloads/download-visual-studio-vs#d-express-windows-desktop) 92 | 93 | 94 | 尝试: 95 | https://github.com/nodejs/node-gyp/wiki/Visual-Studio-2010-Setup 96 | 97 | # 更多使用办法 98 | * 可参考webpack的官方文档 99 | * [webpack使用优化(基本篇)](https://github.com/lcxfs1991/blog/issues/2) 100 | * [webpack使用优化(react篇)](https://github.com/lcxfs1991/blog/issues/7) 101 | -------------------------------------------------------------------------------- /config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'), 4 | __basename = path.dirname(__dirname), 5 | isProduction = process.env.NODE_ENV === '__PROD__' || process.env.NODE_ENV === '__PROD_NODE__', 6 | isNode = process.env.NODE_ENV === '__NODE_DEV__' || process.env.NODE_ENV === '__NODE_PROD__'; 7 | 8 | /** 9 | * [config basic configuration] 10 | * @type {Object} 11 | */ 12 | var config = { 13 | env: process.env.NODE_ENV, 14 | path: { 15 | src: path.resolve(__basename, "src"), 16 | dist: path.resolve(__basename, "dist"), 17 | pub: path.resolve(__basename, "pub"), 18 | node: path.resolve(__basename, "node"), 19 | }, 20 | gulpPath: { 21 | src: './src/', 22 | dist: './dist/', 23 | pub: './pub/', 24 | offline: './offline/', 25 | node: './node/', 26 | }, 27 | chunkhash: (isProduction) ? "-[chunkhash:6]" : "", 28 | hash: (isProduction) ? "-[hash:6]" : "", 29 | defaultPath: "//localhost:9000/", 30 | cdn: "//localhost:8000/", 31 | serverPort: 9000, // port for local server 32 | hostDirectory: "/news/" // http://host/hostDirectory/ 33 | }; 34 | 35 | if (!isNode) { 36 | const utils = require('./utils'); 37 | config.html = utils.getHtmlFile(config.path.src); 38 | } 39 | 40 | config.sprites = { 41 | // imgPath: '../../../css/sprites/sprites.png', 42 | imgPath: '../../../css/sprites/', 43 | imgName: 'sprites.png', 44 | cssName: 'sprites.scss', 45 | imgDest: config.gulpPath.src + 'css/sprites/', 46 | cssDest: config.gulpPath.src + 'css/sprites/' 47 | }; 48 | 49 | module.exports = config; 50 | -------------------------------------------------------------------------------- /config/scss.template.handlebars: -------------------------------------------------------------------------------- 1 | { 2 | // Default options 3 | 'functions': true, 4 | 'variableNameTransforms': ['dasherize'] 5 | } 6 | 7 | {{#block "sprites-comment"}} 8 | /* 9 | SCSS variables are information about icon's compiled state, stored under its original file name 10 | 11 | .icon-home { 12 | width: $icon-home-width; 13 | } 14 | 15 | The large array-like variables contain all information about a single icon 16 | $icon-home: x y offset_x offset_y width height total_width total_height image_path; 17 | 18 | At the bottom of this section, we provide information about the spritesheet itself 19 | $spritesheet: width height image $spritesheet-sprites; 20 | */ 21 | {{/block}} 22 | {{#block "sprites"}} 23 | {{#each sprites}} 24 | ${{strings.name_name}}: '{{name}}'; 25 | ${{strings.name_x}}: {{px.x}}; 26 | ${{strings.name_y}}: {{px.y}}; 27 | ${{strings.name_offset_x}}: {{px.offset_x}}; 28 | ${{strings.name_offset_y}}: {{px.offset_y}}; 29 | ${{strings.name_width}}: {{px.width}}; 30 | ${{strings.name_height}}: {{px.height}}; 31 | ${{strings.name_total_width}}: {{px.total_width}}; 32 | ${{strings.name_total_height}}: {{px.total_height}}; 33 | ${{strings.name_image}}: '{{{escaped_image}}}'; 34 | ${{strings.name}}: ({{px.x}}, {{px.y}}, {{px.offset_x}}, {{px.offset_y}}, {{px.width}}, {{px.height}}, {{px.total_width}}, {{px.total_height}}, '{{{escaped_image}}}', '{{name}}', ); 35 | {{/each}} 36 | {{/block}} 37 | {{#block "spritesheet"}} 38 | ${{spritesheet_info.strings.name_width}}: {{spritesheet.px.width}}; 39 | ${{spritesheet_info.strings.name_height}}: {{spritesheet.px.height}}; 40 | ${{spritesheet_info.strings.name_image}}: '{{{spritesheet.escaped_image}}}'; 41 | ${{spritesheet_info.strings.name_sprites}}: ({{#each sprites}}${{strings.name}}, {{/each}}); 42 | ${{spritesheet_info.strings.name}}: ({{spritesheet.px.width}}, {{spritesheet.px.height}}, '{{{spritesheet.escaped_image}}}', ${{spritesheet_info.strings.name_sprites}}, ); 43 | {{/block}} 44 | 45 | {{#block "sprite-functions-comment"}} 46 | {{#if options.functions}} 47 | /* 48 | The provided mixins are intended to be used with the array-like variables 49 | 50 | .icon-home { 51 | @include sprite-width($icon-home); 52 | } 53 | 54 | .icon-email { 55 | @include sprite($icon-email); 56 | } 57 | 58 | Here are example usages in HTML: 59 | 60 | `display: block` sprite: 61 |
62 | 63 | `display: inline-block` sprite: 64 | 65 | */ 66 | {{/if}} 67 | {{/block}} 68 | {{#block "sprite-functions"}} 69 | {{#if options.functions}} 70 | @mixin sprite-width($sprite) { 71 | width: nth($sprite, 5); 72 | } 73 | 74 | @mixin sprite-height($sprite) { 75 | height: nth($sprite, 6); 76 | } 77 | 78 | @mixin sprite-position($sprite) { 79 | $sprite-offset-x: nth($sprite, 3); 80 | $sprite-offset-y: nth($sprite, 4); 81 | background-position: $sprite-offset-x / 2 $sprite-offset-y / 2; 82 | } 83 | 84 | @mixin sprite-image($sprite) { 85 | $sprite-image: nth($sprite, 9); 86 | background-image: url(#{$sprite-image}); 87 | } 88 | 89 | // nth是指#block "sprites"第几个参数,可以输出$sprite来查看, 90 | @mixin sprite($sprite) { 91 | @include sprite-image($sprite); 92 | @include sprite-position($sprite); 93 | // @include sprite-width($sprite); 94 | // @include sprite-height($sprite); 95 | background-size: nth($sprite, 7) / 2 auto; 96 | } 97 | {{/if}} 98 | {{/block}} 99 | 100 | {{#block "spritesheet-functions-comment"}} 101 | {{#if options.functions}} 102 | /* 103 | The `sprites` mixin generates identical output to the CSS template 104 | but can be overridden inside of SCSS 105 | 106 | @include sprites($spritesheet-sprites); 107 | */ 108 | {{/if}} 109 | {{/block}} 110 | {{#block "spritesheet-functions"}} 111 | {{#if options.functions}} 112 | @mixin sprites($sprites) { 113 | @each $sprite in $sprites { 114 | $sprite-name: nth($sprite, 10); 115 | .#{$sprite-name} { 116 | @include sprite($sprite); 117 | } 118 | } 119 | } 120 | {{/if}} 121 | {{/block}} 122 | -------------------------------------------------------------------------------- /config/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'), 4 | path = require('path'); 5 | 6 | module.exports = { 7 | getHtmlFile: function(srcPath) { 8 | // read html filename from 9 | var srcFiles = fs.readdirSync(srcPath); 10 | 11 | srcFiles = srcFiles.filter((item, index) => { 12 | return !!~item.indexOf('.html'); 13 | }); 14 | 15 | srcFiles = srcFiles.map((item, index) => { 16 | return item.replace('.html', ''); 17 | }); 18 | 19 | return srcFiles; 20 | } 21 | }; -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var gulp = require("gulp"); 3 | var run = require('run-sequence'); 4 | var merge = require('merge-stream'); 5 | var replace = require('gulp-replace'); 6 | // 合图 7 | // var spritesmith = require('gulp.spritesmith'); 8 | var spritesmith = require('gulp.spritesmith-multi'); 9 | 10 | var config = require('./config/config.js'); 11 | 12 | gulp.task('sprites', function (cb) { 13 | var spriteData = gulp.src(config.gulpPath.src + 'img/sprites/**/*.png') 14 | .pipe(spritesmith({ 15 | spritesmith: function(options) { 16 | options.imgPath = config.sprites.imgPath + options.imgName; 17 | 18 | options.cssName = options.cssName.replace('.css', '.scss'); 19 | // customized generated css template 20 | options.cssTemplate = './config/scss.template.handlebars'; 21 | } 22 | })); 23 | 24 | // Pipe image stream through image optimizer and onto disk 25 | var imgStream = spriteData.img 26 | // DEV: We must buffer our stream into a Buffer for `imagemin` 27 | .pipe(gulp.dest(config.sprites.imgDest)); 28 | 29 | // Pipe CSS stream through CSS optimizer and onto disk 30 | var cssStream = spriteData.css 31 | .pipe(gulp.dest(config.sprites.cssDest)); 32 | 33 | // Return a merged stream to handle both `end` events 34 | return merge(imgStream, cssStream); 35 | }); 36 | 37 | gulp.task('dist', ['sprites'], function(cb) { 38 | cb(); 39 | }); 40 | 41 | gulp.task('default', function() { 42 | run('dist'); 43 | }); -------------------------------------------------------------------------------- /node/README.md: -------------------------------------------------------------------------------- 1 | monk dependency: 2 | "mongoskin": "2.0.3" -------------------------------------------------------------------------------- /node/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | const koa = require('koa'); 5 | const mount = require('koa-mount'); 6 | const logger = require('koa-logger'); 7 | const render = require('koa-swig'); 8 | const serve = require('koa-static'); 9 | const router = require('./config/router'); 10 | const bodyParser = require('koa-bodyparser'); 11 | const fs = require('fs'); 12 | const path = require('path'); 13 | const app = koa(); 14 | 15 | // 指向静态文件夹 16 | console.log(path.resolve(path.resolve('view/'))); 17 | app.context.render = render({ 18 | root: path.resolve(path.resolve('view/')), 19 | autoescape: true, 20 | cache: false, 21 | ext: 'html' 22 | }); 23 | 24 | // 处理静态文件 25 | app.use(serve(path.resolve('static/'))); 26 | 27 | //使用logger日志库 28 | app.use(logger()); 29 | 30 | app.use(bodyParser()); 31 | 32 | //路由处理,首页指定用index函数处理,但需要先经过validate函数校验 33 | app.use(mount('/', router.RULE)); 34 | 35 | // 监听3001端口 36 | var port = 3001; 37 | app.listen(port, function(err) { 38 | if (err) { 39 | console.error(err); 40 | } 41 | else { 42 | console.info("Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port); 43 | } 44 | }); 45 | -------------------------------------------------------------------------------- /node/asset/comment.js: -------------------------------------------------------------------------------- 1 | const vm = require('vm'), 2 | React = require('react'); 3 | // Root = React.createFactory(require('Root').default); 4 | 5 | var CGI_PATH = require('../config/cgiPath'); 6 | var requestSync = plugin('requestSync'); 7 | 8 | import { renderToString } from 'react-dom/server'; 9 | import { Provider } from 'react-redux'; 10 | import { configureStore } from 'configureStore'; 11 | import { match, RouterContext } from 'react-router'; 12 | import { routeConfig } from 'routes'; 13 | 14 | module.exports = function* (req, res) { 15 | 16 | let comment_id = req.params.commentid, 17 | otype = "jsonp", 18 | callback = "renderComment", 19 | lcount = 20, 20 | from = 'share', 21 | v = (new Date()).getTime(); 22 | 23 | let urlParam = '?comment_id=' + comment_id + '&otype=' + otype 24 | + '&callback=' + callback + '&lcount=' + lcount 25 | + '&from=' + from + '&v=' + v; 26 | 27 | 28 | var response = yield requestSync.requestSync({ 29 | uri: CGI_PATH['GET_COMMENT_LIST'] + urlParam, 30 | method: 'GET' 31 | }); 32 | 33 | // console.log(JSON.stringify(response.body)); 34 | 35 | let jsonpSandbox = vm.createContext({renderComment: function(r){return r;}}); 36 | let jsonData = vm.runInContext(response.body, jsonpSandbox); 37 | 38 | // // console.log(jsonData); 39 | 40 | const store = configureStore(); 41 | 42 | yield store.dispatch({ 43 | type: 'GET_COMMENT_LIST_SUCCESS', 44 | data: jsonData, 45 | param:{ 46 | comment_id: comment_id, 47 | otype: otype, 48 | callback: callback, 49 | lcount: lcount, 50 | from: from, 51 | v: v 52 | } 53 | }); 54 | 55 | let path = (process.env.NODE_ENV === '__NODE_DEV__') ? 'src' : 'pub'; 56 | let finalState = store.getState(); 57 | let fileContent = require('../../' + path + '/spa.html'); 58 | 59 | // console.log(finalState); 60 | 61 | let reactHtml = ""; 62 | match({ routes: routeConfig, location: req.url }, (error, redirectLocation, renderProps) => { 63 | if (renderProps) { 64 | reactHtml = renderToString( 65 | 66 | 67 | 68 | ); 69 | } 70 | else { 71 | res.body = "404"; 72 | } 73 | }); 74 | 75 | fileContent = fileContent 76 | .replace("window.isNode=false;", "window.isNode=true;") 77 | .replace('
', 78 | '
' + reactHtml + '
' 79 | + ''); 80 | 81 | res.body = fileContent; 82 | }; -------------------------------------------------------------------------------- /node/asset/detail.js: -------------------------------------------------------------------------------- 1 | const vm = require('vm'), 2 | React = require('react'); 3 | // Root = React.createFactory(require('Root').default); 4 | 5 | var CGI_PATH = require('../config/cgiPath'); 6 | var requestSync = plugin('requestSync'); 7 | 8 | import { renderToString } from 'react-dom/server'; 9 | import { Provider } from 'react-redux'; 10 | import { configureStore } from 'configureStore'; 11 | import { match, RouterContext } from 'react-router'; 12 | import { routeConfig } from 'routes'; 13 | 14 | module.exports = function* (req, res) { 15 | 16 | var response = yield requestSync.requestSync({ 17 | uri: CGI_PATH['GET_NEWS_DETAIL'], 18 | method: 'POST', 19 | form: { 20 | news_id: req.params.newsid 21 | } 22 | }); 23 | 24 | 25 | const store = configureStore(); 26 | 27 | yield store.dispatch({ 28 | type: 'GET_NEWS_DETAIL_SUCCESS', 29 | data: JSON.parse(response.body), 30 | param:{ 31 | news_id: req.params.newsid, 32 | } 33 | }); 34 | 35 | let path = (process.env.NODE_ENV === '__NODE_DEV__') ? 'src' : 'pub'; 36 | let finalState = store.getState(); 37 | let fileContent = require('../../' + path + '/spa.html'); 38 | 39 | let reactHtml = ""; 40 | match({ routes: routeConfig, location: req.url }, (error, redirectLocation, renderProps) => { 41 | if (renderProps) { 42 | reactHtml = renderToString( 43 | 44 | 45 | 46 | ); 47 | } 48 | else { 49 | res.body = "404"; 50 | } 51 | }); 52 | 53 | fileContent = fileContent 54 | .replace("window.isNode=false;", "window.isNode=true;") 55 | .replace('
', 56 | '
' + reactHtml + '
' 57 | + ''); 58 | 59 | res.body = fileContent; 60 | }; -------------------------------------------------------------------------------- /node/asset/index.js: -------------------------------------------------------------------------------- 1 | const vm = require('vm'), 2 | React = require('react'); 3 | // Root = React.createFactory(require('Root').default); 4 | 5 | var CGI_PATH = require('../config/cgiPath'); 6 | var requestSync = plugin('requestSync'); 7 | 8 | import { renderToString } from 'react-dom/server'; 9 | import { Provider } from 'react-redux'; 10 | import { configureStore } from 'configureStore'; 11 | import { match, RouterContext } from 'react-router'; 12 | import { routeConfig } from 'routes'; 13 | 14 | module.exports = function* (req, res) { 15 | let chlid = 'news_news_top', 16 | refer = 'mobilewwwqqcom', 17 | otype = 'jsonp', 18 | callback = 'getNewsIndexOutput', 19 | t = (new Date()).getTime(); 20 | 21 | let urlParam = '?chlid=' + chlid + '&refer=' + refer 22 | + '&otype=' + otype + '&callback=' + callback 23 | + '&=t' + t; 24 | 25 | 26 | var response = yield requestSync.requestSync({ 27 | uri: CGI_PATH['GET_TOP_NEWS'] + urlParam, 28 | method: 'GET' 29 | }); 30 | 31 | // console.log(JSON.stringify(response.body)); 32 | 33 | let jsonpSandbox = vm.createContext({getNewsIndexOutput: function(r){return r;}}); 34 | let jsonData = vm.runInContext(response.body, jsonpSandbox); 35 | 36 | // console.log(jsonData); 37 | 38 | const store = configureStore(); 39 | 40 | yield store.dispatch({ 41 | type: 'GET_TOP_NEWS_SUCCESS', 42 | data: jsonData, 43 | param:{ 44 | chlid: chlid, 45 | refer: refer, 46 | otype: otype, 47 | callback: callback, 48 | t: t 49 | } 50 | }); 51 | 52 | let path = (process.env.NODE_ENV === '__NODE_DEV__') ? 'src' : 'pub'; 53 | let finalState = store.getState(); 54 | let fileContent = require('../../' + path + '/spa.html'); 55 | 56 | // let reactHtml = renderToString( 57 | // 58 | //
59 | // 60 | // 61 | // 62 | //
63 | //
64 | // ); 65 | // console.log(req.url); 66 | // console.log(routeConfig); 67 | let reactHtml = ""; 68 | match({ routes: routeConfig, location: req.url }, (error, redirectLocation, renderProps) => { 69 | // console.log(error); 70 | // console.log(redirectLocation); 71 | // console.log(renderProps); 72 | if (renderProps) { 73 | reactHtml = renderToString( 74 | 75 | 76 | 77 | ); 78 | } 79 | else { 80 | res.body = "404"; 81 | } 82 | }); 83 | 84 | fileContent = fileContent 85 | .replace("window.isNode=false;", "window.isNode=true;") 86 | .replace('
', 87 | '
' + reactHtml + '
' 88 | + ''); 89 | 90 | res.body = fileContent; 91 | }; -------------------------------------------------------------------------------- /node/common/nodeUtils.js: -------------------------------------------------------------------------------- 1 | global.plugin = function(pkg) { 2 | return require('./' + pkg); 3 | // let pkgMapping = { 4 | // requestSync: require('../common/requestSync').requestSync 5 | // }; 6 | 7 | // return pkgMapping[pkg]; 8 | } -------------------------------------------------------------------------------- /node/common/requestSync.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | 3 | exports.requestSync = function(option) { 4 | return function(callback) { 5 | request(option, function (error, response, body) { 6 | callback(error, response); 7 | }); 8 | }; 9 | } ; -------------------------------------------------------------------------------- /node/config/cgiPath.js: -------------------------------------------------------------------------------- 1 | const baseUrl = 'http://openapi.inews.qq.com/', 2 | baseUrl1 = 'http://view.inews.qq.com/', 3 | baseUrl2 = 'http://localhost:3001/api/' 4 | 5 | const CGI_PATH = { 6 | 'GET_TOP_NEWS': baseUrl + 'getQQNewsIndexAndItems', 7 | 'GET_NEWS_LIST': baseUrl + 'getQQNewsNormalContent', 8 | 'GET_COMMENT_LIST': baseUrl1 + 'getQQNewsComment', 9 | 'GET_NEWS_DETAIL': baseUrl2 + 'getQQNewsDetail', 10 | }; 11 | 12 | module.exports = CGI_PATH; -------------------------------------------------------------------------------- /node/config/mongo.js: -------------------------------------------------------------------------------- 1 | var monk = require('monk'); 2 | module.exports = monk('localhost/hw'); 3 | 4 | 5 | // var MongoClient = require('mongodb').MongoClient; 6 | // var assert = require('assert'); 7 | 8 | // var url = 'mongodb://localhost:27017/hw'; 9 | // MongoClient.connect(url, function(err, db) { 10 | // assert.equal(null, err); 11 | // console.log("Connected correctly to server."); 12 | // db.close(); 13 | // }); -------------------------------------------------------------------------------- /node/config/router.js: -------------------------------------------------------------------------------- 1 | const router = require('koa-router'); 2 | const view = require('../controller/controller'); 3 | 4 | //路由处理,首页指定用index函数处理,但需要先经过validate函数校验 5 | var API = new router(); 6 | 7 | API.get('/api/', view.index) 8 | .get('/api/getQQNewsDetail/', view.detail) 9 | .post('/api/getQQNewsDetail/', view.detail) 10 | .get('/api/getQQNewsIndexAndItems/', view.list) 11 | .post('/api/getQQNewsIndexAndItems/', view.list) 12 | .get('/api/GET_COMMENT_LIST/', view.comment) 13 | .post('/api/GET_COMMENT_LIST/', view.comment) 14 | .get('/spa', view.spa) 15 | .get('/spa/detail/:newsid/:commentid', view.spaDetail) 16 | .get('/spa/comment/:commentid', view.spaComment); 17 | 18 | exports.RULE = API.middleware(); 19 | -------------------------------------------------------------------------------- /node/controller/controller.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var data = require('../model/model'); 4 | // var hw = require('../model/db'); 5 | var requestSync = require('../common/requestSync').requestSync; 6 | var htmlparser = require("htmlparser"); 7 | var htmlToText = require('html-to-text'); 8 | var CGI_PATH = require('../config/cgiPath'); 9 | var nodeUtils = require('../common/nodeUtils'); 10 | 11 | const fs = require('fs'), 12 | path = require('path'); 13 | 14 | exports.index = function* () { 15 | yield* this.render('index', {content: 'tencent news'}); 16 | }; 17 | 18 | // exports.create = function* () { 19 | // yield hw.insert({ 20 | // id: 1, 21 | // content: "heyman" 22 | // }); 23 | // this.body = "success"; 24 | // }; 25 | 26 | // exports.list = function* () { 27 | // var id = this.query.id; 28 | // var numPerPage = 5; 29 | // var blogList = data.blogList; 30 | 31 | // if (id) { 32 | // blogList = blogList.slice((id - 1) * numPerPage, numPerPage * id); 33 | // } 34 | 35 | // this.set('Access-Control-Allow-Origin', 'http://localhost:8008'); 36 | // // this.set('Access-Control-Allow-Origin', 'http://localhost:9000'); 37 | // this.set('Access-Control-Allow-Credentials', true); 38 | // this.body = { 39 | // retcode: 0, 40 | // data: blogList 41 | // }; 42 | // }; 43 | 44 | // exports.detail = function* () { 45 | // var blogDetail = data.blogDetail; 46 | // var id = this.query.id; 47 | // var blog = {}; 48 | 49 | // for (let item of blogDetail) { 50 | // if (item.id == parseInt(id)) { 51 | // blog = item; 52 | // break; 53 | // } 54 | // } 55 | 56 | // this.set('Access-Control-Allow-Origin', 'http://localhost:8008'); 57 | // // this.set('Access-Control-Allow-Origin', 'http://localhost:9000'); 58 | // this.set('Access-Control-Allow-Credentials', true); 59 | // this.body = { 60 | // retcode: 0, 61 | // data: blog 62 | // }; 63 | // }; 64 | // 65 | 66 | exports.list = function* () { 67 | 68 | let query = this.request.query, 69 | urlParam = '?chlid=' + query.chlid + '&refer=' + query.refer 70 | + '&otype=' + query.otype + '&callback=' + query.callback 71 | + '&=t' + query.t; 72 | 73 | 74 | var res = yield requestSync({ 75 | uri: CGI_PATH['GET_TOP_NEWS'] + urlParam, 76 | method: 'GET' 77 | }); 78 | 79 | this.set('Access-Control-Allow-Origin', 'http://localhost:9000'); 80 | this.set('Access-Control-Allow-Credentials', true); 81 | 82 | this.body = res.body; 83 | }; 84 | 85 | exports.detail = function* () { 86 | var res = yield requestSync({ 87 | uri: "http://view.inews.qq.com/a/" + this.request.body.news_id //this.request.body.url 88 | }); 89 | 90 | var text = htmlToText.fromString(res.body, { 91 | ignoreImage: false, 92 | ignoreHref: false, 93 | }); 94 | 95 | this.set('Access-Control-Allow-Origin', 'http://localhost:9000'); 96 | this.set('Access-Control-Allow-Credentials', true); 97 | 98 | this.body = { 99 | ret: 0, 100 | content: text 101 | }; 102 | }; 103 | 104 | exports.comment = function* () { 105 | 106 | let query = this.request.query, 107 | urlParam = '?chlid=' + query.chlid + '&refer=' + query.refer 108 | + '&otype=' + query.otype + '&callback=' + query.callback 109 | + '&=t' + query.t; 110 | 111 | 112 | var res = yield requestSync({ 113 | uri: CGI_PATH['GET_TOP_NEWS'] + urlParam, 114 | method: 'GET' 115 | }); 116 | 117 | this.set('Access-Control-Allow-Origin', 'http://localhost:9000'); 118 | this.set('Access-Control-Allow-Credentials', true); 119 | 120 | this.body = res.body; 121 | }; 122 | 123 | 124 | exports.spa = function* () { 125 | let dir = path.dirname(path.resolve()), 126 | appPath = path.join(dir, '/pub/node/index.js'); 127 | 128 | 129 | if (fs.existsSync(appPath)) { 130 | var ReactRender = require(appPath); 131 | yield ReactRender(this.request, this.response); 132 | this.body = this.response.body; 133 | } 134 | else { 135 | this.body = "spa list"; 136 | } 137 | }; 138 | 139 | exports.spaDetail = function* () { 140 | let dir = path.dirname(path.resolve()), 141 | appPath = path.join(dir, '/pub/node/detail.js'); 142 | 143 | 144 | if (fs.existsSync(appPath)) { 145 | var ReactRender = require(appPath); 146 | this.request.params = this.params; 147 | yield ReactRender(this.request, this.response); 148 | this.body = this.response.body; 149 | } 150 | else { 151 | this.body = "spa detail"; 152 | } 153 | }; 154 | 155 | exports.spaComment = function* () { 156 | let dir = path.dirname(path.resolve()), 157 | appPath = path.join(dir, '/pub/node/comment.js'); 158 | 159 | 160 | if (fs.existsSync(appPath)) { 161 | var ReactRender = require(appPath); 162 | this.request.params = this.params; 163 | yield ReactRender(this.request, this.response); 164 | this.body = this.response.body; 165 | } 166 | else { 167 | this.body = "spa comment"; 168 | } 169 | }; -------------------------------------------------------------------------------- /node/main.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcxfs1991/steamer-react/ad7d574bd66b56e7ac8ff9cfe4e0e230338b48f9/node/main.js -------------------------------------------------------------------------------- /node/model/db.js: -------------------------------------------------------------------------------- 1 | var db = require('../config/mongo'), 2 | wrap = require('co-monk'); 3 | 4 | module.exports = wrap(db.get('hw')); -------------------------------------------------------------------------------- /node/model/model.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | blogList: [ 3 | { 4 | id: 1, 5 | title: 'Blog1', 6 | date: '2016-01-01', 7 | }, 8 | { 9 | id: 2, 10 | title: 'Blog2', 11 | date: '2016-01-22', 12 | }, 13 | { 14 | id: 3, 15 | title: 'Blog3', 16 | date: '2016-01-27', 17 | }, 18 | { 19 | id: 4, 20 | title: 'Blog4', 21 | date: '2016-02-27', 22 | }, 23 | { 24 | id: 5, 25 | title: 'Blog5', 26 | date: '2016-02-23', 27 | }, 28 | { 29 | id: 6, 30 | title: 'Blog6', 31 | date: '2016-01-01', 32 | }, 33 | { 34 | id: 7, 35 | title: 'Blog7', 36 | date: '2016-01-22', 37 | }, 38 | { 39 | id: 8, 40 | title: 'Blog8', 41 | date: '2016-01-27', 42 | }, 43 | { 44 | id: 9, 45 | title: 'Blog9', 46 | date: '2016-02-27', 47 | }, 48 | { 49 | id: 10, 50 | title: 'Blog10', 51 | date: '2016-02-23', 52 | } 53 | ], 54 | blogDetail: [ 55 | { 56 | id: 1, 57 | content: 'blog1' 58 | }, 59 | { 60 | id: 2, 61 | content: 'blog2' 62 | }, 63 | { 64 | id: 3, 65 | content: 'blog3' 66 | }, 67 | { 68 | id: 4, 69 | content: 'blog4' 70 | }, 71 | { 72 | id: 5, 73 | content: 'blog5' 74 | }, 75 | { 76 | id: 6, 77 | content: 'blog6' 78 | }, 79 | { 80 | id: 7, 81 | content: 'blog7' 82 | }, 83 | { 84 | id: 8, 85 | content: 'blog8' 86 | }, 87 | { 88 | id: 9, 89 | content: 'blog9' 90 | }, 91 | { 92 | id: 10, 93 | content: 'blog10' 94 | } 95 | ] 96 | }; -------------------------------------------------------------------------------- /node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "steamer-koa", 3 | "version": "0.0.1", 4 | "description": "koa starter kit", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node-dev ./app.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/lcxfs1991/steamer-koa.git" 12 | }, 13 | "keywords": [ 14 | "koa", 15 | "starter", 16 | "kit", 17 | "boilerplate" 18 | ], 19 | "author": "lcxfs1991", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/lcxfs1991/steamer-koa/issues" 23 | }, 24 | "homepage": "https://github.com/lcxfs1991/steamer-koa#readme", 25 | "dependencies": {}, 26 | "devDependencies": {}, 27 | "dependenciesBk": { 28 | "html-to-text": "^2.1.0", 29 | "htmlparser": "^1.7.7", 30 | "koa": "^1.1.2", 31 | "koa-bodyparser": "^2.0.1", 32 | "koa-logger": "^1.3.0", 33 | "koa-mount": "^1.3.0", 34 | "koa-router": "^5.3.0", 35 | "koa-static": "^2.0.0", 36 | "koa-swig": "^2.1.0", 37 | "koa-views": "^4.0.1", 38 | "request": "^2.72.0", 39 | "monk": "^2.0.0", 40 | "co-monk": "^1.0.0" 41 | }, 42 | "devDependenciesBk": {} 43 | } -------------------------------------------------------------------------------- /node/view/index.html: -------------------------------------------------------------------------------- 1 | {% extends './layout.html' %} 2 | 3 | {% block content %} 4 |
hello world
5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /node/view/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | LeeHey 8 | 9 | 10 | 11 | 12 | {% block content %} 13 | {% endblock %} 14 | 15 | 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "steamer-react", 3 | "version": "0.0.1", 4 | "description": "react-redux-webpack-boilerplate", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "export NODE_ENV=__DEV__&&gulp&&node ./webpack.server.js", 8 | "pub": "gulp sprites&&export NODE_ENV=__PROD__&&webpack", 9 | "dev-node": "export NODE_ENV=__NODE_DEV__&&webpack", 10 | "dev-node-static": "export NODE_ENV=__DEV_NODE__&&webpack", 11 | "pub-node": "export NODE_ENV=__PROD_NODE__&&webpack&&export NODE_ENV=__NODE_PROD__&&webpack" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/lcxfs1991/steamer-react.git" 16 | }, 17 | "keywords": [ 18 | "react", 19 | "redux", 20 | "webpack", 21 | "boilerplate" 22 | ], 23 | "author": "lcxfs1991", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/lcxfs1991/steamer-react/issues" 27 | }, 28 | "homepage": "https://github.com/lcxfs1991/steamer-react#readme", 29 | "dependencies": { 30 | "react": "^15.1.0", 31 | "react-dom": "^15.1.0", 32 | "react-redux": "^4.0.6", 33 | "react-router": "^2.4.1", 34 | "react-router-redux": "^4.0.5", 35 | "react-immutable-render-mixin": "^0.9.5", 36 | "redux": "^3.2.0", 37 | "classnames": "^2.2.3", 38 | "redux-immutable": "^3.0.6", 39 | "redux-thunk": "^1.0.3", 40 | "history": "^3.0.0", 41 | "immutable": "^3.7.6", 42 | "lodash.merge": "^4.4.0" 43 | }, 44 | "devDependencies": { 45 | "babel-core": "^6.1.4", 46 | "babel-loader": "^6.1.0", 47 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 48 | "babel-preset-es2015": "^6.1.4", 49 | "babel-preset-es2015-loose": "^7.0.0", 50 | "babel-preset-react": "^6.1.4", 51 | "clean-webpack-plugin": "^0.1.4", 52 | "copy-webpack-plugin": "^1.1.1", 53 | "css-loader": "^0.22.0", 54 | "exports-loader": "^0.6.2", 55 | "expose-loader": "^0.7.1", 56 | "express": "^4.13.4", 57 | "extract-text-webpack-plugin": "^1.0.1", 58 | "file-loader": "^0.8.4", 59 | "gulp": "^3.9.0", 60 | "gulp-replace": "^0.5.4", 61 | "gulp-zip": "^3.0.2", 62 | "gulp.spritesmith-multi": "^3.0.0", 63 | "html-loader": "^0.3.0", 64 | "html-res-webpack-plugin": "github:lcxfs1991/html-res-webpack-plugin", 65 | "http-proxy": "^1.12.0", 66 | "image-webpack-loader": "^1.8.0", 67 | "imports-loader": "^0.6.3", 68 | "merge-stream": "^1.0.0", 69 | "node-sass": "^3.4.1", 70 | "proxy-middleware": "^0.15.0", 71 | "react-addons-perf": "^15.1.0", 72 | "react-hot-loader": "^1.3.0", 73 | "redux-devtools": "^3.0.1", 74 | "redux-devtools-dock-monitor": "^1.0.1", 75 | "redux-devtools-log-monitor": "^1.0.2", 76 | "run-sequence": "^1.1.5", 77 | "sass-loader": "^3.1.1", 78 | "style-loader": "^0.13.0", 79 | "url-loader": "^0.5.6", 80 | "webpack": "^1.13.0", 81 | "webpack-dev-middleware": "^1.6.1", 82 | "webpack-dev-server": "^1.14.1", 83 | "webpack-hot-middleware": "^2.10.0", 84 | "webpack-md5-hash": "^0.0.5", 85 | "yargs": "^4.7.0", 86 | "ignore-loader": "^0.1.1" 87 | } 88 | } -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcxfs1991/steamer-react/ad7d574bd66b56e7ac8ff9cfe4e0e230338b48f9/src/.DS_Store -------------------------------------------------------------------------------- /src/css/common/common.scss: -------------------------------------------------------------------------------- 1 | /* let's clear some floats */ 2 | .clearfix:before, 3 | .clearfix:after { 4 | display: table; 5 | content: " "; 6 | } 7 | .clearfix:after { 8 | clear: both; 9 | } 10 | 11 | .hide { 12 | display: none; 13 | } 14 | 15 | .none { 16 | display: none; 17 | } 18 | 19 | .text-overflow-ellipsis { 20 | overflow: hidden; 21 | 22 | white-space: nowrap; 23 | text-overflow: ellipsis; 24 | } 25 | 26 | /*border-1px 部分*/ 27 | .border-1px { 28 | position: relative; 29 | } 30 | .border-1px:before, 31 | .border-1px:after { 32 | position: absolute; 33 | left: 0; 34 | 35 | display: block; 36 | width: 100%; 37 | 38 | content: " "; 39 | 40 | border-top: 1px solid #c8c7cc; 41 | } 42 | .border-1px:before { 43 | top: 0; 44 | 45 | display: none; 46 | } 47 | .border-1px:after { 48 | bottom: 0; 49 | } 50 | @media (-webkit-min-device-pixel-ratio:1.5), (min-device-pixel-ratio: 1.5) { 51 | .border-1px:after, 52 | .border-1px:before { 53 | -webkit-transform: scaleY(.7); 54 | transform: scaleY(.7); 55 | -webkit-transform-origin: 0 0; 56 | } 57 | .border-1px:after { 58 | -webkit-transform-origin: left bottom; 59 | } 60 | } 61 | 62 | @media (-webkit-min-device-pixel-ratio:2), (min-device-pixel-ratio: 2) { 63 | .border-1px:after, 64 | .border-1px:before { 65 | -webkit-transform: scaleY(.5); 66 | transform: scaleY(.5); 67 | } 68 | } 69 | 70 | 71 | /** 1px border 四条边都可以指定 */ 72 | .ui-border-1px { 73 | position: relative; 74 | } 75 | .ui-border-1px:after { 76 | position: absolute; 77 | top: 0; 78 | right: 0; 79 | bottom: 0; 80 | left: 0; 81 | 82 | display: block; 83 | 84 | content: ""; 85 | -webkit-transform: scale(1); 86 | transform: scale(1); 87 | -webkit-transform-origin: 0 0; 88 | transform-origin: 0 0; 89 | pointer-events: none; 90 | } 91 | @media only screen and (-webkit-min-device-pixel-ratio: 2) { 92 | .ui-border-1px:after { 93 | position: absolute; 94 | top: 0; 95 | right: -100%; 96 | bottom: -100%; 97 | left: 0; 98 | 99 | display: block; 100 | 101 | content: ""; 102 | -webkit-transform: scale(.5); 103 | transform: scale(.5); 104 | -webkit-transform-origin: 0 0; 105 | transform-origin: 0 0; 106 | pointer-events: none; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/css/common/icon.scss: -------------------------------------------------------------------------------- 1 | @import"../sprites/sprites"; -------------------------------------------------------------------------------- /src/css/common/reset.scss: -------------------------------------------------------------------------------- 1 | /* 2 | HTML5 Reset :: style.css 3 | ---------------------------------------------------------- 4 | We have learned much from/been inspired by/taken code where offered from: 5 | 6 | Eric Meyer :: http://meyerweb.com 7 | HTML5 Doctor :: http://html5doctor.com 8 | and the HTML5 Boilerplate :: http://html5boilerplate.com 9 | 10 | -------------------------------------------------------------------------------*/ 11 | 12 | /* Let's default this puppy out 13 | -------------------------------------------------------------------------------*/ 14 | html, 15 | body { 16 | font-family: -apple-system-font,"黑体", "Helvetica Neue",Helvetica,STHeiTi,sans-serif; 17 | 18 | -webkit-user-select: none; 19 | 20 | -webkit-user-drag: none; 21 | } 22 | 23 | html, 24 | body, 25 | body div, 26 | span, 27 | object, 28 | iframe, 29 | h1, 30 | h2, 31 | h3, 32 | h4, 33 | h5, 34 | h6, 35 | p, 36 | blockquote, 37 | pre, 38 | abbr, 39 | address, 40 | cite, 41 | code, 42 | del, 43 | dfn, 44 | em, 45 | img, 46 | ins, 47 | kbd, 48 | q, 49 | samp, 50 | small, 51 | strong, 52 | sub, 53 | sup, 54 | var, 55 | b, 56 | i, 57 | dl, 58 | dt, 59 | dd, 60 | ol, 61 | ul, 62 | li, 63 | fieldset, 64 | form, 65 | label, 66 | legend, 67 | table, 68 | caption, 69 | tbody, 70 | tfoot, 71 | thead, 72 | tr, 73 | th, 74 | td, 75 | article, 76 | aside, 77 | figure, 78 | footer, 79 | header, 80 | menu, 81 | nav, 82 | section, 83 | time, 84 | mark, 85 | audio, 86 | video, 87 | details, 88 | summary { 89 | padding: 0; 90 | margin: 0; 91 | 92 | font-size: 100%; 93 | font-weight: normal; 94 | 95 | vertical-align: baseline; 96 | 97 | border: 0; 98 | background: transparent; 99 | } 100 | 101 | article, 102 | aside, 103 | figure, 104 | footer, 105 | header, 106 | nav, 107 | section, 108 | details, 109 | summary { 110 | display: block; 111 | } 112 | 113 | /* Removes webkit border when the element is on focus */ 114 | a, 115 | a:active, 116 | a:focus, 117 | button, 118 | button:active, 119 | input, 120 | input:focus, 121 | select, 122 | select:focus, 123 | textarea, 124 | textarea:focus { 125 | outline: none; 126 | 127 | -webkit-tap-highlight-color: rgba(255, 255, 255, 0); 128 | } 129 | 130 | /* Handle box-sizing while better addressing child elements: 131 | http://css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice/ */ 132 | html { 133 | box-sizing: border-box; 134 | } 135 | 136 | *, 137 | *:before, 138 | *:after { 139 | box-sizing: inherit; 140 | } 141 | 142 | /* consider resetting the default cursor: https://gist.github.com/murtaugh/5247154 */ 143 | 144 | /* Responsive images and other embedded objects 145 | Note: keeping IMG here will cause problems if you're using foreground images as sprites. 146 | If this default setting for images is causing issues, you might want to replace it with a .responsive class instead. */ 147 | img, 148 | object, 149 | embed { 150 | max-width: 100%; 151 | } 152 | 153 | /* force a vertical scrollbar to prevent a jumpy page */ 154 | html { 155 | overflow-y: scroll; 156 | } 157 | 158 | /* we use a lot of ULs that aren't bulleted. 159 | don't forget to restore the bullets within content. */ 160 | ul { 161 | list-style: none; 162 | } 163 | 164 | blockquote, 165 | q { 166 | quotes: none; 167 | } 168 | 169 | blockquote:before, 170 | blockquote:after, 171 | q:before, 172 | q:after { 173 | content: ""; 174 | content: none; 175 | } 176 | 177 | a { 178 | padding: 0; 179 | margin: 0; 180 | 181 | font-size: 100%; 182 | 183 | vertical-align: baseline; 184 | 185 | background: transparent; 186 | } 187 | 188 | del { 189 | text-decoration: line-through; 190 | } 191 | 192 | abbr[title], 193 | dfn[title] { 194 | cursor: help; 195 | 196 | border-bottom: 1px dotted #000; 197 | } 198 | 199 | /* tables still need cellspacing="0" in the markup */ 200 | table { 201 | border-spacing: 0; 202 | border-collapse: collapse; 203 | } 204 | th { 205 | font-weight: bold; 206 | 207 | vertical-align: bottom; 208 | } 209 | td { 210 | font-weight: normal; 211 | 212 | vertical-align: top; 213 | } 214 | 215 | hr { 216 | display: block; 217 | height: 1px; 218 | padding: 0; 219 | margin: 1em 0; 220 | 221 | border: 0; 222 | border-top: 1px solid #ccc; 223 | } 224 | 225 | input, 226 | select { 227 | vertical-align: middle; 228 | } 229 | 230 | pre { 231 | white-space: pre; /* CSS2 */ 232 | white-space: pre-wrap; /* CSS 2.1 */ 233 | white-space: pre-line; /* CSS 3 (and 2.1 as well, actually) */ 234 | word-wrap: break-word; /* IE */ 235 | } 236 | 237 | input[type="radio"] { 238 | vertical-align: text-bottom; 239 | } 240 | input[type="checkbox"] { 241 | vertical-align: bottom; 242 | } 243 | .ie7 input[type="checkbox"] { 244 | vertical-align: baseline; 245 | } 246 | .ie6 input { 247 | vertical-align: text-bottom; 248 | } 249 | 250 | select, 251 | input, 252 | textarea { 253 | font: 99% sans-serif; 254 | } 255 | 256 | table { 257 | font: 100%; 258 | font-size: inherit; 259 | } 260 | 261 | small { 262 | font-size: 85%; 263 | } 264 | 265 | strong { 266 | font-weight: bold; 267 | } 268 | 269 | td, 270 | td img { 271 | vertical-align: top; 272 | } 273 | 274 | /* Make sure sup and sub don't mess with your line-heights http://gist.github.com/413930 */ 275 | sub, 276 | sup { 277 | position: relative; 278 | 279 | font-size: 75%; 280 | line-height: 0; 281 | } 282 | sup { 283 | top: -.5em; 284 | } 285 | sub { 286 | bottom: -.25em; 287 | } 288 | 289 | /* standardize any monospaced elements */ 290 | pre, 291 | code, 292 | kbd, 293 | samp { 294 | font-family: monospace, sans-serif; 295 | } 296 | 297 | /* hand cursor on clickable elements */ 298 | .clickable, 299 | label, 300 | input[type=button], 301 | input[type=submit], 302 | input[type=file], 303 | button { 304 | cursor: pointer; 305 | } 306 | 307 | /* Webkit browsers add a 2px margin outside the chrome of form elements */ 308 | button, 309 | input, 310 | select, 311 | textarea { 312 | margin: 0; 313 | } 314 | 315 | /* make buttons play nice in IE */ 316 | button, 317 | input[type=button] { 318 | width: auto; 319 | overflow: visible; 320 | } 321 | 322 | /* scale images in IE7 more attractively */ 323 | .ie7 img { 324 | -ms-interpolation-mode: bicubic; 325 | } 326 | 327 | /* prevent BG image flicker upon hover 328 | (commented out as usage is rare, and the filter syntax messes with some pre-processors) 329 | .ie6 html {filter: expression(document.execCommand("BackgroundImageCache", false, true));} 330 | */ 331 | -------------------------------------------------------------------------------- /src/css/sprites/list_s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcxfs1991/steamer-react/ad7d574bd66b56e7ac8ff9cfe4e0e230338b48f9/src/css/sprites/list_s.png -------------------------------------------------------------------------------- /src/css/sprites/list_s.scss: -------------------------------------------------------------------------------- 1 | /* 2 | SCSS variables are information about icon's compiled state, stored under its original file name 3 | 4 | .icon-home { 5 | width: $icon-home-width; 6 | } 7 | 8 | The large array-like variables contain all information about a single icon 9 | $icon-home: x y offset_x offset_y width height total_width total_height image_path; 10 | 11 | At the bottom of this section, we provide information about the spritesheet itself 12 | $spritesheet: width height image $spritesheet-sprites; 13 | */ 14 | $icon-name: 'icon'; 15 | $icon-x: 0px; 16 | $icon-y: 60px; 17 | $icon-offset-x: 0px; 18 | $icon-offset-y: -60px; 19 | $icon-width: 238px; 20 | $icon-height: 42px; 21 | $icon-total-width: 238px; 22 | $icon-total-height: 102px; 23 | $icon-image: '../../../css/sprites/list_s.png'; 24 | $icon: (0px, 60px, 0px, -60px, 238px, 42px, 238px, 102px, '../../../css/sprites/list_s.png', 'icon', ); 25 | $logo-news-name: 'logo_news'; 26 | $logo-news-x: 0px; 27 | $logo-news-y: 0px; 28 | $logo-news-offset-x: 0px; 29 | $logo-news-offset-y: 0px; 30 | $logo-news-width: 238px; 31 | $logo-news-height: 60px; 32 | $logo-news-total-width: 238px; 33 | $logo-news-total-height: 102px; 34 | $logo-news-image: '../../../css/sprites/list_s.png'; 35 | $logo-news: (0px, 0px, 0px, 0px, 238px, 60px, 238px, 102px, '../../../css/sprites/list_s.png', 'logo_news', ); 36 | $sp-list-s-width: 238px; 37 | $sp-list-s-height: 102px; 38 | $sp-list-s-image: '../../../css/sprites/list_s.png'; 39 | $sp-list-s-sprites: ($icon, $logo-news, ); 40 | $sp-list-s: (238px, 102px, '../../../css/sprites/list_s.png', $sp-list-s-sprites, ); 41 | 42 | /* 43 | The provided mixins are intended to be used with the array-like variables 44 | 45 | .icon-home { 46 | @include sprite-width($icon-home); 47 | } 48 | 49 | .icon-email { 50 | @include sprite($icon-email); 51 | } 52 | 53 | Here are example usages in HTML: 54 | 55 | `display: block` sprite: 56 |
57 | 58 | `display: inline-block` sprite: 59 | 60 | */ 61 | @mixin sprite-width($sprite) { 62 | width: nth($sprite, 5); 63 | } 64 | 65 | @mixin sprite-height($sprite) { 66 | height: nth($sprite, 6); 67 | } 68 | 69 | @mixin sprite-position($sprite) { 70 | $sprite-offset-x: nth($sprite, 3); 71 | $sprite-offset-y: nth($sprite, 4); 72 | background-position: $sprite-offset-x / 2 $sprite-offset-y / 2; 73 | } 74 | 75 | @mixin sprite-image($sprite) { 76 | $sprite-image: nth($sprite, 9); 77 | background-image: url(#{$sprite-image}); 78 | } 79 | 80 | // nth是指#block "sprites"第几个参数,可以输出$sprite来查看, 81 | @mixin sprite($sprite) { 82 | @include sprite-image($sprite); 83 | @include sprite-position($sprite); 84 | // @include sprite-width($sprite); 85 | // @include sprite-height($sprite); 86 | background-size: nth($sprite, 7) / 2 auto; 87 | } 88 | 89 | /* 90 | The `sprites` mixin generates identical output to the CSS template 91 | but can be overridden inside of SCSS 92 | 93 | @include sprites($spritesheet-sprites); 94 | */ 95 | @mixin sprites($sprites) { 96 | @each $sprite in $sprites { 97 | $sprite-name: nth($sprite, 10); 98 | .#{$sprite-name} { 99 | @include sprite($sprite); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcxfs1991/steamer-react/ad7d574bd66b56e7ac8ff9cfe4e0e230338b48f9/src/favicon.ico -------------------------------------------------------------------------------- /src/img/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcxfs1991/steamer-react/ad7d574bd66b56e7ac8ff9cfe4e0e230338b48f9/src/img/.DS_Store -------------------------------------------------------------------------------- /src/img/sprites/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcxfs1991/steamer-react/ad7d574bd66b56e7ac8ff9cfe4e0e230338b48f9/src/img/sprites/.DS_Store -------------------------------------------------------------------------------- /src/img/sprites/list_s/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcxfs1991/steamer-react/ad7d574bd66b56e7ac8ff9cfe4e0e230338b48f9/src/img/sprites/list_s/.DS_Store -------------------------------------------------------------------------------- /src/img/sprites/list_s/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcxfs1991/steamer-react/ad7d574bd66b56e7ac8ff9cfe4e0e230338b48f9/src/img/sprites/list_s/icon.png -------------------------------------------------------------------------------- /src/img/sprites/list_s/logo_news.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcxfs1991/steamer-react/ad7d574bd66b56e7ac8ff9cfe4e0e230338b48f9/src/img/sprites/list_s/logo_news.png -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 腾讯新闻 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/js/common/immutable-pure-render-decorator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * pure render decorator immutable版 3 | */ 4 | 'use strict'; 5 | 6 | import { is } from 'immutable'; 7 | 8 | let hasOwnProperty = Object.prototype.hasOwnProperty; 9 | 10 | /** 11 | * Performs equality by iterating through keys on an object and returning false 12 | * when any key has values which are not strictly equal between the arguments. 13 | * Returns true when the values of all keys are strictly equal. 14 | */ 15 | function shallowEqual(objA, objB) { 16 | if (objA === objB || is(objA, objB)) { 17 | return true; 18 | } 19 | 20 | if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) { 21 | return false; 22 | } 23 | 24 | let keysA = Object.keys(objA); 25 | let keysB = Object.keys(objB); 26 | 27 | if (keysA.length !== keysB.length) { 28 | return false; 29 | } 30 | 31 | // Test for A's keys different from B. 32 | let bHasOwnProperty = hasOwnProperty.bind(objB); 33 | for (let i = 0; i < keysA.length; i++) { 34 | if (!bHasOwnProperty(keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]])) { 35 | return false; 36 | } 37 | } 38 | 39 | return true; 40 | } 41 | 42 | /** 43 | * Does a shallow comparison for props and state. 44 | * See ReactComponentWithPureRenderMixin 45 | */ 46 | function shallowCompare(instance, nextProps, nextState) { 47 | return !shallowEqual(instance.props, nextProps) || !shallowEqual(instance.state, nextState); 48 | } 49 | 50 | /** 51 | * Tells if a component should update given it's next props 52 | * and state. 53 | * 54 | * @param object nextProps Next props. 55 | * @param object nextState Next state. 56 | */ 57 | function shouldComponentUpdate(nextProps, nextState) { 58 | return shallowCompare(this, nextProps, nextState); 59 | } 60 | 61 | /** 62 | * Makes the given component "pure". 63 | * 64 | * @param object component Component. 65 | */ 66 | function pureRenderDecorator(component) { 67 | component.prototype.shouldComponentUpdate = shouldComponentUpdate; 68 | } 69 | 70 | 71 | module.exports = pureRenderDecorator; -------------------------------------------------------------------------------- /src/js/common/net.js: -------------------------------------------------------------------------------- 1 | /* @example 2 | net.ajax({ 3 | url: baseUrl + "get_material_info.fcg", 4 | param: data, 5 | type: 'GET', 6 | success: function(data){ 7 | // alert(data); 8 | }, 9 | error: function(xhr){ 10 | } 11 | }); 12 | 13 | **/ 14 | 15 | function ajax(options) { 16 | let xhr = new XMLHttpRequest(), 17 | url = options.url, 18 | paramObj = options.param, 19 | success_cb = options.success, 20 | error_cb = options.error, 21 | uploadProgress = options.uploadProgress, 22 | method = options.type || 'GET'; 23 | method = method.toUpperCase(); 24 | 25 | let cgiSt = Date.now(); 26 | 27 | let onDataReturn = data => { 28 | if(data.ret === 0 || data.ret === -1) { 29 | success_cb && success_cb(data); 30 | } 31 | else { 32 | error_cb && error_cb(data); 33 | } 34 | }; 35 | 36 | // 如果本地已经从别的地方获取到数据,就不用请求了 37 | if(options.localData) { 38 | onDataReturn(options.localData); 39 | return; 40 | } 41 | 42 | try{ 43 | xhr.onreadystatechange=function() { 44 | if (xhr.readyState==4) { 45 | if(xhr.status==200) { 46 | let data = JSON.parse(xhr.responseText); 47 | onDataReturn(data); 48 | 49 | } 50 | else { 51 | error_cb && error_cb({ 52 | retcode: xhr.status 53 | }); 54 | 55 | } 56 | } 57 | }; 58 | 59 | let paramArray = [], paramString = ''; 60 | for(let key in paramObj){ 61 | paramArray.push(key + '=' + encodeURIComponent(paramObj[key])); 62 | } 63 | 64 | if (method === 'FORM') { 65 | let formData = new FormData(); 66 |     formData.append('file', paramObj['file']); 67 |     formData.append('bkn', bkn); 68 | xhr.upload.onprogress = function(e) { 69 | if (e.lengthComputable) { 70 | uploadProgress(e.loaded, e.total); 71 | } 72 | }; 73 | xhr.open('POST', url); 74 | xhr.withCredentials = true; 75 |      xhr.send(formData); 76 | } 77 | else if (method === 'JSONP') { 78 | method = 'GET'; 79 | 80 | if (!paramObj['callback']) { 81 | error_cb && error_cb({ret: -1}); 82 | } 83 | 84 | window[paramObj['callback']] = function(data) { 85 | onDataReturn(data); 86 | }; 87 | url += (url.indexOf('?') > -1 ? '&' : '?') + paramArray.join('&'); 88 | var script = document.createElement("script"); 89 | var head = document.getElementsByTagName("head")[0]; 90 | script.src = url; 91 | head.appendChild(script); 92 | } 93 | else { 94 | 95 | if(method === 'GET'){ 96 | url += (url.indexOf('?') > -1 ? '&' : '?') + paramArray.join('&'); 97 | } 98 | 99 | xhr.open(method,url,true); 100 | xhr.withCredentials = true; 101 | xhr.setRequestHeader('Content-type','application/x-www-form-urlencoded'); 102 | xhr.send(method === 'POST' ? paramArray.join('&') : ''); 103 | } 104 | 105 | } catch (e){ 106 | console.error(e); 107 | } 108 | } 109 | 110 | let net = { 111 | ajax 112 | }; 113 | 114 | export default net; 115 | -------------------------------------------------------------------------------- /src/js/common/pure-render-decorator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * pure render 非immutable版 3 | */ 4 | 5 | 'use strict'; 6 | 7 | var maxDep = 6; // 比较的最大深度 8 | 9 | /** 10 | * [type utils] 11 | * @type {Array} 12 | */ 13 | var jsType = ["Boolean", "Number", "String", "Function", "Array", "Date", "RegExp", "Object", "Error"]; 14 | var dUtil = {}; 15 | 16 | for (var i = 0; i < jsType.length; i++) { 17 | (function(k) { 18 | dUtil['is' + jsType[k]] = function(obj) { 19 | return Object.prototype.toString.call(obj) === '[object ' + jsType[k] + ']'; 20 | }; 21 | } 22 | )(i); 23 | } 24 | 25 | var hasOwnProperty = Object.prototype.hasOwnProperty; 26 | 27 | /** 28 | * [value compare] 29 | * @param {[type]} valA [description] 30 | * @param {[type]} valB [description] 31 | * @param {[type]} depth [description] 32 | * @return {[type]} [description] 33 | */ 34 | function valCompare(valA, valB, depth) { 35 | 36 | if (dUtil.isFunction(valA)) { 37 | if (valA.hasOwnProperty('name') && valB.hasOwnProperty('name') 38 | && valA.name === valB.name) { 39 | return true; 40 | } 41 | return false; 42 | } 43 | 44 | if (dUtil.isString(valA) || dUtil.isNumber(valA) || dUtil.isBoolean(valA) || dUtil.isDate(valA)) { 45 | if (valA !== valB) { 46 | return false; 47 | } 48 | return true; 49 | } 50 | 51 | if (dUtil.isObject(valA) || dUtil.isArray(valA)) { 52 | return deepEqual(valA, valB, depth); 53 | } 54 | 55 | if (valA !== valB) { 56 | return false; 57 | } 58 | 59 | return true; 60 | } 61 | 62 | /** 63 | * [Not to be compared properties] 64 | * @param {[type]} key [description] 65 | * @return {[type]} [description] 66 | */ 67 | function skipKeys(key) { 68 | var keyMaps = { 69 | '$$typeof': 1, 70 | '_owner': 1, 71 | '_store': 1, 72 | '_self': 1, 73 | '_source': 1, 74 | }; 75 | 76 | if (keyMaps[key]) { 77 | return true; 78 | } 79 | } 80 | 81 | 82 | /** 83 | * [test whether two values are equal] 84 | * @param {[type]} objA [description] 85 | * @param {[type]} objB [description] 86 | * @param {[type]} depth [description] 87 | * @return {[type]} [description] 88 | */ 89 | function deepEqual(objA, objB, depth) { 90 | 91 | if (depth > maxDep) { 92 | return false; 93 | } 94 | 95 | ++depth; 96 | 97 | if (!dUtil.isObject(objA) && !dUtil.isArray(objB)) { 98 | if (!valCompare(objA, objB)) { 99 | return false; 100 | } 101 | } 102 | 103 | var keysA = Object.keys(objA); 104 | var keysB = Object.keys(objB); 105 | 106 | if (keysA.length !== keysB.length) { 107 | return false; 108 | } 109 | 110 | for (var i = 0; i < keysA.length; i++) { 111 | 112 | var comPareValA = objA[keysA[i]], 113 | comPareValB = objB[keysB[i]]; 114 | 115 | if (keysA[0] === '$$typeof' && keysA[i] === 'children') { 116 | return true; 117 | } 118 | else if (keysA[0] === '$$typeof' && skipKeys(keysA[i])) { 119 | continue; 120 | } 121 | 122 | var bHasOwnProperty = hasOwnProperty.bind(objB); 123 | if (!bHasOwnProperty(keysA[i])) { 124 | return false; 125 | } 126 | 127 | if (!valCompare(comPareValA, comPareValB, depth)) { 128 | return false; 129 | } 130 | 131 | } 132 | 133 | return true; 134 | } 135 | 136 | /** 137 | * [compare props and state] 138 | * @param {[type]} instance [description] 139 | * @param {[type]} nextProps [description] 140 | * @param {[type]} nextState [description] 141 | * @return {[type]} [description] 142 | */ 143 | function deepCompare(instance, nextProps, nextState) { 144 | var result = !deepEqual(instance.props, nextProps, 1) || !deepEqual(instance.state, nextState, 1); 145 | return result; 146 | } 147 | 148 | /** 149 | * [rewite shouldComponentUpdate] 150 | * @param {[type]} nextProps [description] 151 | * @param {[type]} nextState [description] 152 | * @return {[type]} [description] 153 | */ 154 | function shouldComponentUpdate(nextProps, nextState) { 155 | return deepCompare(this, nextProps, nextState); 156 | } 157 | 158 | /** 159 | * [decorator wrapper] 160 | * @param {[type]} component [description] 161 | * @return {[type]} [description] 162 | */ 163 | function pureRenderDecorator(component) { 164 | component.prototype.shouldComponentUpdate = shouldComponentUpdate; 165 | } 166 | 167 | 168 | module.exports = pureRenderDecorator; -------------------------------------------------------------------------------- /src/js/common/utils.js: -------------------------------------------------------------------------------- 1 | var type = ["Boolean", "Number", "String", "Function", "Array", "Date", "RegExp", "Object", "Error"]; 2 | 3 | var is = {}; 4 | for (var i = 0; i < type.length; i++) { 5 | (function(k) { 6 | is[type[k]] = function(obj) { 7 | return Object.prototype.toString.call(obj) === '[object ' + type[k] + ']'; 8 | }; 9 | } 10 | )(i); 11 | } 12 | 13 | export function _stringify(val) { 14 | var returnVal = is.Object(val) ? JSON.stringify(val) : val; 15 | return returnVal; 16 | } 17 | 18 | export function _parse(val) { 19 | var returnVal = JSON.parse(val); 20 | returnVal = is.Object(returnVal) ? returnVal : val; 21 | return returnVal; 22 | } 23 | 24 | // 正则表达式网站 http://www.regexr.com/ 25 | export function setCookie(key, val, days, path, domain) { 26 | var expire = new Date(); 27 | expire.setTime(expire.getTime() + (days ? 3600000 * 24 * days : 30 * 24 * 60 * 60 * 1000)); // 默认1个月 28 | document.cookie = key + '=' + encodeURIComponent(_stringify(val)) + ';expires=' + expire.toGMTString() + ';path=' + (path ? path : '/') + ';' + (domain ? ('domain=' + domain + ';') : ''); 29 | } 30 | 31 | export function delCookie(key, path, domain) { 32 | var expires = new Date(0); 33 | document.cookie = key + '=;expires=' + expires.toUTCString() + ';path=' + (path ? path : '/') + ';' + (domain ? ('domain=' + domain + ';') : ''); 34 | } 35 | 36 | export function getCookie(key) { 37 | var r = new RegExp("(?:^|;+|\\s+)" + key + "=([^;]*)"); 38 | var m = window.document.cookie.match(r); 39 | return (!m ? "" : m[1]); 40 | } 41 | 42 | // 设置缓存 43 | export function setItem(key, val){ 44 | val = _stringify(val); 45 | if (typeof(Storage) !== 'undefined') { 46 | localStorage.setItem(key,val); 47 | } 48 | else { 49 | setCookie(key,val); 50 | } 51 | } 52 | // 获取缓存 53 | export function getItem(key){ 54 | if (typeof(Storage) !== 'undefined') { 55 | return localStorage.getItem(key) && localStorage.getItem(key); 56 | } 57 | else { 58 | return getCookie(key); 59 | } 60 | } 61 | 62 | // 删除缓存 63 | export function delItem(key) { 64 | if (typeof(Storage) !== 'undefined') { 65 | delete localStorage[key]; 66 | } 67 | else { 68 | deleteCookie(key); 69 | } 70 | } 71 | 72 | export function getHash(key) { 73 | var m = window.location.hash.match(new RegExp('(#|&)' + key + '=([^&#]*)(#|&|$)')); 74 | return !m ? "" : decodeURIComponent(m[2]); 75 | } 76 | 77 | export function getQuery(key) { 78 | var m = window.location.search.match(new RegExp('(\\?|&)'+ key + '=([^&]*)(#|&|$)')); 79 | return !m ? "":decodeURIComponent(m[2]); 80 | } 81 | 82 | export function getUrlParam(key) { 83 | var m = window.location.search.match(new RegExp('(\\?|#|&)'+ key + '=([^&]*)(#|&|$)')); 84 | 85 | if (!m) { 86 | m = window.location.hash.match(new RegExp('(#|&)' + key + '=([^&#]*)(#|&|$)')); 87 | } 88 | 89 | return !m ? "":decodeURIComponent(m[2]); 90 | } 91 | 92 | /** 93 | * html实体编码 94 | * @param {String} str html文本 95 | * @return {String} 经html实体编码后的html文本 96 | */ 97 | export function encodeHTML(str) { 98 | //> 实体标签 99 | //" Unicode 编码(可以用charCodeAt方法查看某字符对应的unicode编码) 100 | var s = ""; 101 | if(!str || str.length == 0) return ""; 102 | s = str.replace(/&/g, "&"); 103 | s = s.replace(//g, ">"); 105 | s = s.replace(/\'/g, "'"); 106 | s = s.replace(/\"/g, """); 107 | //空格和换行其实可以不转 108 | s = s.replace(/ /g, " "); 109 | s = s.replace(/\n/g, "
"); 110 | return s; 111 | } 112 | 113 | /** 114 | * html实体编码转义 115 | * @param {String} str html文本 116 | * @return {String} 经html实体编码转义后的html文本 117 | */ 118 | export function decodeHTML(str) { 119 | var s = ""; 120 | if (str.length == 0) return ""; 121 | s = str.replace(/&/g, "&"); 122 | s = s.replace(/</g, "<"); 123 | s = s.replace(/>/g, ">"); 124 | s = s.replace(/'/g, "\'"); 125 | s = s.replace(/"/g, "\""); 126 | s = s.replace(/ /g, " "); 127 | s = s.replace(/
/g, "\n"); 128 | return s; 129 | } 130 | 131 | /** 132 | * 获取日期展示 133 | * @param {Number} timestamp 日期时间戳 134 | * @param {Number} strType 日期显示格式类型,1:[4月21号 星期一 8:00], 2:[2015-7-12 星期一], 3:[07-10 07:30],所有非当前年份日期显示年份 135 | * @param {Number} serverTime 服务器时间 136 | * @param {Boolean} noFixTimezone 是否不需要时区校正 137 | * @return {String} 格式化日期 138 | */ 139 | var formatDate = (function() { 140 | // 修正为北京时间 141 | // 8 * 60 GMT+0800 单位为分 142 | var timezoneOffsetGMT8 = 8 * 60; 143 | // 系统时区 分 (包含夏令时) 144 | var timezoneOffset = (new Date()).getTimezoneOffset(); 145 | // 转换成秒 146 | var timezoneDiff = (timezoneOffsetGMT8 + timezoneOffset) * 60; 147 | 148 | function fixTimezone(timestamp, isFormatToDate){ 149 | // 单位为秒 150 | // 北京时间直接返回 151 | if (timezoneDiff === 0) return parseInt(timestamp); 152 | return parseInt(parseInt(timestamp) + timezoneDiff * (isFormatToDate ? 1 : -1)); 153 | } 154 | 155 | function fillZero(number){ 156 | return ("0"+number).slice(-2,3); 157 | } 158 | 159 | function isYesterday(now, obj) { 160 | var yesterdayString = new Date(now.setDate(now.getDate() - 1)).toDateString(); 161 | return obj.toDateString() === yesterdayString; 162 | } 163 | 164 | // 1:默认显示日期+时间 165 | // 2: 显示日期 166 | // 3: 显示时间 167 | return (function format(timestamp, strType = 1, serverTime = 0, noFixTimezone = false) { 168 | if (!timestamp) { 169 | return ''; 170 | } 171 | 172 | var weekdaymap = ['星期日','星期一','星期二','星期三','星期四','星期五','星期六']; 173 | 174 | var now = serverTime ? 175 | (new Date(noFixTimezone ? serverTime : 176 | fixTimezone(serverTime, true) * 1000)) : 177 | (new Date()), 178 | time = new Date(noFixTimezone ? timestamp : 179 | fixTimezone(timestamp, true) * 1000); 180 | 181 | var formatTime = fillZero(time.getHours()) + ":" + fillZero(time.getMinutes()), 182 | formatDate = "", 183 | year = time.getFullYear(), 184 | month = time.getMonth() + 1, 185 | date = time.getDate(); 186 | 187 | var isCurYear = true; 188 | 189 | strType = strType || 1; 190 | 191 | //判断是否今天 192 | if (now.getFullYear() === year && 193 | now.getMonth() === time.getMonth() && 194 | now.getDate() === date) { 195 | formatDate = "今天"; 196 | } else if (isYesterday(now, time)) { 197 | formatDate = "昨天"; 198 | } else { 199 | // 不是当前年份,都要带上年份显示 200 | if (now.getFullYear() !== year) { 201 | formatDate = year; 202 | isCurYear = false; 203 | } 204 | 205 | switch (strType) { 206 | case 1: 207 | formatDate = (isCurYear ? formatDate : formatDate + '年') + month + '月' + date + '号'; 208 | break; 209 | case 2: 210 | formatDate = year + '-' + month + '-' + date; 211 | break; 212 | case 3: 213 | formatDate = (isCurYear ? formatDate : formatDate + '-') + fillZero(month) + '-' + fillZero(date); 214 | break; 215 | } 216 | } 217 | 218 | switch (strType) { 219 | // 4月21号 星期一 08:00 220 | case 1: 221 | return formatDate + ' ' + weekdaymap[time.getDay()] + ' ' + formatTime; 222 | // 2015-7-12 星期一 223 | case 2: 224 | return formatDate + ' ' + weekdaymap[time.getDay()]; 225 | // 07-10 07:30 226 | case 3: 227 | return formatDate + ' ' + formatTime; 228 | } 229 | }); 230 | })(); 231 | export {formatDate}; 232 | 233 | /** 234 | * [extend 对象继承] 235 | * @param {[Object]} src [源对象] 236 | * @param {[Object]} des [继承对象] 237 | * @param {[Integer]} d [拷贝深度] 238 | */ 239 | export function extend(src, des, d) { 240 | var depth = (d) ? d : 0; 241 | for (var key in src) { 242 | var isObject = is.Object(src[key]); 243 | var isArray = is.Array(src[key]); 244 | if (isObject || isArray) { 245 | if (depth) { 246 | if (isObject) { 247 | des[key] = {}; 248 | extend(src[key], des[key], depth - 1); 249 | } 250 | else if (isArray) { 251 | des[key] = []; 252 | extend(src[key], des[key], depth - 1); 253 | } 254 | } 255 | } 256 | else { 257 | des[key] = src[key]; 258 | } 259 | } 260 | } 261 | 262 | export function platform() { 263 | 264 | let ua = ''; 265 | 266 | if (!isNode) { 267 | ua = navigator.userAgent.toLowerCase(); 268 | } 269 | let _platform = function(os) { 270 | let ver = ('' + (new RegExp(os + '(\\d+((\\.|_)\\d+)*)').exec(ua) || [,0])[1]).replace(/_/g, '.'); 271 | // undefined < 3 === false, but null < 3 === true 272 | return parseFloat(ver) || undefined; 273 | }; 274 | 275 | let os = { 276 | ios: _platform('os '), 277 | android: _platform('android[/ ]'), 278 | pc : !_platform('os ') && !_platform('android[/ ]') 279 | }; 280 | 281 | return os; 282 | } -------------------------------------------------------------------------------- /src/libs/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcxfs1991/steamer-react/ad7d574bd66b56e7ac8ff9cfe4e0e230338b48f9/src/libs/.DS_Store -------------------------------------------------------------------------------- /src/libs/react-dom.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ReactDOM v15.1.0 3 | * 4 | * Copyright 2013-present, Facebook, Inc. 5 | * All rights reserved. 6 | * 7 | * This source code is licensed under the BSD-style license found in the 8 | * LICENSE file in the root directory of this source tree. An additional grant 9 | * of patent rights can be found in the PATENTS file in the same directory. 10 | * 11 | */ 12 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e(require("react"));else if("function"==typeof define&&define.amd)define(["react"],e);else{var f;f="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,f.ReactDOM=e(f.React)}}(function(e){return e.__SECRET_DOM_DO_NOT_USE_OR_YOU_WILL_BE_FIRED}); -------------------------------------------------------------------------------- /src/page/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcxfs1991/steamer-react/ad7d574bd66b56e7ac8ff9cfe4e0e230338b48f9/src/page/.DS_Store -------------------------------------------------------------------------------- /src/page/common/actions/actions.js: -------------------------------------------------------------------------------- 1 | import { API_REQUEST } from '../constants/constants'; 2 | 3 | 4 | export function request(cgiName, params, opts = {}, requiredFields = []) { 5 | return (dispatch, getState) => { 6 | var action = { 7 | 'API': { 8 | cgiName: cgiName, 9 | params: params, 10 | opts: opts 11 | }, 12 | type: API_REQUEST 13 | }; 14 | return dispatch(action); 15 | }; 16 | } -------------------------------------------------------------------------------- /src/page/common/components/scroll/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import pureRender from 'pure-render-decorator'; 3 | import { HW_MINE, HW_ALL } from '../../constants/constants'; 4 | 5 | let ua = ''; 6 | if (!isNode) { 7 | ua = navigator.userAgent.toLowerCase(); 8 | } 9 | 10 | let _platform = function(os) { 11 | let ver = ('' + (new RegExp(os + '(\\d+((\\.|_)\\d+)*)').exec(ua) || [,0])[1]).replace(/_/g, '.'); 12 | // undefined < 3 === false, but null < 3 === true 13 | return parseFloat(ver) || undefined; 14 | }; 15 | let os = { 16 | ios: _platform('os '), 17 | android: _platform('android[/ ]'), 18 | pc : !_platform('os ') && !_platform('android[/ ]') 19 | }; 20 | 21 | require('./index.scss'); 22 | 23 | /** 24 | * 使用方法 25 | * 26 | * 请使用以下HTML嵌套方式 27 | * 28 | * 29 | * 30 | * 31 | * 32 | * 对象参数 33 | * prvScrollTop -> 当前列表上次滚动到的位置 34 | * wrapper -> 滚动的对象 35 | * disable -> 停用 36 | * isEnd -> 列表到底 37 | * 38 | * 方法 39 | * bindScroll -> 滚动绑定 40 | * scrollEvt -> scroll事件回调 41 | * props.loadDataForScroll -> 从上层组件传进来的拉取数据方法 42 | * 43 | * 注意事项 44 | * 1. 记录滚动位置 45 | * 请在放置的组件里面,存放对应列表上次滚动的位置 46 | * 47 | * 2. 还原上次滚动位置 48 | * (1) 如果是双列表滚动,且使用display的block和none切换,则请在放置的组件中,切换列表的方法内,进行还原prvScrollTop 49 | * (2) 如果是双列表滚动,但使用替换的方式切换,则可以通过销毁同时重新创建,然后触发componentWillMount去还原prvScrollTop 50 | * 51 | */ 52 | 53 | @pureRender 54 | export default class Scroll extends Component { 55 | 56 | constructor(props, context) { 57 | super(props, context); 58 | this.state = { 59 | 60 | }; 61 | this.prvScrollTop = 0; 62 | this.wrapper = props.wrapper; 63 | this.bindScroll = this.bindScroll.bind(this); 64 | this.scrollEvt = this.scrollEvt.bind(this); 65 | this.timer = null; 66 | } 67 | 68 | componentWillMount() { 69 | this.prvScrollTop = 0; 70 | } 71 | 72 | componentDidMount() { 73 | this.bindScroll(); 74 | } 75 | 76 | componentDidUpdate(prevProps, prevState) { 77 | 78 | } 79 | 80 | componentWillUnmount() { 81 | this.scrollContainer.removeEventListener('scroll', this.scrollEvt); 82 | } 83 | 84 | bindScroll() { 85 | this.scrollContainer = (os.ios) ? document.querySelector(this.wrapper) : window; 86 | 87 | this.scrollContainer.addEventListener('scroll', this.scrollEvt); 88 | } 89 | 90 | scrollEvt(evt) { 91 | 92 | var isWindow = (this.scrollContainer === window); 93 | 94 | // 延迟计算 95 | this.timer && clearTimeout(this.timer); 96 | this.timer = setTimeout(() => { 97 | if (this.props.disable || this.props.isEnd) { 98 | return; 99 | } 100 | 101 | var scrollEle = (isWindow) ? this.scrollContainer.document : this.scrollContainer; 102 | var scrollTop = (isWindow) ? 103 | scrollEle.body.scrollTop 104 | : scrollEle.scrollTop; 105 | 106 | // 防止向上滚动也拉数据 107 | if (this.prvScrollTop > scrollTop) { 108 | return; 109 | } 110 | this.prvScrollTop = scrollTop; 111 | 112 | var containerHeight = (isWindow) ? scrollEle.documentElement.clientHeight : scrollEle.offsetHeight; 113 | var scrollHeight = (isWindow) ? scrollEle.body.clientHeight : scrollEle.scrollHeight; 114 | 115 | // 条件一: 滚动到最底部才拉数据 116 | // if (scrollTop + winHeight >= clientHeight) { 117 | // 条件二: 滚动到中间拉数据 118 | // console.log(scrollTop, scrollHeight, containerHeight); 119 | 120 | if (scrollTop >= (scrollHeight - containerHeight) / 2) { 121 | this.props.loadDataForScroll && this.props.loadDataForScroll(); 122 | } 123 | 124 | }, 50); 125 | } 126 | 127 | render() { 128 | 129 | console.dev('render Scroll!'); 130 | 131 | let { scrollStyle = null } = this.props; 132 | 133 | return ( 134 |
135 | {this.props.children} 136 |
137 | ) 138 | } 139 | } -------------------------------------------------------------------------------- /src/page/common/components/scroll/index.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcxfs1991/steamer-react/ad7d574bd66b56e7ac8ff9cfe4e0e230338b48f9/src/page/common/components/scroll/index.scss -------------------------------------------------------------------------------- /src/page/common/components/spinner/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import pureRender from 'pure-render-decorator'; 3 | var spin = require('spin'); 4 | 5 | require('./index.scss'); 6 | 7 | @pureRender 8 | export default class Spinner extends Component { 9 | 10 | constructor(props, context) { 11 | super(props, context); 12 | this.state = { 13 | 14 | }; 15 | } 16 | 17 | componentWillMount() { 18 | 19 | } 20 | 21 | componentDidMount() { 22 | var opts = { 23 | lines: 12 // The number of lines to draw 24 | , length: 3 // The length of each line 25 | , width: 2 // The line thickness 26 | , radius: 6 // The radius of the inner circle 27 | , scale: 1.0 // Scales overall size of the spinner 28 | , corners: 1 // Roundness (0..1) 29 | , color: '#777' // #rgb or #rrggbb 30 | , opacity: 1/4 // Opacity of the lines 31 | , rotate: 0 // Rotation offset 32 | , direction: 1 // 1: clockwise, -1: counterclockwise 33 | , speed: 1 // Rounds per second 34 | , trail: 100 // Afterglow percentage 35 | , fps: 20 // Frames per second when using setTimeout() 36 | , zIndex: 2e9 // Use a high z-index by default 37 | , className: 'spin' // CSS class to assign to the element 38 | , top: '50%' // center vertically 39 | , left: '50%' // center horizontally 40 | , shadow: false // Whether to render a shadow 41 | , hwaccel: false // Whether to use hardware acceleration (might be buggy) 42 | , position: 'absolute' // Element positioning 43 | }; 44 | var target = document.getElementById('spin'); 45 | var spinner = new spin(opts).spin(target); 46 | } 47 | 48 | render() { 49 | 50 | console.dev('render spinner'); 51 | 52 | var isShow = this.props.isShow || false; 53 | var spinStyle = { 54 | display: (isShow) ? 'block' : 'none' 55 | }; 56 | 57 | return ( 58 |
59 | ) 60 | } 61 | } -------------------------------------------------------------------------------- /src/page/common/components/spinner/index.scss: -------------------------------------------------------------------------------- 1 | #spin { 2 | position: fixed; 3 | top: 0; 4 | bottom: 0; 5 | left: 0; 6 | right: 0; 7 | margin: auto; 8 | } -------------------------------------------------------------------------------- /src/page/common/components/spinner/spinnerComp.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import pureRender from 'pure-render-decorator'; 3 | var spin = require('spin'); 4 | 5 | require('./index.scss'); 6 | 7 | @pureRender 8 | export default class Spinner extends Component { 9 | 10 | constructor(props, context) { 11 | super(props, context); 12 | this.state = { 13 | 14 | }; 15 | } 16 | 17 | componentWillMount() { 18 | 19 | } 20 | 21 | componentDidMount() { 22 | var opts = { 23 | lines: 12 // The number of lines to draw 24 | , length: 3 // The length of each line 25 | , width: 2 // The line thickness 26 | , radius: 6 // The radius of the inner circle 27 | , scale: 1.0 // Scales overall size of the spinner 28 | , corners: 1 // Roundness (0..1) 29 | , color: '#777' // #rgb or #rrggbb 30 | , opacity: 1/4 // Opacity of the lines 31 | , rotate: 0 // Rotation offset 32 | , direction: 1 // 1: clockwise, -1: counterclockwise 33 | , speed: 1 // Rounds per second 34 | , trail: 100 // Afterglow percentage 35 | , fps: 20 // Frames per second when using setTimeout() 36 | , zIndex: 2e9 // Use a high z-index by default 37 | , className: 'spin' // CSS class to assign to the element 38 | , top: '50%' // center vertically 39 | , left: '50%' // center horizontally 40 | , shadow: false // Whether to render a shadow 41 | , hwaccel: false // Whether to use hardware acceleration (might be buggy) 42 | , position: 'absolute' // Element positioning 43 | }; 44 | var target = document.getElementById('spin'); 45 | var spinner = new spin(opts).spin(target); 46 | } 47 | 48 | render() { 49 | 50 | console.log('render spinner'); 51 | 52 | var isShow = this.props.isShow || false; 53 | var spinStyle = { 54 | display: (isShow) ? 'block' : 'none' 55 | }; 56 | 57 | return ( 58 |
59 | ) 60 | } 61 | } -------------------------------------------------------------------------------- /src/page/common/components/touch/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import merge from 'lodash.merge'; 3 | import pureRender from 'pure-render-decorator'; 4 | 5 | let ua = ''; 6 | if (!isNode) { 7 | ua = navigator.userAgent.toLowerCase(); 8 | } 9 | let _platform = function(os) { 10 | let ver = ('' + (new RegExp(os + '(\\d+((\\.|_)\\d+)*)').exec(ua) || [,0])[1]).replace(/_/g, '.'); 11 | // undefined < 3 === false, but null < 3 === true 12 | return parseFloat(ver) || undefined; 13 | }; 14 | let os = { 15 | ios: _platform('os '), 16 | android: _platform('android[/ ]'), 17 | pc : !_platform('os ') && !_platform('android[/ ]') 18 | }; 19 | 20 | @pureRender 21 | export default class Touch extends Component { 22 | 23 | constructor(props, context) { 24 | super(props, context); 25 | this.state = {}; 26 | 27 | this.touchInfo = { 28 | x: null, 29 | y: null, 30 | x2: null, 31 | y2: null, 32 | start: 0, 33 | last: 0, 34 | isDoubleTap: false, 35 | touchTimeout: null, 36 | tapTimeout: null, 37 | swipeTimeout: null, 38 | longTapTimeout: null 39 | }; 40 | 41 | if (isNode) { 42 | this.devicePixelRatio = 1; 43 | } else { 44 | this.devicePixelRatio = window.devicePixelRatio || 1; 45 | } 46 | this.longTapDelay = 750; 47 | this.maxTapAbsX = 30; 48 | this.maxTapAbsY = os.android ? 5 : 30; 49 | 50 | this.getDefaultTouchInfo = this.getDefaultTouchInfo.bind(this); 51 | this.longTap = this.longTap.bind(this); 52 | this.cancelLongTap = this.cancelLongTap.bind(this); 53 | this.cancelAll = this.cancelAll.bind(this); 54 | 55 | this.calculatePos = this.calculatePos.bind(this); 56 | this.touchStart = this.touchStart.bind(this); 57 | this.touchMove = this.touchMove.bind(this); 58 | this.touchEnd = this.touchEnd.bind(this); 59 | } 60 | 61 | componentWillReceiveProps() { 62 | } 63 | 64 | componentDidMount() { 65 | window.addEventListener('scroll', this.cancelAll, false); 66 | } 67 | 68 | componentWillUnmount() { 69 | window.removeEventListener('scroll', this.cancelAll, false); 70 | } 71 | 72 | getDefaultTouchInfo() { 73 | return { 74 | x: null, 75 | y: null, 76 | x2: null, 77 | y2: null, 78 | start: 0, 79 | last: 0, 80 | isDoubleTap: false, 81 | touchTimeout: null, 82 | tapTimeout: null, 83 | swipeTimeout: null, 84 | longTapTimeout: null 85 | }; 86 | } 87 | 88 | longTap() { 89 | this.touchInfo.longTapTimeout = null; 90 | 91 | if (this.touchInfo.last) { 92 | this.props.onLongTap && this.props.onLongTap(); 93 | this.touchInfo = this.getDefaultTouchInfo(); 94 | } 95 | } 96 | 97 | cancelLongTap() { 98 | this.touchInfo.longTapTimeout && clearTimeout(this.touchInfo.longTapTimeout); 99 | 100 | this.touchInfo.longTapTimeout = null; 101 | } 102 | 103 | cancelAll() { 104 | this.touchInfo.touchTimeout && clearTimeout(this.touchInfo.touchTimeout); 105 | this.touchInfo.tapTimeout && clearTimeout(this.touchInfo.tapTimeout); 106 | this.touchInfo.swipeTimeout && clearTimeout(this.touchInfo.swipeTimeout); 107 | this.touchInfo.longTapTimeout && clearTimeout(this.touchInfo.longTapTimeout); 108 | 109 | this.touchInfo = this.getDefaultTouchInfo(); 110 | } 111 | 112 | calculatePos(e) { 113 | var x = e ? e.touches[0].pageX : this.touchInfo.x2; 114 | var y = e ? e.touches[0].pageY : this.touchInfo.y2; 115 | 116 | if (x === null && y === null) { 117 | return { 118 | deltaX: 0, 119 | deltaY: 0, 120 | absX: 0, 121 | absY: 0 122 | } 123 | } 124 | 125 | var xd = this.touchInfo.x - x; 126 | var yd = this.touchInfo.y - y; 127 | 128 | var axd = Math.abs(xd); 129 | var ayd = Math.abs(yd); 130 | 131 | return { 132 | deltaX: xd, 133 | deltaY: yd, 134 | absX: axd, 135 | absY: ayd 136 | }; 137 | } 138 | 139 | touchStart(e) { 140 | if (e.touches.length > 1) { 141 | return; 142 | } 143 | 144 | let firstTouch = e.touches[0]; 145 | 146 | if (e.touches && e.touches.length === 1 && this.touchInfo.x2) { 147 | // Clear out touch movement data if we have it sticking around 148 | // This can occur if touchcancel doesn't fire due to preventDefault, etc. 149 | merge(this.touchInfo, { 150 | x2: null, 151 | y2: null 152 | }); 153 | } 154 | 155 | let now = Date.now(), 156 | delta = now - (this.touchInfo.last || now); 157 | 158 | this.touchInfo.touchTimeout && clearTimeout(this.touchInfo.touchTimeout); 159 | 160 | if (delta > 0 && delta <= 250) { 161 | merge(this.touchInfo, { 162 | isDoubleTap: true 163 | }); 164 | } 165 | 166 | merge(this.touchInfo, { 167 | start: now, 168 | last: now, 169 | x: firstTouch.pageX, 170 | y: firstTouch.pageY, 171 | longTapTimeout: setTimeout(this.longTap, this.longTapDelay) 172 | }); 173 | } 174 | 175 | touchMove(e) { 176 | this.cancelLongTap(); 177 | 178 | merge(this.touchInfo, { 179 | x2: e.touches[0].pageX, 180 | y2: e.touches[0].pageY 181 | }); 182 | 183 | let pos = this.calculatePos(e); 184 | 185 | if (pos.absX > Math.round(20 / this.devicePixelRatio) && pos.absX > pos.absY) { 186 | e.preventDefault(); 187 | } 188 | } 189 | 190 | touchEnd(e) { 191 | this.cancelLongTap(); 192 | 193 | let pos = this.calculatePos(); 194 | 195 | // swipe 196 | if ((this.touchInfo.x2 && pos.absX > this.maxTapAbsX) || 197 | (this.touchInfo.y2 && pos.absY > this.maxTapAbsY)) { 198 | let time = Date.now() - this.touchInfo.start, 199 | velocity = Math.sqrt(pos.absX * pos.absX + pos.absY * pos.absY) / time, 200 | isFlick = velocity > this.props.flickThreshold; 201 | 202 | e.persist(); 203 | merge(this.touchInfo, { 204 | swipeTimeout: setTimeout(() => { 205 | this.props.onSwipe && this.props.onSwipe(e, pos.deltaX, pos.deltaY, isFlick); 206 | 207 | if (pos.absX > pos.absY) { 208 | if (pos.deltaX > 0) { 209 | this.props.onSwipeLeft && this.props.onSwipeLeft(e, pos.deltaX, isFlick); 210 | } else { 211 | this.props.onSwipeRight && this.props.onSwipeRight(e, pos.deltaX, isFlick); 212 | } 213 | } else { 214 | if (pos.deltaY > 0) { 215 | this.props.onSwipeUp && this.props.onSwipeUp(e, pos.deltaY, isFlick); 216 | } else { 217 | this.props.onSwipeDown && this.props.onSwipeDown(e, pos.deltaY, isFlick); 218 | } 219 | } 220 | 221 | this.touchInfo = this.getDefaultTouchInfo(); 222 | }, 0) 223 | }); 224 | } 225 | // normal tap 226 | else if (this.touchInfo.last) { 227 | // don't fire tap when delta position changed by more than 30 pixels, 228 | // for instance when moving to a point and back to origin 229 | if (pos.absX < this.maxTapAbsX && pos.absY < this.maxTapAbsY) { 230 | // delay by one tick so we can cancel the 'tap' event if 'scroll' fires 231 | // ('tap' fires before 'scroll') 232 | e.persist(); 233 | merge(this.touchInfo, { 234 | tapTimeout: setTimeout(() => { 235 | // trigger universal 'tap' with the option to cancelTouch() 236 | // (cancelTouch cancels processing of single vs double taps for faster 'tap' response) 237 | this.props.onTap && this.props.onTap(e); 238 | 239 | // trigger double tap immediately 240 | if (this.touchInfo.isDoubleTap) { 241 | this.props.onDoubleTap && this.props.onDoubleTap(e); 242 | this.touchInfo = this.getDefaultTouchInfo(); 243 | } 244 | 245 | // trigger single tap after 250ms of inactivity 246 | else { 247 | merge(this.touchInfo, { 248 | touchTimeout: setTimeout(() => { 249 | merge(this.touchInfo, { 250 | touchTimeout: null 251 | }); 252 | this.props.onSingleTap && this.props.onSingleTap(e); 253 | this.touchInfo = this.getDefaultTouchInfo(); 254 | }, 250) 255 | }); 256 | } 257 | }, 0) 258 | }); 259 | } else { 260 | this.touchInfo = this.getDefaultTouchInfo(); 261 | } 262 | } 263 | } 264 | 265 | render() { 266 | // console.log('render Touch'); 267 | return ( 268 |
273 | {this.props.children} 274 |
275 | ) 276 | } 277 | } 278 | 279 | Touch.propTypes = { 280 | onTap: PropTypes.func, 281 | onSingleTap: PropTypes.func, 282 | onDoubleTap: PropTypes.func, 283 | onLongTap: PropTypes.func, 284 | onSwipe: PropTypes.func, 285 | onSwipeUp: PropTypes.func, 286 | onSwipeRight: PropTypes.func, 287 | onSwipeDown: PropTypes.func, 288 | onSwipeLeft: PropTypes.func 289 | }; 290 | Touch.defaultProps = { 291 | flickThreshold: 0.6 292 | }; -------------------------------------------------------------------------------- /src/page/common/components/touch/touchComp.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import objectAssign from 'object-assign'; 3 | import pureRender from 'pure-render-decorator'; 4 | 5 | let ua = ''; 6 | if (!isNode) { 7 | ua = navigator.userAgent.toLowerCase(); 8 | } 9 | let _platform = function(os) { 10 | let ver = ('' + (new RegExp(os + '(\\d+((\\.|_)\\d+)*)').exec(ua) || [,0])[1]).replace(/_/g, '.'); 11 | // undefined < 3 === false, but null < 3 === true 12 | return parseFloat(ver) || undefined; 13 | }; 14 | let os = { 15 | ios: _platform('os '), 16 | android: _platform('android[/ ]'), 17 | pc : !_platform('os ') && !_platform('android[/ ]') 18 | }; 19 | 20 | @pureRender 21 | export default class Touch extends Component { 22 | 23 | constructor(props, context) { 24 | super(props, context); 25 | this.state = {}; 26 | 27 | this.touchInfo = { 28 | x: null, 29 | y: null, 30 | x2: null, 31 | y2: null, 32 | start: 0, 33 | last: 0, 34 | isDoubleTap: false, 35 | touchTimeout: null, 36 | tapTimeout: null, 37 | swipeTimeout: null, 38 | longTapTimeout: null 39 | }; 40 | 41 | if (isNode) { 42 | this.devicePixelRatio = 1; 43 | } else { 44 | this.devicePixelRatio = window.devicePixelRatio || 1; 45 | } 46 | this.longTapDelay = 750; 47 | this.maxTapAbsX = 30; 48 | this.maxTapAbsY = os.android ? 5 : 30; 49 | 50 | this.getDefaultTouchInfo = this.getDefaultTouchInfo.bind(this); 51 | this.longTap = this.longTap.bind(this); 52 | this.cancelLongTap = this.cancelLongTap.bind(this); 53 | this.cancelAll = this.cancelAll.bind(this); 54 | 55 | this.calculatePos = this.calculatePos.bind(this); 56 | this.touchStart = this.touchStart.bind(this); 57 | this.touchMove = this.touchMove.bind(this); 58 | this.touchEnd = this.touchEnd.bind(this); 59 | } 60 | 61 | componentWillReceiveProps() { 62 | } 63 | 64 | componentDidMount() { 65 | window.addEventListener('scroll', this.cancelAll, false); 66 | } 67 | 68 | componentWillUnmount() { 69 | window.removeEventListener('scroll', this.cancelAll, false); 70 | } 71 | 72 | getDefaultTouchInfo() { 73 | return { 74 | x: null, 75 | y: null, 76 | x2: null, 77 | y2: null, 78 | start: 0, 79 | last: 0, 80 | isDoubleTap: false, 81 | touchTimeout: null, 82 | tapTimeout: null, 83 | swipeTimeout: null, 84 | longTapTimeout: null 85 | }; 86 | } 87 | 88 | longTap() { 89 | this.touchInfo.longTapTimeout = null; 90 | 91 | if (this.touchInfo.last) { 92 | this.props.onLongTap && this.props.onLongTap(); 93 | this.touchInfo = this.getDefaultTouchInfo(); 94 | } 95 | } 96 | 97 | cancelLongTap() { 98 | this.touchInfo.longTapTimeout && clearTimeout(this.touchInfo.longTapTimeout); 99 | 100 | this.touchInfo.longTapTimeout = null; 101 | } 102 | 103 | cancelAll() { 104 | this.touchInfo.touchTimeout && clearTimeout(this.touchInfo.touchTimeout); 105 | this.touchInfo.tapTimeout && clearTimeout(this.touchInfo.tapTimeout); 106 | this.touchInfo.swipeTimeout && clearTimeout(this.touchInfo.swipeTimeout); 107 | this.touchInfo.longTapTimeout && clearTimeout(this.touchInfo.longTapTimeout); 108 | 109 | this.touchInfo = this.getDefaultTouchInfo(); 110 | } 111 | 112 | calculatePos(e) { 113 | var x = e ? e.touches[0].pageX : this.touchInfo.x2; 114 | var y = e ? e.touches[0].pageY : this.touchInfo.y2; 115 | 116 | if (x === null && y === null) { 117 | return { 118 | deltaX: 0, 119 | deltaY: 0, 120 | absX: 0, 121 | absY: 0 122 | } 123 | } 124 | 125 | var xd = this.touchInfo.x - x; 126 | var yd = this.touchInfo.y - y; 127 | 128 | var axd = Math.abs(xd); 129 | var ayd = Math.abs(yd); 130 | 131 | return { 132 | deltaX: xd, 133 | deltaY: yd, 134 | absX: axd, 135 | absY: ayd 136 | }; 137 | } 138 | 139 | touchStart(e) { 140 | if (e.touches.length > 1) { 141 | return; 142 | } 143 | 144 | let firstTouch = e.touches[0]; 145 | 146 | if (e.touches && e.touches.length === 1 && this.touchInfo.x2) { 147 | // Clear out touch movement data if we have it sticking around 148 | // This can occur if touchcancel doesn't fire due to preventDefault, etc. 149 | objectAssign(this.touchInfo, { 150 | x2: null, 151 | y2: null 152 | }); 153 | } 154 | 155 | let now = Date.now(), 156 | delta = now - (this.touchInfo.last || now); 157 | 158 | this.touchInfo.touchTimeout && clearTimeout(this.touchInfo.touchTimeout); 159 | 160 | if (delta > 0 && delta <= 250) { 161 | objectAssign(this.touchInfo, { 162 | isDoubleTap: true 163 | }); 164 | } 165 | 166 | objectAssign(this.touchInfo, { 167 | start: now, 168 | last: now, 169 | x: firstTouch.pageX, 170 | y: firstTouch.pageY, 171 | longTapTimeout: setTimeout(this.longTap, this.longTapDelay) 172 | }); 173 | } 174 | 175 | touchMove(e) { 176 | this.cancelLongTap(); 177 | 178 | objectAssign(this.touchInfo, { 179 | x2: e.touches[0].pageX, 180 | y2: e.touches[0].pageY 181 | }); 182 | 183 | let pos = this.calculatePos(e); 184 | 185 | if (pos.absX > Math.round(20 / this.devicePixelRatio) && pos.absX > pos.absY) { 186 | e.preventDefault(); 187 | } 188 | } 189 | 190 | touchEnd(e) { 191 | this.cancelLongTap(); 192 | 193 | let pos = this.calculatePos(); 194 | 195 | // swipe 196 | if ((this.touchInfo.x2 && pos.absX > this.maxTapAbsX) || 197 | (this.touchInfo.y2 && pos.absY > this.maxTapAbsY)) { 198 | let time = Date.now() - this.touchInfo.start, 199 | velocity = Math.sqrt(pos.absX * pos.absX + pos.absY * pos.absY) / time, 200 | isFlick = velocity > this.props.flickThreshold; 201 | 202 | e.persist(); 203 | objectAssign(this.touchInfo, { 204 | swipeTimeout: setTimeout(() => { 205 | this.props.onSwipe && this.props.onSwipe(e, pos.deltaX, pos.deltaY, isFlick); 206 | 207 | if (pos.absX > pos.absY) { 208 | if (pos.deltaX > 0) { 209 | this.props.onSwipeLeft && this.props.onSwipeLeft(e, pos.deltaX, isFlick); 210 | } else { 211 | this.props.onSwipeRight && this.props.onSwipeRight(e, pos.deltaX, isFlick); 212 | } 213 | } else { 214 | if (pos.deltaY > 0) { 215 | this.props.onSwipeUp && this.props.onSwipeUp(e, pos.deltaY, isFlick); 216 | } else { 217 | this.props.onSwipeDown && this.props.onSwipeDown(e, pos.deltaY, isFlick); 218 | } 219 | } 220 | 221 | this.touchInfo = this.getDefaultTouchInfo(); 222 | }, 0) 223 | }); 224 | } 225 | // normal tap 226 | else if (this.touchInfo.last) { 227 | // don't fire tap when delta position changed by more than 30 pixels, 228 | // for instance when moving to a point and back to origin 229 | if (pos.absX < this.maxTapAbsX && pos.absY < this.maxTapAbsY) { 230 | // delay by one tick so we can cancel the 'tap' event if 'scroll' fires 231 | // ('tap' fires before 'scroll') 232 | e.persist(); 233 | objectAssign(this.touchInfo, { 234 | tapTimeout: setTimeout(() => { 235 | // trigger universal 'tap' with the option to cancelTouch() 236 | // (cancelTouch cancels processing of single vs double taps for faster 'tap' response) 237 | this.props.onTap && this.props.onTap(e); 238 | 239 | // trigger double tap immediately 240 | if (this.touchInfo.isDoubleTap) { 241 | this.props.onDoubleTap && this.props.onDoubleTap(e); 242 | this.touchInfo = this.getDefaultTouchInfo(); 243 | } 244 | 245 | // trigger single tap after 250ms of inactivity 246 | else { 247 | objectAssign(this.touchInfo, { 248 | touchTimeout: setTimeout(() => { 249 | objectAssign(this.touchInfo, { 250 | touchTimeout: null 251 | }); 252 | this.props.onSingleTap && this.props.onSingleTap(e); 253 | this.touchInfo = this.getDefaultTouchInfo(); 254 | }, 250) 255 | }); 256 | } 257 | }, 0) 258 | }); 259 | } else { 260 | this.touchInfo = this.getDefaultTouchInfo(); 261 | } 262 | } 263 | } 264 | 265 | render() { 266 | // console.log('render Touch'); 267 | return ( 268 |
273 | {this.props.children} 274 |
275 | ) 276 | } 277 | } 278 | 279 | Touch.propTypes = { 280 | onTap: PropTypes.func, 281 | onSingleTap: PropTypes.func, 282 | onDoubleTap: PropTypes.func, 283 | onLongTap: PropTypes.func, 284 | onSwipe: PropTypes.func, 285 | onSwipeUp: PropTypes.func, 286 | onSwipeRight: PropTypes.func, 287 | onSwipeDown: PropTypes.func, 288 | onSwipeLeft: PropTypes.func 289 | }; 290 | Touch.defaultProps = { 291 | flickThreshold: 0.6 292 | }; -------------------------------------------------------------------------------- /src/page/common/constants/cgiPath.js: -------------------------------------------------------------------------------- 1 | const baseUrl = 'http://openapi.inews.qq.com/', 2 | baseUrl1 = 'http://view.inews.qq.com/', 3 | baseUrl2 = 'http://localhost:3001/api/' 4 | 5 | const CGI_PATH = { 6 | // 'GET_TOP_NEWS': baseUrl + 'getQQNewsIndexAndItems', 7 | 'GET_TOP_NEWS': baseUrl2 + 'getQQNewsIndexAndItems', 8 | 'GET_NEWS_LIST': baseUrl + 'getQQNewsNormalContent', 9 | 'GET_COMMENT_LIST': baseUrl1 + 'getQQNewsComment', 10 | 'GET_NEWS_DETAIL': baseUrl2 + 'getQQNewsDetail', 11 | }; 12 | 13 | module.exports = CGI_PATH; -------------------------------------------------------------------------------- /src/page/common/constants/constants.js: -------------------------------------------------------------------------------- 1 | 2 | export const API_REQUEST = 'API_REQUEST'; 3 | export const GET_NEWS_LIST = 'GET_NEWS_LIST'; 4 | export const GET_TOP_NEWS = 'GET_TOP_NEWS'; 5 | export const GET_COMMENT_LIST = 'GET_COMMENT_LIST'; 6 | export const GET_NEWS_DETAIL = 'GET_NEWS_DETAIL'; -------------------------------------------------------------------------------- /src/page/common/devtools/DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // Exported from redux-devtools 4 | import { createDevTools } from 'redux-devtools'; 5 | 6 | // Monitors are separate packages, and you can make a custom one 7 | import LogMonitor from 'redux-devtools-log-monitor'; 8 | import DockMonitor from 'redux-devtools-dock-monitor'; 9 | 10 | // createDevTools takes a monitor and produces a DevTools component 11 | const DevTools = createDevTools( 12 | // Monitors are individually adjustable with props. 13 | // Consult their repositories to learn about those props. 14 | // Here, we put LogMonitor inside a DockMonitor. 15 | 16 | 17 | 18 | ); 19 | 20 | export default DevTools; -------------------------------------------------------------------------------- /src/page/common/middleware/api.js: -------------------------------------------------------------------------------- 1 | // ajax 2 | import net from '../../../js/common/net'; 3 | import merge from 'lodash.merge'; 4 | import CGI_PATH from '../constants/cgiPath'; 5 | 6 | export default store => next => action => { 7 | 8 | let API_OPT = action['API']; 9 | 10 | if (!API_OPT) { 11 | return next(action); 12 | } 13 | 14 | let ACTION_TYPE = action['type']; 15 | let { cgiName, params, opts = {} } = API_OPT; 16 | let { localData } = opts; 17 | 18 | let { onSuccess, onError, ajaxType = 'GET', param } = params; 19 | 20 | // 触发下一个action 21 | let nextAction = function(type, param, opts) { 22 | action['type'] = type; 23 | action['opts'] = opts; 24 | delete param['onSuccess']; 25 | delete param['onError']; 26 | const nextRequestAction = merge({}, action, param); 27 | return nextRequestAction; 28 | }; 29 | 30 | params.data = null; 31 | // 触发正在请求的action 32 | let result = next(nextAction(cgiName + '_ON', params, opts)); 33 | 34 | net.ajax({ 35 | url: CGI_PATH[cgiName], 36 | type: ajaxType, 37 | param, 38 | localData, 39 | success: data => { 40 | onSuccess && onSuccess(data); 41 | params.data = data; 42 | // 触发请求成功的action 43 | return next(nextAction(cgiName + '_SUCCESS', params, opts)); 44 | }, 45 | error: data => { 46 | 47 | onError && onError(data); 48 | // 触发请求失败的action 49 | return next(nextAction(cgiName + '_ERROR', params, opts)); 50 | } 51 | }); 52 | 53 | return result; 54 | }; 55 | -------------------------------------------------------------------------------- /src/page/common/middleware/logger.js: -------------------------------------------------------------------------------- 1 | const logger = store => next => action => { 2 | let result = next(action); // 返回的也是同样的action值 3 | // console.log('dispatching', action); 4 | // console.log('next state', store.getState()); 5 | return result; 6 | } 7 | 8 | export default logger; -------------------------------------------------------------------------------- /src/page/index/actions/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * action types 3 | */ 4 | 5 | // OTHERS 6 | export const GET_ARGS = 'GET_ARGS'; 7 | 8 | 9 | export const TOGGLE_SPIN_LOADING = 'TOGGLE_SPIN_LOADING'; 10 | export const TOGGLE_LIST_LOADING = 'TOGGLE_LIST_LOADING'; 11 | 12 | export const TABS_UPDATE = 'TABS_UPDATE'; 13 | 14 | export const LIKE_NEWS = 'LIKE_NEWS'; 15 | export const DISLIKE_NEWS = 'DISLIKE_NEWS'; 16 | 17 | /* 18 | * other constants 19 | */ 20 | 21 | 22 | /* 23 | * action creators 24 | */ 25 | 26 | export function getArgs(value) { 27 | return { type: GET_ARGS, value }; 28 | } 29 | 30 | export function toggleListLoading(value) { 31 | return { type: TOGGLE_LIST_LOADING, value }; 32 | } 33 | 34 | export function toggleSpinLoading(value) { 35 | return { type: TOGGLE_SPIN_LOADING, value }; 36 | } 37 | 38 | export function updateActiveTab(value) { 39 | return { type: TABS_UPDATE, value}; 40 | } 41 | 42 | export function likeNews(value) { 43 | return { type: LIKE_NEWS, value }; 44 | } 45 | 46 | export function dislikeNews(value) { 47 | return { type: DISLIKE_NEWS, value }; 48 | } -------------------------------------------------------------------------------- /src/page/index/components/list/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import pureRender from 'pure-render-decorator'; 3 | import classnames from 'classnames'; 4 | import { formatDate } from 'utils'; 5 | import { LATEST_NEWS, LIKE_NEWS} from '../../constants/constants'; 6 | 7 | import Touch from 'touch'; 8 | 9 | require('./index.scss'); 10 | 11 | @pureRender 12 | export default class List extends Component { 13 | 14 | constructor(props, context) { 15 | super(props, context); 16 | this.state = { 17 | activeDelHwId: null, 18 | activeDelBubbleHwId: null 19 | }; 20 | this.jumpToDetail = this.jumpToDetail.bind(this); 21 | this.showLikeBtn = this.showLikeBtn.bind(this); 22 | this.hideLikeBtn = this.hideLikeBtn.bind(this); 23 | this.isClickOnBtn = false; // 是否点击在修改、删除按钮上 24 | this.like = this.like.bind(this); 25 | this.dislike = this.dislike.bind(this); 26 | } 27 | 28 | componentWillMount() { 29 | 30 | } 31 | 32 | componentDidMount() { 33 | window.addEventListener('touchstart', this.hideLikeBtn(), false); 34 | } 35 | 36 | componentWillUnmount() { 37 | window.removeEventListener('touchstart', this.hideLikeBtn(), false); 38 | } 39 | 40 | jumpToDetail(item) { 41 | return (e) => { 42 | if (!this.isClickOnBtn) { 43 | window.location.href = item.url; 44 | } 45 | } 46 | } 47 | 48 | renderNewsIcon(pic) { 49 | return { 50 | "backgroundImage": "url(" + pic + ")", 51 | "backgroundSize": "100%" 52 | } 53 | } 54 | 55 | showLikeBtn(item, e) { 56 | return (e) => { 57 | e.preventDefault(); 58 | 59 | this.setState({ 60 | activeNewsId: item.id 61 | }); 62 | } 63 | } 64 | 65 | hideLikeBtn(e) { 66 | return (e) => { 67 | let target = e.target, 68 | classname = target.className; 69 | 70 | if (this.state.activeNewsId === null) { 71 | return; 72 | } 73 | 74 | this.setState({ 75 | activeNewsId: null, 76 | }); 77 | } 78 | } 79 | 80 | like(item) { 81 | return (e) => { 82 | this.isClickOnBtn = true; 83 | this.props.likeNews(item); 84 | setTimeout(() => { 85 | this.hideLikeBtn(e); 86 | this.isClickOnBtn = false; 87 | }, 20); 88 | } 89 | } 90 | 91 | dislike(item) { 92 | return (e) => { 93 | this.isClickOnBtn = true; 94 | this.props.dislikeNews(item); 95 | setTimeout(() => { 96 | this.hideLikeBtn(e); 97 | this.isClickOnBtn = false; 98 | }, 20); 99 | } 100 | } 101 | 102 | render() { 103 | 104 | console.dev('render List!!'); 105 | 106 | let _this = this; 107 | let prevCreateTime = null; 108 | 109 | let news = this.props.news; 110 | let tabsType = this.props.tabsType; 111 | 112 | let listDataMap = { 113 | [LATEST_NEWS]: 'listLatest', 114 | [LIKE_NEWS] : 'listLike' 115 | }; 116 | 117 | this.listData = news; 118 | 119 | let list = news.map((item, index) => { 120 | return ( 121 |
  • 122 | 123 |
    124 |
    125 |
    126 |
    127 |
    {item.title}
    128 |
    129 |

    {item.des}

    130 |
    131 |
    132 | 134 | {(tabsType === LATEST_NEWS) ? "收藏" : "取消"} 135 | 136 |
    137 |
  • 138 | ) 139 | }); 140 | 141 | let wrapperStyle = { 142 | display: (this.props.tabs === tabsType) ? "block" : "none", 143 | paddingTop: 46, 144 | }; 145 | 146 | return ( 147 |
    148 |
    149 |
      150 | {list} 151 |
    152 |
    153 |
    154 | ) 155 | } 156 | } -------------------------------------------------------------------------------- /src/page/index/components/list/index.scss: -------------------------------------------------------------------------------- 1 | .news-list { 2 | overflow-x: hidden; 3 | width: 100%; 4 | } 5 | 6 | /** 修改1px边框颜色 */ 7 | 8 | .title { 9 | margin: 8px 0 8px 14px; 10 | 11 | font-size: 15px; 12 | 13 | color: #777; 14 | } 15 | 16 | .item { 17 | -webkit-tap-highlight-color: transparent; 18 | &:after { 19 | border-top: 1px solid #dedfe0; 20 | border-bottom: 1px solid #dedfe0; 21 | } 22 | & + .item:after { 23 | border-top: 0 none; 24 | } 25 | .item-inner { 26 | position: relative; 27 | 28 | display: -webkit-box; 29 | padding: 14px 23px 10px 14px; 30 | box-sizing: border-box; 31 | 32 | line-height: 33px; 33 | 34 | cursor: pointer; 35 | 36 | background-color: #fff; 37 | } 38 | 39 | .info-wrap { 40 | display: -webkit-box; 41 | padding-left: 11px; 42 | 43 | -webkit-box-flex: 1; 44 | } 45 | 46 | .info-name-text, 47 | .info-wrap p { 48 | overflow: hidden; 49 | 50 | white-space: nowrap; 51 | text-overflow: ellipsis; 52 | word-wrap: normal; 53 | } 54 | 55 | .info-name { 56 | position: relative; 57 | 58 | display: inline-block; 59 | max-width: 100%; 60 | margin-bottom: 2px; 61 | box-sizing: border-box; 62 | 63 | font-size: 16px; 64 | 65 | vertical-align: middle; 66 | } 67 | 68 | .info-name-text { 69 | line-height: 16px; 70 | } 71 | 72 | .info-wrap p { 73 | line-height: 1.2; 74 | } 75 | .info-content { 76 | margin-bottom: 3px; 77 | 78 | font-size: 13px; 79 | 80 | color: #808080; 81 | } 82 | .icon { 83 | width: 75px; 84 | height: 60px; 85 | border-radius: 3px; 86 | background-size: 100%; 87 | } 88 | .list--ellipsis .item, 89 | .item--ellipsis { 90 | overflow: hidden; 91 | 92 | white-space: nowrap; 93 | text-overflow: ellipsis; 94 | } 95 | 96 | .info-left { 97 | padding-right: 5px; 98 | margin-top: -8px; 99 | 100 | -webkit-box-flex: 1; 101 | } 102 | } 103 | 104 | /** 编辑、删除按钮 */ 105 | .dislike-btn, 106 | .like-btn { 107 | position: absolute; 108 | top: 0; 109 | right: 0; 110 | bottom: 0; 111 | z-index: 10; 112 | 113 | width: 66px; 114 | 115 | font-size: 18px; 116 | line-height: 84px; 117 | 118 | -webkit-transform: translate3d(200%, 0, 0); 119 | transform: translate3d(200%, 0, 0); 120 | text-align: center; 121 | 122 | color: #fff; 123 | background-color: #fe3c2e; 124 | } 125 | 126 | .dislike-btn { 127 | -webkit-transform: translate3d(100%, 0, 0); 128 | transform: translate3d(100%, 0, 0); 129 | } 130 | 131 | .like-btn { 132 | -webkit-transform: translate3d(100%, 0, 0); 133 | transform: translate3d(100%, 0, 0); 134 | 135 | background-color: #00a5e0; 136 | } 137 | 138 | .item { 139 | -webkit-transition: -webkit-transform .2s ease-out; 140 | transition: transform .2s ease-out; 141 | 142 | &.active-like { 143 | -webkit-transform: translate3d(-66px, 0, 0); 144 | transform: translate3d(-66px, 0, 0); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/page/index/components/loading/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import pureRender from 'pure-render-decorator'; 3 | 4 | require('./index.scss'); 5 | 6 | @pureRender 7 | export default class List extends Component { 8 | 9 | constructor(props, context) { 10 | super(props, context); 11 | this.state = { 12 | 13 | }; 14 | } 15 | 16 | componentWillMount() { 17 | 18 | } 19 | 20 | componentDidMount() { 21 | 22 | } 23 | 24 | render() { 25 | 26 | console.dev('render Loading'); 27 | 28 | var isShow = this.props.isShow || false; 29 | var loadingStyle = { 30 | display: (isShow) ? 'block' : 'none' 31 | }; 32 | 33 | var isEnd = this.props.isEnd || false; 34 | var loadingText = (isEnd) ? '已加载全部' : '正在加载中…'; 35 | 36 | return ( 37 |
    38 |

    {loadingText}

    39 |
    40 | ) 41 | } 42 | } -------------------------------------------------------------------------------- /src/page/index/components/loading/index.scss: -------------------------------------------------------------------------------- 1 | .loading { 2 | line-height: 44px; 3 | text-align: center; 4 | font-size: 12px; 5 | color: #808080; 6 | } -------------------------------------------------------------------------------- /src/page/index/components/scroll/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import pureRender from 'pure-render-decorator'; 3 | 4 | @pureRender 5 | export default class Scroll extends Component { 6 | 7 | constructor(props, context) { 8 | super(props, context); 9 | this.state = { 10 | 11 | }; 12 | this.isLoading = false; 13 | this.prvScrollTop = 0; 14 | this.scrollTopCache = {}; 15 | this.prvScrollTopCache = {}; 16 | this.refreshScroll = this.refreshScroll.bind(this); 17 | } 18 | 19 | componentWillMount() { 20 | 21 | } 22 | 23 | componentDidMount() { 24 | this.bindScrollEvt(); 25 | } 26 | 27 | componentDidUpdate(prevProps, prevState) { 28 | console.log("==================componentDidUpdate=============="); 29 | } 30 | 31 | refreshScroll() { 32 | this.prvScrollTop = this.prvScrollTopCache[this.props.tabs.active] = 0; 33 | } 34 | 35 | 36 | bindScrollEvt() { 37 | var _this = this; 38 | var timer = null; 39 | 40 | window.addEventListener('scroll', function(e) { 41 | // 延迟计算 42 | timer && clearTimeout(timer); 43 | timer = setTimeout(function() { 44 | 45 | var doc = window.document; 46 | var scrollTop = doc.body.scrollTop; 47 | var isEnd = _this.props.isEnd; 48 | // console.log(listType, isEnd); 49 | 50 | // 防止向上滚动也拉数据 51 | if (_this.prvScrollTop > scrollTop) { 52 | return; 53 | } 54 | _this.prvScrollTop = scrollTop; 55 | 56 | var winHeight = window.document.documentElement.clientHeight; 57 | var clientHeight = window.document.body.clientHeight; 58 | 59 | // 条件一: 滚动到最底部才拉数据 60 | // if (scrollTop + winHeight >= clientHeight) { 61 | // 条件二: 滚动到中间拉数据 62 | if (scrollTop >= (clientHeight - winHeight) / 2 && !isEnd) { 63 | _this.props.loadNewsList(null, false); 64 | } 65 | 66 | }, 50); 67 | }); 68 | 69 | } 70 | 71 | render() { 72 | 73 | console.dev('render Scroll!'); 74 | 75 | return ( 76 |
    77 | {this.props.children} 78 |
    79 | ) 80 | } 81 | } -------------------------------------------------------------------------------- /src/page/index/components/tab/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import pureRender from 'pure-render-decorator'; 3 | import { LATEST_NEWS, LIKE_NEWS } from '../../constants/constants'; 4 | 5 | import Touch from 'touch'; 6 | import classNames from 'classnames'; 7 | require('./index.scss'); 8 | 9 | 10 | function TabItem(item, key) { 11 | return ( 12 |
  • 15 | {item.text} 16 |
  • 17 | ) 18 | } 19 | 20 | function TabHighlight(props) { 21 | 22 | var isActive = (props.active === LIKE_NEWS); 23 | return ( 24 | 25 | ) 26 | } 27 | 28 | @pureRender 29 | export default class Tab extends Component { 30 | 31 | constructor(props, context) { 32 | super(props, context); 33 | this.state = { 34 | 35 | }; 36 | this.tabs = [ 37 | { 38 | label: LATEST_NEWS, 39 | text: '最新新闻' 40 | }, 41 | { 42 | label: LIKE_NEWS, 43 | text: '我的收藏' 44 | } 45 | ]; 46 | this.switchTab = this.switchTab.bind(this); 47 | } 48 | 49 | componentWillMount() { 50 | 51 | } 52 | 53 | componentDidMount() { 54 | 55 | } 56 | 57 | switchTab(e) { 58 | let tab = parseInt(e.target.dataset.tab); 59 | this.props.updateActiveTab(tab); 60 | 61 | } 62 | 63 | render() { 64 | console.dev('render Tab'); 65 | 66 | return ( 67 |
    68 |
    69 | 75 |
    76 |
    77 | ) 78 | } 79 | } -------------------------------------------------------------------------------- /src/page/index/components/tab/index.scss: -------------------------------------------------------------------------------- 1 | .cm-tabs { 2 | position: fixed; 3 | top: 0; 4 | z-index: 99; 5 | 6 | width: 100%; 7 | } 8 | .nav { 9 | position: relative; 10 | line-height: 45px; 11 | background-color: #fff; 12 | 13 | &:after { 14 | border-bottom: 1px solid #dbdbdb; 15 | } 16 | 17 | .title-list { 18 | display: table; 19 | width: 100%; 20 | 21 | table-layout: fixed; 22 | } 23 | 24 | .title-list li { 25 | display: table-cell; 26 | box-sizing: border-box; 27 | font-size: 16px; 28 | text-align: center; 29 | color: #777; 30 | } 31 | 32 | .title-list li.active { 33 | color: #00a5e0; 34 | } 35 | 36 | .icon-active { 37 | position: absolute; 38 | bottom: 0; 39 | left: 0; 40 | 41 | width: 50%; 42 | height: 4px; 43 | 44 | transform: left .3s ease-in-out; 45 | 46 | background-color: #00a5e0; 47 | } 48 | 49 | .icon-active.pull-right { 50 | right: 0; 51 | left: initial; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/page/index/connect/connect.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { request } from '../../common/actions/actions'; 3 | import { getArgs, updateActiveTab, toggleContent, 4 | toggleListLoading, toggleSpinLoading, toggleDialog, likeNews, dislikeNews } from '../actions/actions'; 5 | 6 | // Map Redux state to component props 7 | // ownProps stores react-router-redux props 8 | function mapStateToProps(state, ownProps) { 9 | return { 10 | args: state.args, 11 | tabs: state.tabs, 12 | news: state.news, 13 | spinLoading: state.spinLoading, 14 | listLoading: state.listLoading, 15 | }; 16 | } 17 | 18 | // Map Redux actions to component props 19 | function mapDispatchToProps(dispatch) { 20 | return { 21 | request: (cgiName, params, opts) => dispatch(request(cgiName, params, opts)), 22 | getArgs: (value) => dispatch(getArgs(value)), 23 | toggleListLoading: (value) => dispatch(toggleListLoading(value)), 24 | toggleSpinLoading: (value) => dispatch(toggleSpinLoading(value)), 25 | updateActiveTab: (value) => dispatch(updateActiveTab(value)), 26 | likeNews: (value) => dispatch(likeNews(value)), 27 | dislikeNews: (value) => dispatch(dislikeNews(value)), 28 | }; 29 | } 30 | 31 | export default connect( 32 | mapStateToProps, 33 | mapDispatchToProps 34 | ); -------------------------------------------------------------------------------- /src/page/index/constants/constants.js: -------------------------------------------------------------------------------- 1 | export const LATEST_NEWS = 10; 2 | export const LIKE_NEWS = 11; 3 | 4 | export const DEBUG = true; -------------------------------------------------------------------------------- /src/page/index/container/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import merge from 'lodash.merge'; 3 | import { render } from 'react-dom'; 4 | import Connect from '../connect/connect'; 5 | import { GET_NEWS_LIST, GET_TOP_NEWS } from '../../common/constants/constants'; 6 | import { LATEST_NEWS, LIKE_NEWS } from '../constants/constants'; 7 | import { platform } from 'utils'; 8 | 9 | require('./index.scss'); 10 | import Scroll from 'scroll'; 11 | import Spinner from 'spinner'; 12 | import List from '../components/list/index'; 13 | import Tab from '../components/tab/index'; 14 | import Loading from '../components/loading/index'; 15 | 16 | 17 | if (platform().ios) { 18 | document.body.className = "ios"; 19 | } 20 | 21 | class Wrapper extends Component { 22 | 23 | constructor(props, context) { 24 | super(props, context); 25 | this.state = { 26 | 27 | }; 28 | this.firstGetAllData = false; 29 | this.loadTopNews = this.loadTopNews.bind(this); 30 | this.loadNewsList = this.loadNewsList.bind(this); 31 | this.loadData = this.loadData.bind(this); 32 | this.loadDataForScroll = this.loadDataForScroll.bind(this); 33 | } 34 | 35 | componentDidMount() { 36 | 37 | } 38 | 39 | componentWillMount() { 40 | this.loadTopNews(); 41 | } 42 | 43 | componentWillReceiveProps(nextProps) { 44 | this.props.toggleSpinLoading(false); 45 | 46 | return true; 47 | } 48 | 49 | loadDataForScroll() { 50 | this.loadNewsList(null); 51 | } 52 | 53 | loadTopNews() { 54 | var url = GET_TOP_NEWS, 55 | opts = {}; 56 | 57 | var pa = merge({}, { 58 | chlid: 'news_news_top', 59 | refer: 'mobilewwwqqcom', 60 | otype: 'jsonp', 61 | callback: 'getNewsIndexOutput', 62 | t: (new Date()).getTime() 63 | }, pa); 64 | 65 | var param = { 66 | param: pa, 67 | ajaxType: 'JSONP', 68 | onSuccess: function(res) { 69 | // console.log(res); 70 | }, 71 | onError: function(res) { 72 | // console.log(res); 73 | // alert(res.errMsg || '加载新闻列表失败,请稍后重试'); 74 | } 75 | }; 76 | 77 | this.props.request(url, param, opts); 78 | } 79 | 80 | loadNewsList(props) { 81 | var props = props || this.props; 82 | 83 | this.loadData(LATEST_NEWS, {}); 84 | } 85 | 86 | //http://mat1.gtimg.com/www/mobi/image/loadimg.png 87 | 88 | loadData(listType, pa = {}, opts = {}) { 89 | var _this = this; 90 | var url = GET_NEWS_LIST; 91 | 92 | var listInfoParam = this.props.news.listInfo['listLatest'], 93 | ids = this.props.news.ids, 94 | args = this.props.args; 95 | 96 | // 防止重复拉取 97 | if (listInfoParam.isLoading) { 98 | return; 99 | } 100 | 101 | var curPage = listInfoParam.curPage, 102 | page_size = listInfoParam.pageSize, 103 | startIndex = 0 + (curPage - 1) * page_size, 104 | endIndex = startIndex + page_size; 105 | 106 | var newIds = ids.slice(startIndex, endIndex), 107 | newIdArray = []; 108 | 109 | newIds.forEach((item, index) => { 110 | newIdArray.push(item.id); 111 | }); 112 | 113 | var pa = merge({}, { 114 | cmd: GET_NEWS_LIST, 115 | ids: newIdArray.join(','), 116 | refer: "mobilewwwqqcom", 117 | otype: "jsonp", 118 | callback: "getNewsContentOnlyOutput", 119 | t: (new Date()).getTime(), 120 | }, pa); 121 | 122 | var param = { 123 | param: pa, 124 | ajaxType: 'JSONP', 125 | onSuccess: function(data) { 126 | // console.log(data); 127 | }, 128 | onError: function(res) { 129 | console.log("err"); 130 | // console.log(res); 131 | // alert(res.errMsg || '加载新闻列表失败,请稍后重试'); 132 | } 133 | }; 134 | 135 | this.props.request(url, param, opts); 136 | } 137 | 138 | render() { 139 | 140 | console.dev('render container!!!'); 141 | let tabStyle = this.props.tabs, 142 | isEnd = this.props.news.listInfo['listLatest']['isEnd'], 143 | isLoadingShow = tabStyle === LATEST_NEWS; 144 | 145 | return ( 146 |
    147 | 151 |
    152 | 157 | 166 | 175 | 176 | 177 |
    178 | 179 |
    180 | ) 181 | } 182 | } 183 | 184 | export default Connect(Wrapper); -------------------------------------------------------------------------------- /src/page/index/container/index.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | @import"../../../css/common/common"; 3 | @import"../../../css/common/reset"; 4 | @import"../../../css/sprites/list_s"; 5 | 6 | html, body { 7 | height: 100%; 8 | width: 100%; 9 | } 10 | 11 | body { 12 | background-color: #f8f9fb; 13 | } 14 | 15 | .ios .cm-page-wrap { 16 | height: 100%; 17 | 18 | > div { 19 | height: 100%; 20 | 21 | > div { 22 | height: 100%; 23 | } 24 | } 25 | 26 | .cm-page { 27 | height: 100%; 28 | } 29 | 30 | .cm-content { 31 | position: relative; 32 | height: 100%; 33 | } 34 | 35 | .content-wrap { 36 | position: absolute; 37 | height: 100%; 38 | width: 100%; 39 | overflow: auto; 40 | } 41 | } 42 | 43 | .logo_news { 44 | width: 119px; 45 | height: 15px; 46 | @include sprite($logo_news); 47 | } -------------------------------------------------------------------------------- /src/page/index/main.js: -------------------------------------------------------------------------------- 1 | import Root from './root/Root'; -------------------------------------------------------------------------------- /src/page/index/reducers/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import merge from 'lodash.merge'; 3 | import { setItem } from 'utils'; 4 | import initialState from '../stores/stores'; 5 | import { GET_NEWS_LIST, GET_TOP_NEWS } from '../../common/constants/constants'; 6 | import { GET_ARGS, TABS_UPDATE, TOGGLE_CONTENT, 7 | TOGGLE_LIST_LOADING, TOGGLE_SPIN_LOADING, LIKE_NEWS, DISLIKE_NEWS } from '../actions/actions'; 8 | 9 | 10 | var news = function(state = initialState.news, action) { 11 | let listInfoMap = { 12 | 10: 'listLatest', // 最新新闻 13 | 11: 'listLike', // 收藏新闻 14 | }; 15 | 16 | switch(action.type) { 17 | 18 | case GET_TOP_NEWS + '_SUCCESS': 19 | 20 | if (!action.data || !action.data.idlist || action.data.idlist.length === 0) { 21 | return state; 22 | } 23 | 24 | var idlist = action.data.idlist, 25 | newState = merge({}, state); 26 | 27 | newState.ids = merge([], idlist[0].ids); 28 | newState.listLatest = merge([], newState.listLatest.concat(idlist[0].newslist)); 29 | 30 | return newState; 31 | 32 | 33 | case GET_NEWS_LIST + '_ON': 34 | var newState = merge({}, state); 35 | newState.listInfo['listLatest'].isLoading = true; 36 | 37 | return newState; 38 | 39 | case GET_NEWS_LIST + '_SUCCESS': 40 | 41 | if (!action.data || !action.data.newslist) { 42 | return state; 43 | } 44 | 45 | var newState = merge({}, state), 46 | listInfo = { 47 | curPage: (++newState.listInfo['listLatest'].curPage), 48 | isLoading: false, 49 | }; 50 | 51 | newState.listInfo['listLatest'] = merge({}, newState.listInfo['listLatest'], listInfo); 52 | newState['listLatest'] = newState['listLatest'].concat(action.data.newslist); 53 | 54 | return newState; 55 | 56 | case GET_NEWS_LIST + '_ERROR': 57 | var newState = merge({}, state); 58 | newState.listInfo['listLatest'].isLoading = false; 59 | 60 | return newState; 61 | 62 | case LIKE_NEWS: 63 | if (!action.value) { 64 | return state; 65 | } 66 | 67 | var newState = merge({}, state), 68 | isDuplicate = false; 69 | 70 | newState['listLike'].map((item, index) => { 71 | if (item.id === action.value.id) { 72 | isDuplicate = true; 73 | } 74 | }); 75 | 76 | if (isDuplicate) { 77 | return newState; 78 | } 79 | 80 | newState['listLike'] = newState['listLike'].concat(action.value); 81 | setItem('like-list', JSON.stringify(newState['listLike'])); 82 | 83 | return newState; 84 | 85 | case DISLIKE_NEWS: 86 | if (!action.value) { 87 | return state; 88 | } 89 | 90 | var newState = merge({}, state); 91 | newState['listLike'] = newState['listLike'].filter((item, index) => { 92 | return (item.id !== action.value.id); 93 | }); 94 | setItem('like-list', JSON.stringify(newState['listLike'])); 95 | 96 | return newState; 97 | 98 | default: 99 | return state; 100 | } 101 | }; 102 | 103 | var args = function(state = initialState.args, action) { 104 | switch(action.type) { 105 | case GET_ARGS: 106 | return merge({}, state, action.value); 107 | default: 108 | return state; 109 | } 110 | }; 111 | 112 | var tabs = function(state = initialState.tabs, action) { 113 | switch(action.type) { 114 | case TABS_UPDATE: 115 | return action.value; 116 | default: 117 | return state; 118 | } 119 | }; 120 | 121 | var listLoading = function(state = initialState.listLoading, action) { 122 | switch(action.type) { 123 | case TOGGLE_LIST_LOADING: 124 | return action.value; 125 | break; 126 | default: 127 | return state; 128 | } 129 | }; 130 | 131 | var spinLoading = function(state = initialState.spinLoading, action) { 132 | switch(action.type) { 133 | case TOGGLE_SPIN_LOADING: 134 | return action.value; 135 | break; 136 | default: 137 | return state; 138 | } 139 | }; 140 | 141 | 142 | const rootReducer = combineReducers({ 143 | args, 144 | tabs, 145 | news, 146 | listLoading, 147 | spinLoading, 148 | }); 149 | 150 | export default rootReducer; -------------------------------------------------------------------------------- /src/page/index/root/Root.dev.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { createStore } from 'redux'; 4 | import { Provider } from 'react-redux'; 5 | import configureStore from '../stores/configureStore'; 6 | import { initialStore } from '../stores/stores'; 7 | 8 | import IndexWrapper from '../container/index'; 9 | import DevTools from '../../common/devtools/DevTools'; 10 | import { DEBUG } from '../constants/constants'; 11 | 12 | let store = configureStore(); 13 | 14 | 15 | var DevToolsWrapper = (DEBUG) ? : null; 16 | 17 | export default class Root extends Component { 18 | 19 | constructor(props, context) { 20 | super(props, context); 21 | } 22 | 23 | render() { 24 | return ( 25 | 26 |
    27 | 28 | { DevToolsWrapper } 29 |
    30 |
    31 | ); 32 | } 33 | } 34 | 35 | render( 36 | , 37 | document.getElementById('pages') 38 | ); 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/page/index/root/Root.js: -------------------------------------------------------------------------------- 1 | if ("__PROD__" !== process.env.NODE_ENV) { 2 | window.console.dev = function(msg) { 3 | console.log(msg); 4 | }; 5 | module.exports = require('./Root.dev'); 6 | } 7 | else { 8 | window.console.dev = function(msg) {}; 9 | module.exports = require('./Root.prod'); 10 | } 11 | -------------------------------------------------------------------------------- /src/page/index/root/Root.prod.js: -------------------------------------------------------------------------------- 1 | import { Component, PropTypes } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { createStore } from 'redux'; 4 | import { Provider } from 'react-redux'; 5 | import { configureStore } from '../stores/configureStore'; 6 | import initialStore from '../stores/stores'; 7 | 8 | import IndexWrapper from '../container/index'; 9 | 10 | let store = configureStore(); 11 | 12 | export default class Root extends Component { 13 | 14 | constructor(props, context) { 15 | super(props, context); 16 | } 17 | 18 | render() { 19 | return ( 20 | 21 |
    22 | 23 |
    24 |
    25 | ); 26 | } 27 | } 28 | 29 | render( 30 | , 31 | document.getElementById('pages') 32 | ); 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/page/index/stores/configureStore.dev.js: -------------------------------------------------------------------------------- 1 | import { createStore, compose, applyMiddleware } from 'redux'; 2 | import { Router, Route, browserHistory } from 'react-router'; 3 | import { syncHistory } from 'react-router-redux'; 4 | import rootReducer from '../reducers/reducers'; 5 | import thunk from 'redux-thunk'; 6 | import { persistState } from 'redux-devtools'; 7 | import DevTools from '../../common/devtools/DevTools'; 8 | // import logger from '../../common/middleware/logger'; 9 | import api from '../../common/middleware/api'; 10 | import { DEBUG } from '../constants/constants'; 11 | 12 | function getDebugSessionKey() { 13 | // You can write custom logic here! 14 | // By default we try to read the key from ?debug_session= in the address bar 15 | const matches = window.location.href.match(/[?&]debug_session=([^&]+)\b/); 16 | return (matches && matches.length > 0) ? matches[1] : null; 17 | } 18 | 19 | var finalCreateStore = null; 20 | if (DEBUG) { 21 | finalCreateStore = compose( 22 | applyMiddleware(thunk, api), 23 | DevTools.instrument(), 24 | persistState(getDebugSessionKey()) 25 | )(createStore); 26 | } 27 | else { 28 | finalCreateStore = compose( 29 | applyMiddleware(thunk, api) 30 | )(createStore); 31 | } 32 | 33 | export default function configureStore(initialState) { 34 | 35 | const store = finalCreateStore(rootReducer, initialState); 36 | 37 | // Required for replaying actions from devtools to work 38 | // reduxRouterMiddleware.listenForReplays(store); 39 | 40 | if (module.hot) { 41 | // Enable Webpack hot module replacement for reducers 42 | module.hot.accept('../reducers/reducers', () => { 43 | const nextRootReducer = require('../reducers/reducers').default; 44 | store.replaceReducer(nextRootReducer); 45 | }); 46 | } 47 | 48 | return store; 49 | } -------------------------------------------------------------------------------- /src/page/index/stores/configureStore.js: -------------------------------------------------------------------------------- 1 | if ("__PROD__" !== process.env.NODE_ENV) { 2 | module.exports = require('./configureStore.dev'); 3 | } 4 | else { 5 | module.exports = require('./configureStore.prod'); 6 | } 7 | -------------------------------------------------------------------------------- /src/page/index/stores/configureStore.prod.js: -------------------------------------------------------------------------------- 1 | import { createStore, compose, applyMiddleware } from 'redux'; 2 | import rootReducer from '../reducers/reducers'; 3 | import thunk from 'redux-thunk'; 4 | import logger from '../../common/middleware/logger'; 5 | import api from '../../common/middleware/api'; 6 | 7 | const finalCreateStore = compose( 8 | applyMiddleware(thunk, api, logger) 9 | )(createStore); 10 | 11 | export function configureStore(initialState) { 12 | 13 | const store = finalCreateStore(rootReducer, initialState); 14 | 15 | return store; 16 | } -------------------------------------------------------------------------------- /src/page/index/stores/stores.js: -------------------------------------------------------------------------------- 1 | import { getItem, getHash } from 'utils'; 2 | import { LATEST_NEWS, LIKE_NEWS } from '../constants/constants'; 3 | 4 | /** other const **/ 5 | const initialState = { 6 | args: { 7 | src: getHash('src'), 8 | }, 9 | tabs: LATEST_NEWS, 10 | news: { 11 | ids: [], // 新闻id 12 | listLatest: [], // 最新新闻 13 | listLike: JSON.parse(getItem('like-list')) || [], // 收藏新闻 14 | listInfo: { 15 | listLatest:{ 16 | isEnd: false, 17 | pageSize: 20, 18 | curPage: 1, 19 | isLoading: false, 20 | }, 21 | listLike: { 22 | isEnd: false, 23 | pageSize: 20, 24 | curPage: 1, 25 | isLoading: false, 26 | } 27 | }, 28 | }, 29 | listLoading: false, 30 | spinLoading: true 31 | }; 32 | 33 | 34 | export default initialState; -------------------------------------------------------------------------------- /src/page/spa/actions/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * action types 3 | */ 4 | 5 | // OTHERS 6 | export const GET_ARGS = 'GET_ARGS'; 7 | 8 | 9 | export const TOGGLE_SPIN_LOADING = 'TOGGLE_SPIN_LOADING'; 10 | export const TOGGLE_LIST_LOADING = 'TOGGLE_LIST_LOADING'; 11 | 12 | export const TABS_UPDATE = 'TABS_UPDATE'; 13 | 14 | export const LIKE_NEWS = 'LIKE_NEWS'; 15 | export const DISLIKE_NEWS = 'DISLIKE_NEWS'; 16 | 17 | /* 18 | * other constants 19 | */ 20 | 21 | 22 | /* 23 | * action creators 24 | */ 25 | 26 | export function getArgs(value) { 27 | return { type: GET_ARGS, value }; 28 | } 29 | 30 | export function toggleListLoading(value) { 31 | return { type: TOGGLE_LIST_LOADING, value }; 32 | } 33 | 34 | export function toggleSpinLoading(value) { 35 | return { type: TOGGLE_SPIN_LOADING, value }; 36 | } 37 | 38 | export function updateActiveTab(value) { 39 | return { type: TABS_UPDATE, value}; 40 | } 41 | 42 | export function likeNews(value) { 43 | return { type: LIKE_NEWS, value }; 44 | } 45 | 46 | export function dislikeNews(value) { 47 | return { type: DISLIKE_NEWS, value }; 48 | } -------------------------------------------------------------------------------- /src/page/spa/components/list/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import pureRender from 'pure-render-decorator'; 3 | import classnames from 'classnames'; 4 | import { formatDate } from 'utils'; 5 | import { LATEST_NEWS, LIKE_NEWS} from '../../constants/constants'; 6 | 7 | import Touch from 'touch'; 8 | 9 | require('./index.scss'); 10 | 11 | let spaPath = ""; 12 | if ("__DEV__" === process.env.NODE_ENV || "__PROD__" === process.env.NODE_ENV) { 13 | spaPath = "spa.html"; 14 | } 15 | else { 16 | spaPath = "spa"; 17 | } 18 | 19 | @pureRender 20 | export default class List extends Component { 21 | 22 | constructor(props, context) { 23 | super(props, context); 24 | this.state = { 25 | activeDelHwId: null, 26 | activeDelBubbleHwId: null 27 | }; 28 | this.jumpToDetail = this.jumpToDetail.bind(this); 29 | this.showLikeBtn = this.showLikeBtn.bind(this); 30 | this.hideLikeBtn = this.hideLikeBtn.bind(this); 31 | this.isClickOnBtn = false; // 是否点击在修改、删除按钮上 32 | this.like = this.like.bind(this); 33 | this.dislike = this.dislike.bind(this); 34 | } 35 | 36 | componentWillMount() { 37 | 38 | } 39 | 40 | componentDidMount() { 41 | window.addEventListener('touchstart', this.hideLikeBtn(), false); 42 | } 43 | 44 | componentWillUnmount() { 45 | window.removeEventListener('touchstart', this.hideLikeBtn(), false); 46 | } 47 | 48 | jumpToDetail(item) { 49 | return (e) => { 50 | if (!this.isClickOnBtn) { 51 | // console.log(item.articletype); 52 | if (item.articletype === '100') { 53 | var win = window.open(item.url, '_blank'); 54 | win.focus(); 55 | } 56 | else { 57 | if (!this.props.details.hasOwnProperty(item.id)) { 58 | this.props.getNewsDetail(item.id); 59 | } 60 | this.context.router.push('/' + spaPath + '/detail/' + item.id + '/' + item.commentid); 61 | } 62 | 63 | } 64 | } 65 | } 66 | 67 | renderNewsIcon(pic) { 68 | return { 69 | "backgroundImage": "url(" + pic + ")", 70 | "backgroundSize": "100%" 71 | } 72 | } 73 | 74 | showLikeBtn(item, e) { 75 | return (e) => { 76 | e.preventDefault(); 77 | 78 | this.setState({ 79 | activeNewsId: item.id 80 | }); 81 | } 82 | } 83 | 84 | hideLikeBtn(e) { 85 | return (e) => { 86 | let target = e.target, 87 | classname = target.className; 88 | 89 | if (this.state.activeNewsId === null) { 90 | return; 91 | } 92 | 93 | this.setState({ 94 | activeNewsId: null, 95 | }); 96 | } 97 | } 98 | 99 | like(item) { 100 | return (e) => { 101 | this.isClickOnBtn = true; 102 | this.props.likeNews(item); 103 | setTimeout(() => { 104 | this.hideLikeBtn(e); 105 | this.isClickOnBtn = false; 106 | }, 20); 107 | } 108 | } 109 | 110 | dislike(item) { 111 | return (e) => { 112 | this.isClickOnBtn = true; 113 | this.props.dislikeNews(item); 114 | setTimeout(() => { 115 | this.hideLikeBtn(e); 116 | this.isClickOnBtn = false; 117 | }, 20); 118 | } 119 | } 120 | 121 | render() { 122 | 123 | console.dev('render List!!'); 124 | 125 | let _this = this; 126 | let prevCreateTime = null; 127 | 128 | let news = this.props.news; 129 | let tabsType = this.props.tabsType; 130 | 131 | let listDataMap = { 132 | [LATEST_NEWS]: 'listLatest', 133 | [LIKE_NEWS] : 'listLike' 134 | }; 135 | 136 | this.listData = news; 137 | 138 | let list = news.map((item, index) => { 139 | return ( 140 |
  • 141 | 142 |
    143 |
    144 |
    145 |
    146 |
    {item.title}
    147 |
    148 |

    {item.des}

    149 |
    150 |
    151 | 153 | {(tabsType === LATEST_NEWS) ? "收藏" : "取消"} 154 | 155 |
    156 |
  • 157 | ) 158 | }); 159 | 160 | let wrapperStyle = { 161 | display: (this.props.tabs === tabsType) ? "block" : "none", 162 | paddingTop: 46, 163 | }; 164 | 165 | return ( 166 |
    167 |
    168 |
      169 | {list} 170 |
    171 |
    172 |
    173 | ) 174 | } 175 | } 176 | 177 | List.contextTypes = { 178 | router: React.PropTypes.object.isRequired 179 | }; -------------------------------------------------------------------------------- /src/page/spa/components/list/index.scss: -------------------------------------------------------------------------------- 1 | .news-list { 2 | overflow-x: hidden; 3 | width: 100%; 4 | } 5 | 6 | /** 修改1px边框颜色 */ 7 | 8 | .title { 9 | margin: 8px 0 8px 14px; 10 | 11 | font-size: 15px; 12 | 13 | color: #777; 14 | } 15 | 16 | .item { 17 | -webkit-tap-highlight-color: transparent; 18 | &:after { 19 | border-top: 1px solid #dedfe0; 20 | border-bottom: 1px solid #dedfe0; 21 | } 22 | & + .item:after { 23 | border-top: 0 none; 24 | } 25 | .item-inner { 26 | position: relative; 27 | 28 | display: -webkit-box; 29 | padding: 14px 23px 10px 14px; 30 | box-sizing: border-box; 31 | 32 | line-height: 33px; 33 | 34 | cursor: pointer; 35 | 36 | background-color: #fff; 37 | } 38 | 39 | .info-wrap { 40 | display: -webkit-box; 41 | padding-left: 11px; 42 | 43 | -webkit-box-flex: 1; 44 | } 45 | 46 | .info-name-text, 47 | .info-wrap p { 48 | overflow: hidden; 49 | 50 | white-space: nowrap; 51 | text-overflow: ellipsis; 52 | word-wrap: normal; 53 | } 54 | 55 | .info-name { 56 | position: relative; 57 | 58 | display: inline-block; 59 | max-width: 100%; 60 | margin-bottom: 2px; 61 | box-sizing: border-box; 62 | 63 | font-size: 16px; 64 | 65 | vertical-align: middle; 66 | } 67 | 68 | .info-name-text { 69 | line-height: 16px; 70 | } 71 | 72 | .info-wrap p { 73 | line-height: 1.2; 74 | } 75 | .info-content { 76 | margin-bottom: 3px; 77 | 78 | font-size: 13px; 79 | 80 | color: #808080; 81 | } 82 | .icon { 83 | width: 75px; 84 | height: 60px; 85 | border-radius: 3px; 86 | background-size: 100%; 87 | } 88 | .list--ellipsis .item, 89 | .item--ellipsis { 90 | overflow: hidden; 91 | 92 | white-space: nowrap; 93 | text-overflow: ellipsis; 94 | } 95 | 96 | .info-left { 97 | padding-right: 5px; 98 | margin-top: -8px; 99 | 100 | -webkit-box-flex: 1; 101 | } 102 | } 103 | 104 | /** 编辑、删除按钮 */ 105 | .dislike-btn, 106 | .like-btn { 107 | position: absolute; 108 | top: 0; 109 | right: 0; 110 | bottom: 0; 111 | z-index: 10; 112 | 113 | width: 66px; 114 | 115 | font-size: 18px; 116 | line-height: 84px; 117 | 118 | -webkit-transform: translate3d(200%, 0, 0); 119 | transform: translate3d(200%, 0, 0); 120 | text-align: center; 121 | 122 | color: #fff; 123 | background-color: #fe3c2e; 124 | } 125 | 126 | .dislike-btn { 127 | -webkit-transform: translate3d(100%, 0, 0); 128 | transform: translate3d(100%, 0, 0); 129 | } 130 | 131 | .like-btn { 132 | -webkit-transform: translate3d(100%, 0, 0); 133 | transform: translate3d(100%, 0, 0); 134 | 135 | background-color: #00a5e0; 136 | } 137 | 138 | .item { 139 | -webkit-transition: -webkit-transform .2s ease-out; 140 | transition: transform .2s ease-out; 141 | 142 | &.active-like { 143 | -webkit-transform: translate3d(-66px, 0, 0); 144 | transform: translate3d(-66px, 0, 0); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/page/spa/components/loading/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import pureRender from 'pure-render-decorator'; 3 | 4 | require('./index.scss'); 5 | 6 | @pureRender 7 | export default class List extends Component { 8 | 9 | constructor(props, context) { 10 | super(props, context); 11 | this.state = { 12 | 13 | }; 14 | } 15 | 16 | componentWillMount() { 17 | 18 | } 19 | 20 | componentDidMount() { 21 | 22 | } 23 | 24 | render() { 25 | 26 | console.dev('render Loading'); 27 | 28 | var isShow = this.props.isShow || false; 29 | var loadingStyle = { 30 | display: (isShow) ? 'block' : 'none' 31 | }; 32 | 33 | var isEnd = this.props.isEnd || false; 34 | var loadingText = (isEnd) ? '已加载全部' : '正在加载中…'; 35 | 36 | return ( 37 |
    38 |

    {loadingText}

    39 |
    40 | ) 41 | } 42 | } -------------------------------------------------------------------------------- /src/page/spa/components/loading/index.scss: -------------------------------------------------------------------------------- 1 | .loading { 2 | line-height: 44px; 3 | text-align: center; 4 | font-size: 12px; 5 | color: #808080; 6 | } -------------------------------------------------------------------------------- /src/page/spa/components/tab/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import pureRender from 'pure-render-decorator'; 3 | import { LATEST_NEWS, LIKE_NEWS } from '../../constants/constants'; 4 | 5 | import Touch from 'touch'; 6 | import classNames from 'classnames'; 7 | require('./index.scss'); 8 | 9 | 10 | function TabItem(item, key) { 11 | return ( 12 |
  • 15 | {item.text} 16 |
  • 17 | ) 18 | } 19 | 20 | function TabHighlight(props) { 21 | 22 | var isActive = (props.active === LIKE_NEWS); 23 | return ( 24 | 25 | ) 26 | } 27 | 28 | @pureRender 29 | export default class Tab extends Component { 30 | 31 | constructor(props, context) { 32 | super(props, context); 33 | this.state = { 34 | 35 | }; 36 | this.tabs = [ 37 | { 38 | label: LATEST_NEWS, 39 | text: '最新新闻' 40 | }, 41 | { 42 | label: LIKE_NEWS, 43 | text: '我的收藏' 44 | } 45 | ]; 46 | this.switchTab = this.switchTab.bind(this); 47 | } 48 | 49 | componentWillMount() { 50 | 51 | } 52 | 53 | componentDidMount() { 54 | 55 | } 56 | 57 | switchTab(e) { 58 | let tab = parseInt(e.target.dataset.tab); 59 | this.props.updateActiveTab(tab); 60 | 61 | } 62 | 63 | render() { 64 | console.dev('render Tab'); 65 | 66 | return ( 67 |
    68 |
    69 | 75 |
    76 |
    77 | ) 78 | } 79 | } -------------------------------------------------------------------------------- /src/page/spa/components/tab/index.scss: -------------------------------------------------------------------------------- 1 | .cm-tabs { 2 | position: fixed; 3 | top: 0; 4 | z-index: 99; 5 | 6 | width: 100%; 7 | } 8 | .nav { 9 | position: relative; 10 | line-height: 45px; 11 | background-color: #fff; 12 | 13 | &:after { 14 | border-bottom: 1px solid #dbdbdb; 15 | } 16 | 17 | .title-list { 18 | display: table; 19 | width: 100%; 20 | 21 | table-layout: fixed; 22 | } 23 | 24 | .title-list li { 25 | display: table-cell; 26 | box-sizing: border-box; 27 | font-size: 16px; 28 | text-align: center; 29 | color: #777; 30 | } 31 | 32 | .title-list li.active { 33 | color: #00a5e0; 34 | } 35 | 36 | .icon-active { 37 | position: absolute; 38 | bottom: 0; 39 | left: 0; 40 | 41 | width: 50%; 42 | height: 4px; 43 | 44 | transform: left .3s ease-in-out; 45 | 46 | background-color: #00a5e0; 47 | } 48 | 49 | .icon-active.pull-right { 50 | right: 0; 51 | left: initial; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/page/spa/connect/connect.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { request } from '../../common/actions/actions'; 3 | import { getArgs, updateActiveTab, toggleContent, 4 | toggleListLoading, toggleSpinLoading, toggleDialog, likeNews, dislikeNews } from '../actions/actions'; 5 | 6 | // Map Redux state to component props 7 | // ownProps stores react-router-redux props 8 | function mapStateToProps(state, ownProps) { 9 | return { 10 | args: state.args, 11 | tabs: state.tabs, 12 | news: state.news, 13 | details: state.details, 14 | comments: state.comments, 15 | spinLoading: state.spinLoading, 16 | listLoading: state.listLoading, 17 | }; 18 | } 19 | 20 | // Map Redux actions to component props 21 | function mapDispatchToProps(dispatch) { 22 | return { 23 | request: (cgiName, params, opts) => dispatch(request(cgiName, params, opts)), 24 | getArgs: (value) => dispatch(getArgs(value)), 25 | toggleListLoading: (value) => dispatch(toggleListLoading(value)), 26 | toggleSpinLoading: (value) => dispatch(toggleSpinLoading(value)), 27 | updateActiveTab: (value) => dispatch(updateActiveTab(value)), 28 | likeNews: (value) => dispatch(likeNews(value)), 29 | dislikeNews: (value) => dispatch(dislikeNews(value)), 30 | }; 31 | } 32 | 33 | export default connect( 34 | mapStateToProps, 35 | mapDispatchToProps 36 | ); -------------------------------------------------------------------------------- /src/page/spa/constants/constants.js: -------------------------------------------------------------------------------- 1 | export const LATEST_NEWS = 10; 2 | export const LIKE_NEWS = 11; 3 | 4 | export const DEBUG = true; -------------------------------------------------------------------------------- /src/page/spa/container/app.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { render } from 'react-dom'; 3 | import Connect from '../connect/connect'; 4 | import { Link, browserHistory } from 'react-router'; 5 | 6 | 7 | require('./index.scss'); 8 | 9 | 10 | function App(props) { 11 | 12 | return ( 13 |
    14 | {props.children} 15 |
    16 | ) 17 | } 18 | 19 | App.contextTypes = { 20 | router: React.PropTypes.object.isRequired 21 | }; 22 | 23 | export default Connect(App); -------------------------------------------------------------------------------- /src/page/spa/container/comment.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import merge from 'lodash.merge'; 3 | import { render } from 'react-dom'; 4 | import { formatDate } from 'utils'; 5 | import Connect from '../connect/connect'; 6 | import { GET_COMMENT_LIST } from '../../common/constants/constants'; 7 | import { LATEST_NEWS, LIKE_NEWS } from '../constants/constants'; 8 | 9 | import Spinner from 'spinner'; 10 | import Touch from 'touch'; 11 | 12 | 13 | require('./comment.scss'); 14 | 15 | 16 | class Comment extends Component { 17 | 18 | constructor(props, context) { 19 | super(props, context); 20 | this.state = { 21 | 22 | }; 23 | this.commentId = this.props.params.id; 24 | 25 | } 26 | 27 | componentDidMount() { 28 | 29 | } 30 | 31 | componentWillMount() { 32 | if (!this.props.comments.hasOwnProperty(this.commentId)) { 33 | this.getCommentList(); 34 | } 35 | } 36 | 37 | getCommentList() { 38 | let url = GET_COMMENT_LIST, 39 | opts = {}; 40 | 41 | var pa = merge({}, { 42 | comment_id: this.props.params.id, 43 | otype: "jsonp", 44 | callback: "renderComment", 45 | lcount: 20, 46 | from: 'share', 47 | v: (new Date()).getTime(), 48 | }, pa); 49 | 50 | var param = { 51 | param: pa, 52 | ajaxType: 'JSONP', 53 | onSuccess: function(data) { 54 | // console.log(data); 55 | }, 56 | onError: function(res) { 57 | console.log("err"); 58 | } 59 | }; 60 | 61 | this.props.request(url, param, opts); 62 | } 63 | 64 | render() { 65 | var commentId = this.commentId; 66 | var commentData = (this.props.comments.hasOwnProperty(commentId)) ? 67 | this.props.comments[commentId] : []; 68 | 69 | var commentList = commentData.map((items, index) => { 70 | let item = items[0]; 71 | 72 | return ( 73 |
    74 |
    75 |
    76 | 77 |
    78 |
    79 | {item.nick} 80 | {formatDate(item.pub_time, 2)} 81 |
    82 |
    83 |

    {item.reply_content}

    84 |
    85 |
    86 |
    87 | ) 88 | }); 89 | 90 | commentList = (!commentList.length) ?
    暂无评论
    : commentList; 91 | 92 | return ( 93 |
    94 |
    95 |

    96 | 精选评论 97 | { 98 | this.context.router.goBack(); 99 | // this.context.router 100 | }}>
    返回
    101 |

    102 | 103 |
    104 | {commentList} 105 |
    106 |
    107 | 108 |
    109 | ) 110 | } 111 | } 112 | 113 | Comment.contextTypes = { 114 | router: React.PropTypes.object.isRequired 115 | }; 116 | 117 | export default Connect(Comment); -------------------------------------------------------------------------------- /src/page/spa/container/comment.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | @import"../../../css/common/common"; 3 | @import"../../../css/common/reset"; 4 | 5 | html, body { 6 | height: 100%; 7 | width: 100%; 8 | } 9 | 10 | body { 11 | background-color: #f8f9fb; 12 | } 13 | 14 | .comment-list { 15 | 16 | h1 { 17 | margin-bottom: 10px; 18 | font-size: 19px; 19 | color: #28292d; 20 | line-height: 19px; 21 | border-left: 4px solid #ff9c00; 22 | padding-left: 11px; 23 | 24 | div { 25 | float: right; 26 | font-size: 15px; 27 | font-weight: 100; 28 | } 29 | 30 | .back { 31 | font-size: 18px; 32 | color: #ff9c00; 33 | padding: 5px; 34 | } 35 | } 36 | } 37 | 38 | .comment-list_item { 39 | display: block; 40 | 41 | .item { 42 | position: relative; 43 | margin-left: 15px; 44 | margin-right: 15px; 45 | color: #47494c; 46 | border-bottom: 1px solid #e0e0e0; 47 | vertical-align: top; 48 | 49 | .avatar { 50 | position: absolute; 51 | top: 15px; 52 | 53 | img { 54 | display: block; 55 | width: 30px; 56 | height: 30px; 57 | border-radius: 30px; 58 | } 59 | } 60 | 61 | .nameBar { 62 | padding-top: 14px; 63 | margin-left: 40px; 64 | font-size: 14px; 65 | overflow: hidden; 66 | text-overflow: ellipsis; 67 | font-weight: 700; 68 | 69 | span { 70 | float: right; 71 | font-size: 13px; 72 | color: #8d8d8d; 73 | } 74 | } 75 | 76 | .contentBox { 77 | padding-left: 40px; 78 | overflow: hidden; 79 | 80 | p { 81 | font-size: 14px; 82 | line-height: 24px; 83 | margin: 7px 0 10px; 84 | white-space: normal; 85 | word-break: break-all; 86 | } 87 | } 88 | } 89 | 90 | 91 | } -------------------------------------------------------------------------------- /src/page/spa/container/detail.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import merge from 'lodash.merge'; 3 | import { render } from 'react-dom'; 4 | import { formatDate } from 'utils'; 5 | import Connect from '../connect/connect'; 6 | import { GET_COMMENT_LIST, GET_NEWS_DETAIL } from '../../common/constants/constants'; 7 | import { LATEST_NEWS, LIKE_NEWS } from '../constants/constants'; 8 | 9 | import Spinner from 'spinner'; 10 | import Touch from 'touch'; 11 | 12 | require('./detail.scss'); 13 | 14 | let spaPath = ""; 15 | if ("__DEV__" === process.env.NODE_ENV || "__PROD__" === process.env.NODE_ENV) { 16 | spaPath = "spa.html"; 17 | } 18 | else { 19 | spaPath = "spa"; 20 | } 21 | 22 | class Detail extends Component { 23 | 24 | constructor(props, context) { 25 | super(props, context); 26 | this.state = { 27 | 28 | }; 29 | this.newsId = this.props.params.id; 30 | this.commentId = this.props.params.commentid; 31 | this.getNewsDetail = this.getNewsDetail.bind(this); 32 | } 33 | 34 | componentDidMount() { 35 | if (!this.props.details.hasOwnProperty(this.newsId)) { 36 | this.getNewsDetail(this.newsId); 37 | } 38 | } 39 | 40 | componentWillMount() { 41 | 42 | } 43 | 44 | getNewsDetail(newsId) { 45 | let url = GET_NEWS_DETAIL, 46 | opts = {}; 47 | 48 | var pa = merge({}, { 49 | // url: item.url, 50 | news_id: newsId,//item.id, 51 | v: (new Date()).getTime(), 52 | }, pa); 53 | 54 | var param = { 55 | param: pa, 56 | ajaxType: 'POST', 57 | onSuccess: function(data) { 58 | 59 | }, 60 | onError: function(res) { 61 | console.log("err"); 62 | } 63 | }; 64 | 65 | this.props.request(url, param, opts); 66 | } 67 | 68 | render() { 69 | var details = this.props.details || {}, 70 | detailStr = details.hasOwnProperty(this.newsId) ? details[this.newsId] : ''; 71 | 72 | // console.dev(detailStr); 73 | var detailContent = detailStr.split('\n\n').map((item, index) => { 74 | // console.log(item); 75 | switch (index) { 76 | case 0: 77 | return ( 78 |

    {item}

    79 | ); 80 | case 1: 81 | return ( 82 |

    {item}

    83 | ); 84 | default: 85 | 86 | var regex = new RegExp('(\[http:\/\/(\w.+)\])', 'i'); 87 | var matches = item.match(regex); 88 | // console.log(matches); 89 | if (matches !== null && !!~matches.input.indexOf("\[http://")) { 90 | // console.log(item); 91 | return ( 92 |

    93 | 94 |

    95 | ) 96 | } 97 | else { 98 | return ( 99 |

    {item}

    100 | ); 101 | } 102 | } 103 | }); 104 | 105 | return ( 106 |
    107 | {detailContent} 108 |
    109 | { 110 | // this.context.router.goBack(); 111 | this.context.router.push('/' + spaPath); 112 | // this.context.router 113 | }}>首页 114 | { 115 | this.context.router.push('/' + spaPath + '/comment/' + this.commentId); 116 | }}>精彩评论 117 |
    118 | 119 |
    120 | ) 121 | } 122 | } 123 | 124 | Detail.contextTypes = { 125 | router: React.PropTypes.object.isRequired 126 | }; 127 | 128 | export default Connect(Detail); -------------------------------------------------------------------------------- /src/page/spa/container/detail.scss: -------------------------------------------------------------------------------- 1 | .detail-wrapper { 2 | 3 | .title { 4 | font-size: 22px; 5 | font-weight: 700; 6 | padding: 8px 10px; 7 | word-wrap: break-word; 8 | text-align: center; 9 | } 10 | 11 | .src { 12 | font-size: 13px; 13 | border-bottom: 1px solid #d6d6d6; 14 | color: #aaa; 15 | padding-bottom: 10px; 16 | position: relative; 17 | text-align: center; 18 | } 19 | 20 | .text { 21 | font-size: 17px; 22 | text-align: justify; 23 | word-wrap: break-word; 24 | line-height: 25px; 25 | padding: 8px 13px; 26 | } 27 | 28 | .imgNode { 29 | text-align: center; 30 | padding: 10px 15px; 31 | } 32 | 33 | .btns { 34 | display: inline-block; 35 | 36 | div { 37 | display: inline-block; 38 | padding: 10px; 39 | font-size: 18px; 40 | color: #ff9c00; 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/page/spa/container/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import merge from 'lodash.merge'; 3 | import { render } from 'react-dom'; 4 | import Connect from '../connect/connect'; 5 | import { GET_NEWS_LIST, GET_TOP_NEWS, GET_NEWS_DETAIL } from '../../common/constants/constants'; 6 | import { LATEST_NEWS, LIKE_NEWS } from '../constants/constants'; 7 | import { platform } from 'utils'; 8 | 9 | require('./index.scss'); 10 | 11 | import Scroll from 'scroll'; 12 | import Spinner from 'spinner'; 13 | import List from '../components/list/index'; 14 | import Tab from '../components/tab/index'; 15 | import Loading from '../components/loading/index'; 16 | 17 | if (platform().ios) { 18 | document.body.className = "ios"; 19 | } 20 | 21 | 22 | class Wrapper extends Component { 23 | 24 | constructor(props, context) { 25 | super(props, context); 26 | this.state = { 27 | lock: true 28 | }; 29 | this.firstGetAllData = false; 30 | this.loadTopNews = this.loadTopNews.bind(this); 31 | this.loadNewsList = this.loadNewsList.bind(this); 32 | this.loadData = this.loadData.bind(this); 33 | this.loadDataForScroll = this.loadDataForScroll.bind(this); 34 | this.getNewsDetail = this.getNewsDetail.bind(this); 35 | } 36 | 37 | componentDidMount() { 38 | setTimeout(() => { 39 | this.setState({ 40 | lock: false, 41 | }); 42 | }, 100); 43 | 44 | this.props.toggleSpinLoading(false); 45 | } 46 | 47 | componentWillMount() { 48 | if (this.props.news.ids.length === 0 && !isNode) { 49 | this.loadTopNews(); 50 | } 51 | } 52 | 53 | componentWillReceiveProps(nextProps) { 54 | 55 | return true; 56 | } 57 | 58 | loadDataForScroll() { 59 | this.loadNewsList(null); 60 | } 61 | 62 | loadTopNews() { 63 | 64 | var url = GET_TOP_NEWS, 65 | opts = {}; 66 | 67 | var pa = merge({}, { 68 | chlid: 'news_news_top', 69 | refer: 'mobilewwwqqcom', 70 | otype: 'jsonp', 71 | callback: 'getNewsIndexOutput', 72 | t: (new Date()).getTime() 73 | }, pa); 74 | 75 | var param = { 76 | param: pa, 77 | ajaxType: 'JSONP', 78 | onSuccess: function(res) { 79 | // console.log(res); 80 | }, 81 | onError: function(res) { 82 | // console.log(res); 83 | // alert(res.errMsg || '加载新闻列表失败,请稍后重试'); 84 | } 85 | }; 86 | 87 | this.props.request(url, param, opts); 88 | } 89 | 90 | loadNewsList(props) { 91 | var props = props || this.props; 92 | 93 | this.loadData(LATEST_NEWS, {}); 94 | } 95 | 96 | //http://mat1.gtimg.com/www/mobi/image/loadimg.png 97 | 98 | loadData(listType, pa = {}, opts = {}) { 99 | var _this = this; 100 | var url = GET_NEWS_LIST; 101 | 102 | var listInfoParam = this.props.news.listInfo['listLatest'], 103 | ids = this.props.news.ids, 104 | args = this.props.args; 105 | 106 | // 防止重复拉取 107 | if (listInfoParam.isLoading) { 108 | return; 109 | } 110 | 111 | var curPage = listInfoParam.curPage, 112 | page_size = listInfoParam.pageSize, 113 | startIndex = 0 + (curPage) * page_size, 114 | endIndex = startIndex + page_size; 115 | 116 | var newIds = ids.slice(startIndex, endIndex), 117 | newIdArray = []; 118 | 119 | 120 | newIds.forEach((item, index) => { 121 | newIdArray.push(item.id); 122 | }); 123 | 124 | var pa = merge({}, { 125 | cmd: GET_NEWS_LIST, 126 | ids: newIdArray.join(','), 127 | refer: "mobilewwwqqcom", 128 | otype: "jsonp", 129 | callback: "getNewsContentOnlyOutput", 130 | t: (new Date()).getTime(), 131 | }, pa); 132 | 133 | var param = { 134 | param: pa, 135 | ajaxType: 'JSONP', 136 | onSuccess: function(data) { 137 | console.log(data); 138 | }, 139 | onError: function(res) { 140 | console.log("err"); 141 | // console.log(res); 142 | // alert(res.errMsg || '加载新闻列表失败,请稍后重试'); 143 | } 144 | }; 145 | 146 | this.props.request(url, param, opts); 147 | } 148 | 149 | getNewsDetail(newsId) { 150 | let url = GET_NEWS_DETAIL, 151 | opts = {}; 152 | 153 | var pa = merge({}, { 154 | // url: item.url, 155 | news_id: newsId,//item.id, 156 | v: (new Date()).getTime(), 157 | }, pa); 158 | 159 | var param = { 160 | param: pa, 161 | ajaxType: 'POST', 162 | onSuccess: function(data) { 163 | 164 | }, 165 | onError: function(res) { 166 | console.log("err"); 167 | } 168 | }; 169 | 170 | this.props.request(url, param, opts); 171 | } 172 | 173 | render() { 174 | // console.log(this.state.lock); 175 | console.dev('render container!!!'); 176 | let tabStyle = this.props.tabs, 177 | isEnd = this.props.news.listInfo['listLatest']['isEnd'], 178 | isLoadingShow = tabStyle === LATEST_NEWS; 179 | 180 | return ( 181 |
    182 | 186 |
    187 | 193 | 203 | 213 | 214 | 215 |
    216 | 217 |
    218 | ) 219 | } 220 | } 221 | 222 | Wrapper.contextTypes = { 223 | router: React.PropTypes.object.isRequired 224 | }; 225 | 226 | export default Connect(Wrapper); -------------------------------------------------------------------------------- /src/page/spa/container/index.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | @import"../../../css/common/common"; 3 | @import"../../../css/common/reset"; 4 | @import"../../../css/sprites/list_s"; 5 | 6 | html, body { 7 | height: 100%; 8 | width: 100%; 9 | } 10 | 11 | body { 12 | background-color: #f8f9fb; 13 | } 14 | 15 | .ios .cm-page-wrap { 16 | height: 100%; 17 | 18 | > div { 19 | height: 100%; 20 | 21 | > div { 22 | height: 100%; 23 | } 24 | } 25 | 26 | .cm-page { 27 | height: 100%; 28 | } 29 | 30 | .cm-content { 31 | position: relative; 32 | height: 100%; 33 | } 34 | 35 | .content-wrap { 36 | position: absolute; 37 | height: 100%; 38 | width: 100%; 39 | overflow: auto; 40 | } 41 | } -------------------------------------------------------------------------------- /src/page/spa/main.js: -------------------------------------------------------------------------------- 1 | import Root from './root/Root'; -------------------------------------------------------------------------------- /src/page/spa/reducers/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { routerReducer } from 'react-router-redux' 3 | import merge from 'lodash.merge'; 4 | import { setItem } from 'utils'; 5 | import initialState from '../stores/stores'; 6 | import { GET_NEWS_LIST, GET_TOP_NEWS, GET_COMMENT_LIST, GET_NEWS_DETAIL } from '../../common/constants/constants'; 7 | import { GET_ARGS, TABS_UPDATE, TOGGLE_CONTENT, 8 | TOGGLE_LIST_LOADING, TOGGLE_SPIN_LOADING, LIKE_NEWS, DISLIKE_NEWS } from '../actions/actions'; 9 | 10 | 11 | var news = function(state = initialState.news, action) { 12 | let listInfoMap = { 13 | 10: 'listLatest', // 最新新闻 14 | 11: 'listLike', // 收藏新闻 15 | }; 16 | 17 | switch(action.type) { 18 | 19 | case GET_TOP_NEWS + '_SUCCESS': 20 | 21 | if (!action.data || !action.data.idlist || action.data.idlist.length === 0) { 22 | return state; 23 | } 24 | 25 | var idlist = action.data.idlist, 26 | newState = merge({}, state); 27 | 28 | newState.ids = merge([], idlist[0].ids); 29 | newState.listLatest = merge([], newState.listLatest.concat(idlist[0].newslist)); 30 | 31 | return newState; 32 | 33 | 34 | case GET_NEWS_LIST + '_ON': 35 | var newState = merge({}, state); 36 | newState.listInfo['listLatest'].isLoading = true; 37 | 38 | return newState; 39 | 40 | case GET_NEWS_LIST + '_SUCCESS': 41 | 42 | if (!action.data || !action.data.newslist) { 43 | return state; 44 | } 45 | 46 | var newState = merge({}, state), 47 | listInfo = { 48 | curPage: (++newState.listInfo['listLatest'].curPage), 49 | isLoading: false, 50 | }; 51 | 52 | newState.listInfo['listLatest'] = merge({}, newState.listInfo['listLatest'], listInfo); 53 | newState['listLatest'] = newState['listLatest'].concat(action.data.newslist); 54 | 55 | return newState; 56 | 57 | case GET_NEWS_LIST + '_ERROR': 58 | var newState = merge({}, state); 59 | newState.listInfo['listLatest'].isLoading = false; 60 | 61 | return newState; 62 | 63 | case LIKE_NEWS: 64 | if (!action.value) { 65 | return state; 66 | } 67 | 68 | var newState = merge({}, state), 69 | isDuplicate = false; 70 | 71 | newState['listLike'].map((item, index) => { 72 | if (item.id === action.value.id) { 73 | isDuplicate = true; 74 | } 75 | }); 76 | 77 | if (isDuplicate) { 78 | return newState; 79 | } 80 | 81 | newState['listLike'] = newState['listLike'].concat(action.value); 82 | setItem('like-list', JSON.stringify(newState['listLike'])); 83 | 84 | return newState; 85 | 86 | case DISLIKE_NEWS: 87 | if (!action.value) { 88 | return state; 89 | } 90 | 91 | var newState = merge({}, state); 92 | newState['listLike'] = newState['listLike'].filter((item, index) => { 93 | return (item.id !== action.value.id); 94 | }); 95 | setItem('like-list', JSON.stringify(newState['listLike'])); 96 | 97 | return newState; 98 | 99 | default: 100 | return state; 101 | } 102 | }; 103 | 104 | var details = function(state = initialState.details, action) { 105 | switch (action.type) { 106 | case GET_NEWS_DETAIL + '_SUCCESS': 107 | var newState = merge({}, state); 108 | if (!action.data || !action.data.content) { 109 | return newState; 110 | } 111 | newState[action.param.news_id] = action.data.content; 112 | return newState; 113 | default: 114 | return state; 115 | } 116 | } 117 | 118 | var comments = function(state = initialState.comments, action) { 119 | switch (action.type) { 120 | case GET_COMMENT_LIST + '_SUCCESS': 121 | var newState = merge({}, state); 122 | if (!action.data || !action.data.comments || !action.data.comments.list) { 123 | return newState; 124 | } 125 | 126 | newState[action.param.comment_id] = action.data.comments.list; 127 | return newState; 128 | default: 129 | 130 | return state; 131 | } 132 | }; 133 | 134 | var args = function(state = initialState.args, action) { 135 | switch(action.type) { 136 | case GET_ARGS: 137 | return merge({}, state, action.value); 138 | default: 139 | return state; 140 | } 141 | }; 142 | 143 | var tabs = function(state = initialState.tabs, action) { 144 | switch(action.type) { 145 | case TABS_UPDATE: 146 | return action.value; 147 | default: 148 | return state; 149 | } 150 | }; 151 | 152 | var listLoading = function(state = initialState.listLoading, action) { 153 | switch(action.type) { 154 | case TOGGLE_LIST_LOADING: 155 | return action.value; 156 | 157 | default: 158 | return state; 159 | } 160 | }; 161 | 162 | var spinLoading = function(state = initialState.spinLoading, action) { 163 | switch(action.type) { 164 | case TOGGLE_SPIN_LOADING: 165 | return action.value; 166 | 167 | case GET_COMMENT_LIST + '_ON': 168 | case GET_NEWS_DETAIL + '_ON': 169 | return true; 170 | 171 | case GET_TOP_NEWS + '_SUCCESS': 172 | case GET_NEWS_LIST + '_SUCCESS': 173 | case GET_COMMENT_LIST + '_SUCCESS': 174 | case GET_COMMENT_LIST + '_ERROR': 175 | case GET_NEWS_DETAIL + '_SUCCESS': 176 | case GET_NEWS_DETAIL + '_ERROR': 177 | return false; 178 | 179 | default: 180 | return state; 181 | } 182 | }; 183 | 184 | 185 | const rootReducer = combineReducers({ 186 | routing: routerReducer, 187 | args, 188 | tabs, 189 | news, 190 | details, 191 | comments, 192 | listLoading, 193 | spinLoading, 194 | }); 195 | 196 | export default rootReducer; -------------------------------------------------------------------------------- /src/page/spa/root/Root.dev.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { createStore } from 'redux'; 4 | import { Provider } from 'react-redux'; 5 | import { configureStore } from '../stores/configureStore'; 6 | import { initialStore } from '../stores/stores'; 7 | 8 | import IndexWrapper from '../container/index'; 9 | import CommentWrapper from '../container/comment'; 10 | import DetailWrapper from '../container/detail'; 11 | 12 | import App from '../container/app'; 13 | import DevTools from '../../common/devtools/DevTools'; 14 | import { DEBUG } from '../constants/constants'; 15 | import { routeConfig } from './route_server'; 16 | 17 | import { syncHistoryWithStore } from 'react-router-redux'; 18 | import { Router, IndexRoute, Route, browserHistory, useRouterHistory, hashHistory, match } from 'react-router'; 19 | import { createHashHistory, createHistory } from 'history'; 20 | 21 | var globalVar = (isNode) ? global : window; 22 | 23 | let store = configureStore(globalVar.__REDUX_STATE__ || {}); 24 | 25 | let history = syncHistoryWithStore(browserHistory, store); 26 | 27 | var DevToolsWrapper = (DEBUG) ? : null; 28 | 29 | const { pathname, search, hash } = window.location; 30 | const location = `${pathname}${search}${hash}`; 31 | 32 | export default class Root extends Component { 33 | 34 | constructor(props, context) { 35 | super(props, context); 36 | } 37 | 38 | render() { 39 | return ( 40 | 41 |
    42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | {/* */} 50 | {DevToolsWrapper} 51 |
    52 |
    53 | ); 54 | } 55 | } 56 | 57 | match({ routes: routeConfig, location: location }, () => { 58 | render( 59 | 60 |
    61 | 62 |
    63 |
    , 64 | document.getElementById('pages') 65 | ) 66 | }); 67 | 68 | // render( 69 | // , 70 | // document.getElementById('pages') 71 | // ); 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/page/spa/root/Root.dev_browser.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { createStore } from 'redux'; 4 | import { Provider } from 'react-redux'; 5 | import { configureStore } from '../stores/configureStore'; 6 | import { initialStore } from '../stores/stores'; 7 | 8 | import IndexWrapper from '../container/index'; 9 | import CommentWrapper from '../container/comment'; 10 | import DetailWrapper from '../container/detail'; 11 | 12 | import App from '../container/app'; 13 | import DevTools from '../../common/devtools/DevTools'; 14 | import { DEBUG } from '../constants/constants'; 15 | import { routeConfig } from './route'; 16 | 17 | import { syncHistoryWithStore } from 'react-router-redux'; 18 | import { Router, IndexRoute, Route, browserHistory, useRouterHistory, hashHistory } from 'react-router'; 19 | import { createHashHistory } from 'history'; 20 | 21 | var globalVar = (isNode) ? global : window; 22 | 23 | let store = configureStore(globalVar.__REDUX_STATE__ || {}); 24 | 25 | let history = syncHistoryWithStore(browserHistory, store); 26 | 27 | var DevToolsWrapper = (DEBUG) ? : null; 28 | 29 | export default class Root extends Component { 30 | 31 | constructor(props, context) { 32 | super(props, context); 33 | } 34 | 35 | render() { 36 | return ( 37 | 38 |
    39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | {/* */} 47 | {DevToolsWrapper} 48 |
    49 |
    50 | ); 51 | } 52 | } 53 | 54 | render( 55 | , 56 | document.getElementById('pages') 57 | ); 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/page/spa/root/Root.dev_hash.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { createStore } from 'redux'; 4 | import { Provider } from 'react-redux'; 5 | import { configureStore } from '../stores/configureStore'; 6 | import { initialStore } from '../stores/stores'; 7 | 8 | import IndexWrapper from '../container/index'; 9 | import CommentWrapper from '../container/comment'; 10 | import DetailWrapper from '../container/detail'; 11 | 12 | import App from '../container/app'; 13 | import DevTools from '../../common/devtools/DevTools'; 14 | import { DEBUG } from '../constants/constants'; 15 | import { routeConfig } from './route'; 16 | 17 | import { syncHistoryWithStore } from 'react-router-redux'; 18 | 19 | import { Router, IndexRoute, Route, browserHistory, useRouterHistory, hashHistory } from 'react-router'; 20 | import { createHashHistory } from 'history'; 21 | 22 | var globalVar = (isNode) ? global : window; 23 | 24 | let store = configureStore(globalVar.__REDUX_STATE__ || {}); 25 | 26 | let history = syncHistoryWithStore(hashHistory, store); 27 | 28 | var DevToolsWrapper = (DEBUG) ? : null; 29 | 30 | export default class Root extends Component { 31 | 32 | constructor(props, context) { 33 | super(props, context); 34 | } 35 | 36 | render() { 37 | return ( 38 | 39 |
    40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | {/* */} 48 | {DevToolsWrapper} 49 |
    50 |
    51 | ); 52 | } 53 | } 54 | 55 | if (!isNode) { 56 | render( 57 | , 58 | document.getElementById('pages') 59 | ); 60 | } 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/page/spa/root/Root.js: -------------------------------------------------------------------------------- 1 | if ("__DEV_NODE__" === process.env.NODE_ENV || "__NODE_DEV__" === process.env.NODE_ENV) { 2 | module.exports = require('./Root.dev'); 3 | } 4 | else if ("__DEV__" === process.env.NODE_ENV) { 5 | module.exports = require('./Root.dev_browser'); 6 | } 7 | else if ("__PROD__" === process.env.NODE_ENV) { 8 | module.exports = require('./Root.prod_browser'); 9 | } 10 | else if ("__NODE_PROD__") { 11 | module.exports = require('./Root.prod'); 12 | } 13 | -------------------------------------------------------------------------------- /src/page/spa/root/Root.prod.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { createStore } from 'redux'; 4 | import { Provider } from 'react-redux'; 5 | import { configureStore } from '../stores/configureStore'; 6 | import { initialStore } from '../stores/stores'; 7 | 8 | import IndexWrapper from '../container/index'; 9 | import CommentWrapper from '../container/comment'; 10 | import DetailWrapper from '../container/detail'; 11 | 12 | import App from '../container/app'; 13 | 14 | import { routeConfig } from './route_server'; 15 | 16 | import { syncHistoryWithStore } from 'react-router-redux'; 17 | import { Router, IndexRoute, Route, browserHistory, useRouterHistory, hashHistory, match } from 'react-router'; 18 | import { createHashHistory } from 'history'; 19 | 20 | var globalVar = (isNode) ? global : window; 21 | 22 | let store = configureStore(globalVar.__REDUX_STATE__ || {}); 23 | 24 | let history = syncHistoryWithStore(browserHistory, store); 25 | 26 | const { pathname, search, hash } = window.location; 27 | const location = `${pathname}${search}${hash}`; 28 | 29 | export default class Root extends Component { 30 | 31 | constructor(props, context) { 32 | super(props, context); 33 | } 34 | 35 | render() { 36 | return ( 37 | 38 |
    39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
    47 |
    48 | ); 49 | } 50 | } 51 | 52 | match({ routes: routeConfig, location: location }, () => { 53 | render( 54 | 55 |
    56 | 57 |
    58 |
    , 59 | document.getElementById('pages') 60 | ) 61 | }); 62 | 63 | // render( 64 | // , 65 | // document.getElementById('pages') 66 | // ); 67 | 68 | -------------------------------------------------------------------------------- /src/page/spa/root/Root.prod_browser.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { createStore } from 'redux'; 4 | import { Provider } from 'react-redux'; 5 | import { configureStore } from '../stores/configureStore'; 6 | import { initialStore } from '../stores/stores'; 7 | 8 | import IndexWrapper from '../container/index'; 9 | import CommentWrapper from '../container/comment'; 10 | import DetailWrapper from '../container/detail'; 11 | 12 | import App from '../container/app'; 13 | 14 | import { routeConfig } from './route'; 15 | 16 | import { syncHistoryWithStore } from 'react-router-redux'; 17 | import { Router, IndexRoute, Route, browserHistory, useRouterHistory, hashHistory } from 'react-router'; 18 | import { createHashHistory } from 'history'; 19 | 20 | var globalVar = (isNode) ? global : window; 21 | 22 | let store = configureStore(globalVar.__REDUX_STATE__ || {}); 23 | 24 | let history = syncHistoryWithStore(browserHistory, store); 25 | 26 | export default class Root extends Component { 27 | 28 | constructor(props, context) { 29 | super(props, context); 30 | } 31 | 32 | render() { 33 | return ( 34 | 35 |
    36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
    44 |
    45 | ); 46 | } 47 | } 48 | 49 | render( 50 | , 51 | document.getElementById('pages') 52 | ); 53 | 54 | -------------------------------------------------------------------------------- /src/page/spa/root/route.js: -------------------------------------------------------------------------------- 1 | import IndexWrapper from '../container/index'; 2 | import CommentWrapper from '../container/comment'; 3 | import App from '../container/app'; 4 | 5 | export const routeConfig = [ 6 | { path: '/spa.html', 7 | component: App, 8 | indexRoute: { 9 | component: IndexWrapper, 10 | }, 11 | childRoutes:[ 12 | { 13 | path: '', 14 | component: IndexWrapper 15 | }, 16 | { 17 | path: '/comment', 18 | component: CommentWrapper, 19 | } 20 | ] 21 | } 22 | ]; 23 | -------------------------------------------------------------------------------- /src/page/spa/root/route_server.js: -------------------------------------------------------------------------------- 1 | import IndexWrapper from '../container/index'; 2 | import CommentWrapper from '../container/comment'; 3 | import DetailWrapper from '../container/detail'; 4 | import App from '../container/app'; 5 | 6 | export const routeConfig = [ 7 | { path: '/spa', 8 | component: App, 9 | indexRoute: { 10 | component: IndexWrapper, 11 | }, 12 | childRoutes:[ 13 | { 14 | path: '/spa/detail/:id/:commentid', 15 | component: DetailWrapper 16 | }, 17 | { 18 | path: '/spa/comment/:id', 19 | component: CommentWrapper, 20 | } 21 | ] 22 | } 23 | ]; 24 | 25 | // export const routeConfig = { 26 | // path: '/spa/', 27 | // component: App, 28 | // indexRoute: { 29 | // component: IndexWrapper 30 | // } 31 | // }; 32 | -------------------------------------------------------------------------------- /src/page/spa/stores/configureStore.dev.js: -------------------------------------------------------------------------------- 1 | import { createStore, compose, applyMiddleware } from 'redux'; 2 | import { browserHistory } from 'react-router'; 3 | import { syncHistoryWithStore, routerMiddleware } from 'react-router-redux'; 4 | import rootReducer from '../reducers/reducers'; 5 | import thunk from 'redux-thunk'; 6 | import { persistState } from 'redux-devtools'; 7 | import DevTools from '../../common/devtools/DevTools'; 8 | // import logger from '../../common/middleware/logger'; 9 | import api from '../../common/middleware/api'; 10 | import { DEBUG } from '../constants/constants'; 11 | 12 | 13 | const reduxRouterMiddleware = routerMiddleware(browserHistory); 14 | 15 | function getDebugSessionKey() { 16 | // You can write custom logic here! 17 | // By default we try to read the key from ?debug_session= in the address bar 18 | const matches = (!isNode) ? window.location.href.match(/[?&]debug_session=([^&]+)\b/) : null; 19 | return (matches && matches.length > 0) ? matches[1] : null; 20 | } 21 | 22 | var finalCreateStore = null; 23 | if (DEBUG) { 24 | finalCreateStore = compose( 25 | applyMiddleware(thunk, api, reduxRouterMiddleware), 26 | DevTools.instrument(), 27 | persistState(getDebugSessionKey()) 28 | )(createStore); 29 | } 30 | else { 31 | finalCreateStore = compose( 32 | applyMiddleware(thunk, api, reduxRouterMiddleware) 33 | )(createStore); 34 | } 35 | 36 | export function configureStore(initialState) { 37 | 38 | const store = finalCreateStore(rootReducer, initialState); 39 | 40 | // Required for replaying actions from devtools to work 41 | // reduxRouterMiddleware.listenForReplays(store); 42 | 43 | if (module.hot) { 44 | // Enable Webpack hot module replacement for reducers 45 | module.hot.accept('../reducers/reducers', () => { 46 | const nextRootReducer = require('../reducers/reducers').default; 47 | store.replaceReducer(nextRootReducer); 48 | }); 49 | } 50 | 51 | return store; 52 | } -------------------------------------------------------------------------------- /src/page/spa/stores/configureStore.dev_browser.js: -------------------------------------------------------------------------------- 1 | import { createStore, compose, applyMiddleware } from 'redux'; 2 | import { browserHistory } from 'react-router'; 3 | import { syncHistoryWithStore, routerMiddleware } from 'react-router-redux'; 4 | import rootReducer from '../reducers/reducers'; 5 | import thunk from 'redux-thunk'; 6 | import { persistState } from 'redux-devtools'; 7 | import DevTools from '../../common/devtools/DevTools'; 8 | // import logger from '../../common/middleware/logger'; 9 | import api from '../../common/middleware/api'; 10 | import { DEBUG } from '../constants/constants'; 11 | 12 | 13 | const reduxRouterMiddleware = routerMiddleware(browserHistory); 14 | 15 | function getDebugSessionKey() { 16 | // You can write custom logic here! 17 | // By default we try to read the key from ?debug_session= in the address bar 18 | const matches = (!isNode) ? window.location.href.match(/[?&]debug_session=([^&]+)\b/) : null; 19 | return (matches && matches.length > 0) ? matches[1] : null; 20 | } 21 | 22 | var finalCreateStore = null; 23 | if (DEBUG) { 24 | finalCreateStore = compose( 25 | applyMiddleware(thunk, api, reduxRouterMiddleware), 26 | DevTools.instrument(), 27 | persistState(getDebugSessionKey()) 28 | )(createStore); 29 | } 30 | else { 31 | finalCreateStore = compose( 32 | applyMiddleware(thunk, api, reduxRouterMiddleware) 33 | )(createStore); 34 | } 35 | 36 | export function configureStore(initialState) { 37 | 38 | const store = finalCreateStore(rootReducer, initialState); 39 | 40 | // Required for replaying actions from devtools to work 41 | // reduxRouterMiddleware.listenForReplays(store); 42 | 43 | if (module.hot) { 44 | // Enable Webpack hot module replacement for reducers 45 | module.hot.accept('../reducers/reducers', () => { 46 | const nextRootReducer = require('../reducers/reducers').default; 47 | store.replaceReducer(nextRootReducer); 48 | }); 49 | } 50 | 51 | return store; 52 | } -------------------------------------------------------------------------------- /src/page/spa/stores/configureStore.dev_hash.js: -------------------------------------------------------------------------------- 1 | import { createStore, compose, applyMiddleware } from 'redux'; 2 | import { Router, Route, browserHistory, useRouterHistory, hashHistory } from 'react-router'; 3 | import { createHashHistory } from 'history'; 4 | import { syncHistoryWithStore, routerMiddleware } from 'react-router-redux'; 5 | import rootReducer from '../reducers/reducers'; 6 | import thunk from 'redux-thunk'; 7 | import { persistState } from 'redux-devtools'; 8 | import DevTools from '../../common/devtools/DevTools'; 9 | // import logger from '../../common/middleware/logger'; 10 | import api from '../../common/middleware/api'; 11 | import { DEBUG } from '../constants/constants'; 12 | 13 | 14 | const reduxRouterMiddleware = routerMiddleware(hashHistory); 15 | 16 | function getDebugSessionKey() { 17 | // You can write custom logic here! 18 | // By default we try to read the key from ?debug_session= in the address bar 19 | const matches = (!isNode) ? window.location.href.match(/[?&]debug_session=([^&]+)\b/) : null; 20 | return (matches && matches.length > 0) ? matches[1] : null; 21 | } 22 | 23 | var finalCreateStore = null; 24 | if (DEBUG) { 25 | finalCreateStore = compose( 26 | applyMiddleware(thunk, api, reduxRouterMiddleware), 27 | DevTools.instrument(), 28 | persistState(getDebugSessionKey()) 29 | )(createStore); 30 | } 31 | else { 32 | finalCreateStore = compose( 33 | applyMiddleware(thunk, api, reduxRouterMiddleware) 34 | )(createStore); 35 | } 36 | 37 | export function configureStore(initialState) { 38 | 39 | const store = finalCreateStore(rootReducer, initialState); 40 | 41 | // Required for replaying actions from devtools to work 42 | // reduxRouterMiddleware.listenForReplays(store); 43 | 44 | if (module.hot) { 45 | // Enable Webpack hot module replacement for reducers 46 | module.hot.accept('../reducers/reducers', () => { 47 | const nextRootReducer = require('../reducers/reducers').default; 48 | store.replaceReducer(nextRootReducer); 49 | }); 50 | } 51 | 52 | return store; 53 | } -------------------------------------------------------------------------------- /src/page/spa/stores/configureStore.js: -------------------------------------------------------------------------------- 1 | if ("__DEV_NODE__" === process.env.NODE_ENV || "__NODE_DEV__" === process.env.NODE_ENV) { 2 | module.exports = require('./configureStore.dev'); 3 | } 4 | else if ("__DEV__" === process.env.NODE_ENV) { 5 | module.exports = require('./configureStore.dev_browser'); 6 | } 7 | else if ("__PROD__" === process.env.NODE_ENV) { 8 | module.exports = require('./configureStore.prod_browser'); 9 | } 10 | else if ("__NODE_PROD__") { 11 | module.exports = require('./configureStore.prod'); 12 | } 13 | -------------------------------------------------------------------------------- /src/page/spa/stores/configureStore.prod.js: -------------------------------------------------------------------------------- 1 | import { createStore, compose, applyMiddleware } from 'redux'; 2 | import { browserHistory } from 'react-router'; 3 | import { routerMiddleware } from 'react-router-redux'; 4 | import rootReducer from '../reducers/reducers'; 5 | import thunk from 'redux-thunk'; 6 | import logger from '../../common/middleware/logger'; 7 | import api from '../../common/middleware/api'; 8 | 9 | const reduxRouterMiddleware = routerMiddleware(browserHistory); 10 | 11 | const finalCreateStore = compose( 12 | applyMiddleware(thunk, api, reduxRouterMiddleware) 13 | )(createStore); 14 | 15 | export function configureStore(initialState) { 16 | 17 | const store = finalCreateStore(rootReducer, initialState); 18 | 19 | return store; 20 | } -------------------------------------------------------------------------------- /src/page/spa/stores/configureStore.prod_browser.js: -------------------------------------------------------------------------------- 1 | import { createStore, compose, applyMiddleware } from 'redux'; 2 | import { browserHistory } from 'react-router'; 3 | import { routerMiddleware } from 'react-router-redux'; 4 | import rootReducer from '../reducers/reducers'; 5 | import thunk from 'redux-thunk'; 6 | import logger from '../../common/middleware/logger'; 7 | import api from '../../common/middleware/api'; 8 | 9 | const reduxRouterMiddleware = routerMiddleware(browserHistory); 10 | 11 | const finalCreateStore = compose( 12 | applyMiddleware(thunk, api, reduxRouterMiddleware) 13 | )(createStore); 14 | 15 | export function configureStore(initialState) { 16 | 17 | const store = finalCreateStore(rootReducer, initialState); 18 | 19 | return store; 20 | } -------------------------------------------------------------------------------- /src/page/spa/stores/stores.js: -------------------------------------------------------------------------------- 1 | import { LATEST_NEWS, LIKE_NEWS } from '../constants/constants'; 2 | 3 | let src = null, 4 | listLike = []; 5 | 6 | if (!isNode) { 7 | var { getHash, getItem } = require('utils'); 8 | src = getHash('src'); 9 | listLike = JSON.parse(getItem('like-list')) || []; 10 | } 11 | 12 | /** other const **/ 13 | const initialState = { 14 | args: { 15 | src: src, 16 | }, 17 | tabs: LATEST_NEWS, 18 | news: { 19 | ids: [], // 新闻id 20 | listLatest: [], // 最新新闻 21 | listLike: listLike, // 收藏新闻 22 | listInfo: { 23 | listLatest:{ 24 | isEnd: false, 25 | pageSize: 20, 26 | curPage: 1, 27 | isLoading: false, 28 | }, 29 | listLike: { 30 | isEnd: false, 31 | pageSize: 20, 32 | curPage: 1, 33 | isLoading: false, 34 | } 35 | }, 36 | }, 37 | details: { 38 | 39 | }, 40 | comments: { 41 | 42 | }, 43 | listLoading: false, 44 | spinLoading: true 45 | }; 46 | 47 | 48 | export default initialState; -------------------------------------------------------------------------------- /src/spa.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 腾讯新闻 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
    16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var config = require('./config/config'); 3 | 4 | let configMapping = { 5 | '__DEV__': './webpack.dev.js', 6 | '__DEV_NODE__': './webpack.dev.js', 7 | '__PROD__': './webpack.prod.js', 8 | '__PROD_NODE__': './webpack.prod.js', 9 | '__NODE_DEV__': './webpack.node.js', 10 | '__NODE_PROD__': './webpack.node.js' 11 | }; 12 | 13 | var webpackConfigPath = configMapping[config.env], 14 | webpackConfig = require(webpackConfigPath); 15 | 16 | module.exports = webpackConfig; -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'), 4 | utils = require('./config/utils'), 5 | webpack = require('webpack'); 6 | 7 | var config = require('./config/config'), 8 | nodeModulesPath = path.join(__dirname, 'node_modules'), 9 | parentNodeModulePath = path.join(path.dirname(__dirname), 'node_modules'); 10 | 11 | var HtmlResWebpackPlugin = require('html-res-webpack-plugin'); 12 | var CopyWebpackPlugin = require("copy-webpack-plugin"); 13 | 14 | /** 15 | * [devConfig config for development mode] 16 | * @type {Object} 17 | */ 18 | var devConfig = { 19 | entry: { 20 | index: [path.join(config.path.src, "/page/index/main.js")], 21 | spa: [path.join(config.path.src, "/page/spa/main.js")], 22 | }, 23 | output: { 24 | publicPath: config.defaultPath, 25 | path: path.join(config.path.dist), 26 | filename: "js/[name]" + config.chunkhash + ".js", 27 | chunkFilename: "js/chunk/[name]" + config.chunkhash + ".js", 28 | }, 29 | module: { 30 | loaders: [ 31 | { 32 | test: /\.js?$/, 33 | loaders: ['react-hot'], 34 | exclude: /node_modules/, 35 | }, 36 | { 37 | test: /\.js?$/, 38 | loader: 'babel', 39 | query: { 40 | cacheDirectory: false,//'/webpack_cache/', 41 | plugins: ['transform-decorators-legacy'], 42 | presets: [ 43 | 'es2015-loose', 44 | 'react', 45 | ] 46 | }, 47 | exclude: /node_modules/, 48 | }, 49 | { 50 | test: /\.css$/, 51 | loader: "style-loader!css-loader", 52 | include: path.resolve(config.path.src) 53 | }, 54 | { 55 | test: /\.less$/, 56 | loader: "style-loader!css-loader!less-loader", 57 | include: [parentNodeModulePath, nodeModulesPath, path.resolve(config.path.src)] 58 | }, 59 | { 60 | test: /\.scss$/, 61 | loader: "style-loader!css-loader!sass-loader", 62 | include: [parentNodeModulePath, nodeModulesPath, path.resolve(config.path.src)] 63 | }, 64 | { 65 | test: /\.html$/, 66 | loader: 'html-loader' 67 | }, 68 | { 69 | test: /\.(jpe?g|png|gif|svg)$/i, 70 | loaders: [ 71 | "url-loader?limit=1000&name=img/[name]" + config.hash + ".[ext]", 72 | ], 73 | include: path.resolve(config.path.src) 74 | }, 75 | { 76 | test: /\.ico$/, 77 | loader: "url-loader?name=[name].[ext]", 78 | include: path.resolve(config.path.src) 79 | }, 80 | { 81 | test: /\.(woff|woff2|eot|ttf|svg)(\?.*$|$)/, 82 | loader: 'url-loader?importLoaders=1&limit=10000&name=fonts/[name]' + config.hash + '.[ext]' 83 | }, 84 | ], 85 | noParse: [ 86 | 87 | ] 88 | }, 89 | resolve: { 90 | moduledirectories:['node_modules', config.path.src], 91 | extensions: ["", ".js", ".jsx", ".es6", "css", "scss", "png", "jpg", "jpeg", "ico"], 92 | alias: { 93 | 'redux': 'redux/dist/redux', 94 | 'react-redux': 'react-redux/dist/react-redux', 95 | 'classnames': 'classnames', 96 | 'utils': path.join(config.path.src, '/js/common/utils'), 97 | 'spin': path.join(config.path.src, '/js/common/spin'), 98 | 'spinner': path.join(config.path.src, '/page/common/components/spinner/'), 99 | 'report': path.join(config.path.src, '/js/common/report'), 100 | 'touch': path.join(config.path.src, '/page/common/components/touch/'), 101 | 'scroll':path.join(config.path.src, '/page/common/components/scroll/'), 102 | 'immutable-pure-render-decorator': path.join(config.path.src, '/js/common/immutable-pure-render-decorator'), 103 | 'pure-render-decorator': path.join(config.path.src, '/js/common/pure-render-decorator'), 104 | } 105 | }, 106 | plugins: [ 107 | new webpack.optimize.OccurrenceOrderPlugin(), 108 | new webpack.NoErrorsPlugin(), 109 | new CopyWebpackPlugin([ 110 | { 111 | from: 'src/libs/', 112 | to: 'libs/' 113 | } 114 | ]), 115 | ], 116 | watch: true, // watch mode 117 | // devtool: "#inline-source-map", 118 | }; 119 | 120 | devConfig.addPlugins = function(plugin, opt) { 121 | devConfig.plugins.push(new plugin(opt)); 122 | }; 123 | 124 | config.html.forEach(function(page) { 125 | devConfig.addPlugins(HtmlResWebpackPlugin, { 126 | filename: page + ".html", 127 | template: "src/" + page + ".html", 128 | favicon: "src/favicon.ico", 129 | jsHash: "[name]" + config.chunkhash + ".js", 130 | cssHash: "[name]" + config.chunkhash + ".css", 131 | isHotReload: true, 132 | templateContent: function(tpl) { 133 | // 生产环境不作处理 134 | if (!this.options.isWatch) { 135 | return tpl; 136 | } 137 | // 开发环境先去掉外链react.js 138 | var regex = new RegExp("<\/script>", "ig"); 139 | tpl = tpl.replace(regex, function(script, route) { 140 | if (!!~script.indexOf('react.js') || !!~script.indexOf('react-dom.js')) { 141 | return ''; 142 | } 143 | return script; 144 | }); 145 | return tpl; 146 | }, 147 | htmlMinify: null 148 | }); 149 | }); 150 | 151 | devConfig.addPlugins(webpack.HotModuleReplacementPlugin); 152 | 153 | devConfig.addPlugins(webpack.DefinePlugin, { 154 | "process.env": { 155 | NODE_ENV: JSON.stringify(process.env.NODE_ENV) 156 | }, 157 | "isNode": false, 158 | "console.dev": function(msg) { console.log(msg); } 159 | }); 160 | 161 | module.exports = devConfig; -------------------------------------------------------------------------------- /webpack.node.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // require("babel-register")({ 4 | // ignore: /node_modules/, 5 | // optional: ["es7.objectRestSpread", "runtime"] 6 | // }); 7 | 8 | const path = require('path'), 9 | webpack = require('webpack'); 10 | 11 | var config = require('./config/config'), 12 | nodeModulesPath = path.join(__dirname, 'node_modules'), 13 | parentNodeModulePath = path.join(path.dirname(__dirname), 'node_modules'); 14 | 15 | /** 16 | * [nodeConfig config for backend] 17 | * @type {Object} 18 | */ 19 | var nodeConfig = { 20 | entry: { 21 | index: [path.join(config.path.node, "/asset/index.js")], 22 | detail: [path.join(config.path.node, "/asset/detail.js")], 23 | comment: [path.join(config.path.node, "/asset/comment.js")], 24 | }, 25 | output: { 26 | publicPath: config.defaultPath, 27 | path: path.join(config.path.pub), 28 | filename: "node/[name].js", 29 | }, 30 | target: 'node', 31 | node: { 32 | __filename: true, 33 | __dirname: true 34 | }, 35 | module: { 36 | loaders: [ 37 | { 38 | test: /\.js?$/, 39 | loader: 'babel', 40 | query: { 41 | cacheDirectory: '/webpack_cache/', 42 | plugins: [ 43 | 'transform-decorators-legacy', 44 | [ 45 | "transform-runtime", { 46 | "polyfill": false, 47 | "regenerator": true 48 | } 49 | ] 50 | ], 51 | presets: [ 52 | 'es2015-loose', 53 | 'react', 54 | ] 55 | }, 56 | exclude: /node_modules/, 57 | }, 58 | { 59 | test: /\.css$/, 60 | loader: "ignore-loader", 61 | }, 62 | // { 63 | // test: /\.less$/, 64 | // loader: "style-loader!css-loader!less-loader", 65 | // include: [parentNodeModulePath, nodeModulesPath, path.resolve(config.path.src)] 66 | // }, 67 | { 68 | test: /\.scss$/, 69 | loader: "ignore-loader", 70 | // include: [parentNodeModulePath, nodeModulesPath, path.resolve(config.path.src)] 71 | }, 72 | { 73 | test: /\.html$/, 74 | loader: 'html-loader' 75 | }, 76 | ], 77 | noParse: [ 78 | 79 | ] 80 | }, 81 | resolve: { 82 | moduledirectories:['node_modules', config.path.src], 83 | extensions: ["", ".js", ".jsx", ".es6", "css", "scss", "png", "jpg", "jpeg", "ico"], 84 | alias: { 85 | 'Root': path.join(config.path.src, '/page/spa/root/Root'), 86 | 'routes': path.join(config.path.src, '/page/spa/root/route_server'), 87 | 'configureStore': path.join(config.path.src, '/page/spa/stores/configureStore.js'), 88 | 'redux': 'redux/dist/redux', 89 | 'react-redux': 'react-redux/dist/react-redux', 90 | 'classnames': 'classnames', 91 | 'utils': path.join(config.path.src, '/js/common/utils'), 92 | 'spin': path.join(config.path.src, '/js/common/spin'), 93 | 'spinner': path.join(config.path.src, '/page/common/components/spinner/'), 94 | 'report': path.join(config.path.src, '/js/common/report'), 95 | 'touch': path.join(config.path.src, '/page/common/components/touch/'), 96 | 'scroll':path.join(config.path.src, '/page/common/components/scroll/'), 97 | 'immutable-pure-render-decorator': path.join(config.path.src, '/js/common/immutable-pure-render-decorator'), 98 | 'pure-render-decorator': path.join(config.path.src, '/js/common/pure-render-decorator'), 99 | 'requestSync': path.join(config.path.node, '/common/requestSync'), 100 | } 101 | }, 102 | plugins: [ 103 | new webpack.DefinePlugin({ 104 | "process.env": { 105 | NODE_ENV: JSON.stringify(process.env.NODE_ENV) 106 | }, 107 | "isNode": true, 108 | "console.dev": (process.env.NODE_ENV === "__NODE_DEV__") ? 109 | function(msg) { console.log(msg); } : 110 | function(msg) {} 111 | }), 112 | new webpack.BannerPlugin("module.exports = ", {entryOnly : true, raw: true}), 113 | // new webpack.optimize.UglifyJsPlugin( 114 | // { 115 | // compress: { 116 | // warnings: false 117 | // } 118 | // } 119 | // ) 120 | // new webpack.optimize.OccurrenceOrderPlugin(), 121 | // new webpack.NoErrorsPlugin() 122 | ], 123 | watch: true, // watch mode 124 | }; 125 | 126 | module.exports = nodeConfig; -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'), 6 | utils = require('./config/utils'), 7 | webpack = require('webpack'); 8 | 9 | var config = require('./config/config'), 10 | nodeModulesPath = path.join(__dirname, 'node_modules'), 11 | parentNodeModulePath = path.join(path.dirname(__dirname), 'node_modules'); 12 | 13 | var HtmlResWebpackPlugin = require('html-res-webpack-plugin'); 14 | var Clean = require('clean-webpack-plugin'); 15 | var ExtractTextPlugin = require("extract-text-webpack-plugin"); 16 | var CopyWebpackPlugin = require("copy-webpack-plugin"); 17 | var WebpackMd5Hash = require('webpack-md5-hash'); 18 | 19 | /** 20 | * [prodConfig config for production mode] 21 | * @type {Object} 22 | */ 23 | var prodConfig = { 24 | entry: { 25 | index: [path.join(config.path.src, "/page/index/main.js")], 26 | spa: [path.join(config.path.src, "/page/spa/main.js")], 27 | }, 28 | output: { 29 | publicPath: config.cdn, 30 | path: path.join(config.path.pub), 31 | filename: "js/[name]" + config.chunkhash + ".js", 32 | chunkFilename: "js/chunk/[name]" + config.chunkhash + ".js", 33 | }, 34 | module: { 35 | loaders: [ 36 | { 37 | test: /\.js?$/, 38 | loader: 'babel', 39 | query: { 40 | cacheDirectory: '/webpack_cache/', 41 | plugins: ['transform-decorators-legacy'], 42 | presets: [ 43 | 'es2015-loose', 44 | 'react', 45 | ] 46 | }, 47 | exclude: /node_modules/, 48 | }, 49 | { 50 | test: /\.css$/, 51 | // extract style and make it stand-alone css file 52 | // for dev environment, inline style can be hot reload 53 | loader: ExtractTextPlugin.extract("style-loader", "css-loader"), 54 | include: path.resolve(config.path.src) 55 | }, 56 | { 57 | test: /\.less$/, 58 | loader: ExtractTextPlugin.extract("style-loader", "css-loader!less-loader"), 59 | include: [parentNodeModulePath, nodeModulesPath, path.resolve(config.path.src)] 60 | }, 61 | { 62 | test: /\.scss$/, 63 | loader: ExtractTextPlugin.extract("style-loader", "css-loader!sass-loader"), 64 | include: [parentNodeModulePath, nodeModulesPath, path.resolve(config.path.src)] 65 | }, 66 | { 67 | test: /\.html$/, 68 | loader: 'html-loader' 69 | }, 70 | { 71 | test: /\.(jpe?g|png|gif|svg)$/i, 72 | loaders: [ 73 | "url-loader?limit=1000&name=img/[name]" + config.hash + ".[ext]", 74 | // 压缩png图片 75 | 'image-webpack?{progressive:true, optimizationLevel: 7, interlaced: false, pngquant:{quality: "65-90", speed: 4}}' 76 | ], 77 | include: path.resolve(config.path.src) 78 | }, 79 | { 80 | test: /\.ico$/, 81 | loader: "url-loader?name=[name].[ext]", 82 | include: path.resolve(config.path.src) 83 | }, 84 | { 85 | test: /\.(woff|woff2|eot|ttf|svg)(\?.*$|$)/, 86 | loader: 'url-loader?importLoaders=1&limit=10000&name=fonts/[name]' + config.hash + '.[ext]' 87 | }, 88 | ], 89 | noParse: [ 90 | 91 | ] 92 | }, 93 | resolve: { 94 | moduledirectories:['node_modules', config.path.src], 95 | extensions: ["", ".js", ".jsx", ".es6", "css", "scss", "png", "jpg", "jpeg", "ico"], 96 | alias: { 97 | // use production version of redux 98 | 'redux': 'redux/dist/redux.min', 99 | 'react-redux': 'react-redux/dist/react-redux', 100 | 'classnames': 'classnames', 101 | 'utils': path.join(config.path.src, '/js/common/utils'), 102 | 'spin': path.join(config.path.src, '/js/common/spin'), 103 | 'spinner': path.join(config.path.src, '/page/common/components/spinner/'), 104 | 'report': path.join(config.path.src, '/js/common/report'), 105 | 'touch': path.join(config.path.src, '/page/common/components/touch/'), 106 | 'scroll':path.join(config.path.src, '/page/common/components/scroll/'), 107 | 'immutable-pure-render-decorator': path.join(config.path.src, '/js/common/immutable-pure-render-decorator'), 108 | 'pure-render-decorator': path.join(config.path.src, '/js/common/pure-render-decorator'), 109 | } 110 | }, 111 | plugins: [ 112 | new WebpackMd5Hash(), 113 | new CopyWebpackPlugin([ 114 | { 115 | from: 'src/libs/', 116 | to: 'libs/' 117 | } 118 | ]), 119 | new webpack.optimize.OccurrenceOrderPlugin(), 120 | // make css file standalone 121 | new ExtractTextPlugin("./css/[name]" + config.chunkhash + ".css"), 122 | new webpack.NoErrorsPlugin() 123 | ], 124 | // use external react library 125 | externals: { 126 | 'react': "React", 127 | 'react-dom': "ReactDOM", 128 | }, 129 | // disable watch mode 130 | watch: false, // watch mode 131 | devtool: "#inline-source-map", 132 | }; 133 | 134 | prodConfig.addPlugins = function(plugin, opt) { 135 | prodConfig.plugins.push(new plugin(opt)); 136 | }; 137 | 138 | config.html.forEach(function(page) { 139 | prodConfig.addPlugins(HtmlResWebpackPlugin, { 140 | filename: page + ".html", 141 | template: "src/" + page + ".html", 142 | // favicon: "src/favicon.ico", 143 | jsHash: "[name]" + config.chunkhash + ".js", 144 | cssHash: "[name]" + config.chunkhash + ".css", 145 | isHotReload: false, 146 | templateContent: function(tpl) { 147 | return tpl; 148 | }, 149 | htmlMinify: { 150 | removeComments: true, 151 | collapseWhitespace: true, 152 | } 153 | }); 154 | }); 155 | 156 | // remove old pub folder 157 | prodConfig.addPlugins(Clean, ['pub']); 158 | 159 | // file compression 160 | prodConfig.addPlugins(webpack.optimize.UglifyJsPlugin, { 161 | compress: { 162 | warnings: false 163 | } 164 | }); 165 | 166 | // inject process.env.NODE_ENV so that it will recognize if (process.env.NODE_ENV === "production") 167 | prodConfig.addPlugins(webpack.DefinePlugin, { 168 | "process.env": { 169 | NODE_ENV: JSON.stringify(process.env.NODE_ENV) 170 | }, 171 | "isNode": false, 172 | "console.dev": function(msg) {} 173 | }); 174 | 175 | prodConfig.addPlugins(webpack.optimize.DedupePlugin); 176 | 177 | prodConfig.addPlugins(webpack.optimize.OccurrenceOrderPlugin, true); 178 | 179 | 180 | module.exports = prodConfig; -------------------------------------------------------------------------------- /webpack.server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | var webpack = require('webpack'); 4 | var webpackDevMiddleware = require("webpack-dev-middleware"); 5 | var webpackHotMiddleware = require("webpack-hot-middleware"); 6 | var proxy = require('proxy-middleware'); 7 | 8 | var webpackConfig = require("./webpack.config.js"), 9 | config = require("./config/config.js"); 10 | var port = config.serverPort; 11 | 12 | for (var key in webpackConfig.entry) { 13 | webpackConfig.entry[key].unshift('webpack-hot-middleware/client'); 14 | } 15 | 16 | var compiler = webpack(webpackConfig); 17 | app.use(webpackDevMiddleware(compiler, { 18 | hot: true, 19 | // historyApiFallback: false, 20 | noInfo: true, 21 | stats: { 22 | colors: true 23 | }, 24 | })); 25 | app.use(webpackHotMiddleware(compiler)); 26 | 27 | app.use(config.hostDirectory, proxy('http://localhost:' + port)); 28 | 29 | app.use('/api/', proxy('http://localhost:3001')); 30 | 31 | app.listen(port, function(err) { 32 | if (err) { 33 | console.error(err); 34 | } 35 | else { 36 | console.info("Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port); 37 | } 38 | }); --------------------------------------------------------------------------------