├── .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 | ''
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 | ''
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 | ''
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 |
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 |
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 |
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 |
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 |
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 |
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 | });
--------------------------------------------------------------------------------