├── .npmrc
├── postcss.config.js
├── bin
├── stop.sh
├── start.sh
└── build.sh
├── .gitattributes
├── src
├── states
│ ├── account.js
│ ├── job.js
│ └── menu.js
├── images
│ ├── 404.gif
│ ├── 500.jpg
│ ├── hello.gif
│ └── logo.jpg
├── styles
│ ├── font
│ │ ├── iconfont.eot
│ │ ├── iconfont.ttf
│ │ ├── iconfont.woff
│ │ ├── iconfont.css
│ │ ├── iconfont.svg
│ │ └── iconfont.js
│ ├── variables.scss
│ ├── reset.scss
│ ├── main.scss
│ ├── vender
│ │ └── nprogress.css
│ └── antd_qbt.scss
├── components
│ ├── notFoundPage
│ │ ├── notFountPage.scss
│ │ ├── Hello.js
│ │ └── NotFoundPage.js
│ └── menuHeader
│ │ ├── MenuHeader.js
│ │ └── menuHeader.scss
├── helpers
│ ├── progress.js
│ ├── mergeObservables.js
│ ├── onEnter.js
│ ├── location.js
│ └── utils.js
├── index.html
├── states.js
├── actions
│ ├── menu.js
│ ├── account.js
│ └── job.js
├── containers
│ ├── AppEnterLogin.js
│ ├── AppEnterWithoutLogin.js
│ ├── Root.js
│ ├── account
│ │ ├── account.scss
│ │ ├── Login.js
│ │ └── Signup.js
│ ├── App.js
│ └── job
│ │ ├── List.js
│ │ └── job.scss
├── routes.js
├── index.js
└── serverRender.js
├── config.json
├── gifs
├── 404.gif
├── list.gif
└── account.gif
├── public
└── favicon.ico
├── .babelrc
├── .editorconfig
├── .gitignore
├── webpack
├── loaders.js
├── plugins.js
└── webpack.config.js
├── tools
├── prodServer.js
├── build.js
└── devServer.js
├── README.md
├── .eslintrc
├── package.json
└── server
└── middlewares
└── ua.js
/.npmrc:
--------------------------------------------------------------------------------
1 | save-exact=true
2 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {};
--------------------------------------------------------------------------------
/bin/stop.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | pm2 stop all
3 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.css linguist-language=JavaScript
2 |
--------------------------------------------------------------------------------
/bin/start.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | pm2 start ./process.json
3 |
--------------------------------------------------------------------------------
/src/states/account.js:
--------------------------------------------------------------------------------
1 | export default {
2 | prefix: []
3 | };
--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "STATIC_PREFIX": "http://localhost:20000/"
3 | }
--------------------------------------------------------------------------------
/gifs/404.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/L-x-C/isomorphic-react-with-mobx/HEAD/gifs/404.gif
--------------------------------------------------------------------------------
/gifs/list.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/L-x-C/isomorphic-react-with-mobx/HEAD/gifs/list.gif
--------------------------------------------------------------------------------
/gifs/account.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/L-x-C/isomorphic-react-with-mobx/HEAD/gifs/account.gif
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/L-x-C/isomorphic-react-with-mobx/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/images/404.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/L-x-C/isomorphic-react-with-mobx/HEAD/src/images/404.gif
--------------------------------------------------------------------------------
/src/images/500.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/L-x-C/isomorphic-react-with-mobx/HEAD/src/images/500.jpg
--------------------------------------------------------------------------------
/src/images/hello.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/L-x-C/isomorphic-react-with-mobx/HEAD/src/images/hello.gif
--------------------------------------------------------------------------------
/src/images/logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/L-x-C/isomorphic-react-with-mobx/HEAD/src/images/logo.jpg
--------------------------------------------------------------------------------
/bin/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | NPM_CONFIG_REGISTRY=https://registry.npm.taobao.org npm install && npm run build
4 |
--------------------------------------------------------------------------------
/src/styles/font/iconfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/L-x-C/isomorphic-react-with-mobx/HEAD/src/styles/font/iconfont.eot
--------------------------------------------------------------------------------
/src/styles/font/iconfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/L-x-C/isomorphic-react-with-mobx/HEAD/src/styles/font/iconfont.ttf
--------------------------------------------------------------------------------
/src/styles/font/iconfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/L-x-C/isomorphic-react-with-mobx/HEAD/src/styles/font/iconfont.woff
--------------------------------------------------------------------------------
/src/states/job.js:
--------------------------------------------------------------------------------
1 | export default {
2 | //职位列表
3 | jobList: {
4 | datas: [],
5 | pagination: {
6 | current: 1,
7 | total: 0,
8 | pageSize: 20
9 | }
10 | }
11 | };
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react", "stage-1"],
3 | "plugins": [
4 | "transform-decorators-legacy",
5 | ["import", {
6 | "libraryName": "antd"
7 | }]
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/src/states/menu.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: '', //用户名
3 | access: [], //权限
4 | pathname: '',
5 | tdk: {
6 | title: 'Up',
7 | description: 'up的招聘后台',
8 | keywords: '招聘,后台,精准匹配'
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/src/styles/variables.scss:
--------------------------------------------------------------------------------
1 | $background-color: #f4f8f9;
2 | $profile-font-color: $background-color;
3 |
4 | $career-title-color: #1899d1;
5 |
6 | $career-font-size-3: #333333;
7 | $career-font-size-6: #666666;
8 | $career-font-size-9: #999999;
9 |
10 | $font-family: '';
11 |
--------------------------------------------------------------------------------
/src/components/notFoundPage/notFountPage.scss:
--------------------------------------------------------------------------------
1 | .qbt-notFound {
2 | display: flex;
3 | justify-content: center;
4 | align-items: center;
5 | position: relative;
6 | overflow: hidden;
7 | height: 85vh;
8 | background: #00ac83;
9 | &.qbt-notFound_500 {
10 | background: #fefefe;
11 | }
12 | &.qbt-notFount_hello {
13 | background: #fff;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/helpers/progress.js:
--------------------------------------------------------------------------------
1 | import Nprogress from 'nprogress';
2 | import {isClient} from './utils';
3 |
4 | export function progressStart() {
5 | if (isClient()) {
6 | Nprogress.start();
7 | }
8 | }
9 |
10 | export function progressDone() {
11 | if (isClient()) {
12 | Nprogress.done();
13 | }
14 | }
15 |
16 | export function progress() {
17 | progressStart();
18 | progressDone();
19 | }
20 |
--------------------------------------------------------------------------------
/src/styles/reset.scss:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | -webkit-tap-highlight-color: rgba(0,0,0,0);
5 | }
6 |
7 | img {
8 | border: none;
9 | vertical-align: top;
10 | &.error, &.empty {
11 | display: none;
12 | }
13 | }
14 |
15 | a {
16 | text-decoration: none
17 | }
18 |
19 |
20 | body {
21 | font-weight: 400;
22 | font-size: 12px;
23 | overflow: auto;
24 | background-color: $background-color;
25 | }
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | L-x-C
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/states.js:
--------------------------------------------------------------------------------
1 | import {observable, toJS} from 'mobx';
2 | import mergeObservables from './helpers/mergeObservables';
3 | import menuState from './states/menu';
4 | import jobState from './states/job';
5 | import accountState from './states/account';
6 |
7 | const defaultState = observable({
8 | menu: menuState,
9 | job: jobState,
10 | account: accountState
11 | });
12 |
13 | export const createServerState = () => toJS(defaultState);
14 |
15 | export const createClientState = () => mergeObservables(defaultState, window.__INITIAL_STATE__);
16 |
17 |
--------------------------------------------------------------------------------
/src/actions/menu.js:
--------------------------------------------------------------------------------
1 | import {action, toJS} from 'mobx';
2 | import fetch from 'isomorphic-fetch';
3 | import {UP_API_SERVER} from '../../config.json';
4 | import {redirect, login} from '../helpers/location';
5 |
6 | export default {
7 | @action fetchUsers: function(states, needLogin) {
8 | return new Promise((resolve) => {
9 | resolve();
10 | });
11 | },
12 |
13 | @action setTDK(states, t, d, k) {
14 | t && (states.menu.tdk.title = t);
15 | d && (states.menu.tdk.description = d);
16 | k && (states.menu.tdk.keywords = k);
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/src/containers/AppEnterLogin.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes, Component} from 'react';
2 | import {action} from 'mobx';
3 | import menuActions from '../actions/menu';
4 |
5 | export default class AppEnterLogin extends Component {
6 | @action
7 | static onEnter({states, pathname, query, params}) {
8 | return Promise.all([
9 | menuActions.fetchUsers(states, true)
10 | ]);
11 | }
12 |
13 | render() {
14 | return (
15 |
16 | {this.props.children}
17 |
18 | );
19 | }
20 | }
21 |
22 | AppEnterLogin.propTypes = {
23 | children: PropTypes.element
24 | };
25 |
--------------------------------------------------------------------------------
/src/containers/AppEnterWithoutLogin.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes, Component} from 'react';
2 | import {action} from 'mobx';
3 | import menuActions from '../actions/menu';
4 |
5 | export default class AppEnterWithoutLogin extends Component {
6 | @action
7 | static onEnter({states, pathname, query, params}) {
8 | return Promise.all([
9 | menuActions.fetchUsers(states)
10 | ]);
11 | }
12 |
13 | render() {
14 | return (
15 |
16 | {this.props.children}
17 |
18 | );
19 | }
20 | }
21 |
22 | AppEnterWithoutLogin.propTypes = {
23 | children: PropTypes.element
24 | };
25 |
--------------------------------------------------------------------------------
/src/components/notFoundPage/Hello.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import './notFountPage.scss';
3 | import imageHello from '../../images/hello.gif';
4 | import MenuHeader from '../menuHeader/MenuHeader';
5 |
6 | export default class Hello extends Component {
7 | render() {
8 | return (
9 |
10 |
11 |
12 |

13 |
14 |
15 | );
16 | }
17 | }
18 |
19 | Hello.propTypes = {
20 | type: PropTypes.string,
21 | params: PropTypes.object
22 | };
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # node-waf configuration
20 | .lock-wscript
21 |
22 | # Compiled binary addons (http://nodejs.org/api/addons.html)
23 | build/Release
24 |
25 | # Dependency directory
26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
27 | node_modules
28 |
29 | #dist folder
30 | dist
31 |
32 | #Webstorm metadata
33 | .idea
34 |
35 | # Mac files
36 | .DS_Store
--------------------------------------------------------------------------------
/src/styles/main.scss:
--------------------------------------------------------------------------------
1 | @import './vender/antd.css';
2 | @import "./vender/nprogress.css";
3 | @import './variables.scss';
4 | @import './font/iconfont.css';
5 | @import './reset.scss';
6 | @import './antd_qbt.scss';
7 |
8 | $nprogressColor: #1da0f8;
9 |
10 | #app {
11 | min-height: 100%;
12 | position: relative;
13 | }
14 |
15 | #nprogress {
16 | .bar {
17 | background: $nprogressColor;
18 | }
19 | .spinner-icon {
20 | border-top-color: $nprogressColor;
21 | border-left-color: $nprogressColor;
22 | }
23 | .peg {
24 | box-shadow: 0 0 10px $nprogressColor, 0 0 5px $nprogressColor;
25 | }
26 | }
27 |
28 | .clear {
29 | &:after {
30 | clear: both;
31 | content: '.';
32 | display: block;
33 | width: 0;
34 | height: 0;
35 | visibility: hidden;
36 | }
37 | }
--------------------------------------------------------------------------------
/src/helpers/mergeObservables.js:
--------------------------------------------------------------------------------
1 | const {isObservableArray, isObservableMap} = require('mobx');
2 | import {observable, toJS} from 'mobx';
3 |
4 | /**
5 | * Helper function that supports merging maps
6 | * @param target
7 | * @param source
8 | */
9 | function mergeObservables(target, source) {
10 | if (!source) {
11 | return target;
12 | } else {
13 | Object.keys(target).forEach(key => {
14 | if (typeof target[key] === 'object') {
15 | if (isObservableMap(target[key])) return target[key].merge(source[key]);
16 | if (isObservableArray(target[key])) return target[key].replace(source[key]);
17 | target[key] = source[key];
18 | } else {
19 | target[key] = source[key];
20 | }
21 | });
22 |
23 | return window.__INITIAL_STATE__ = target;
24 | }
25 | }
26 |
27 | module.exports = mergeObservables;
28 |
--------------------------------------------------------------------------------
/webpack/loaders.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import ExtractTextPlugin from 'extract-text-webpack-plugin';
3 |
4 | export default function getLoaders(env) {
5 | return [{
6 | test: /\.jsx?$/,
7 | use: ["babel-loader", "eslint-loader"],
8 | include: path.join(__dirname, '..', 'src'),
9 | exclude: path.join(__dirname, '../node_modules')
10 | }, {
11 | test: /\.(jpe?g|png|gif|woff|svg|eot|ttf)\??.*$/,
12 | loader: 'url-loader',
13 | options: {
14 | limit: 8192
15 | }
16 | }, {
17 | test: /(\.css|\.scss)$/,
18 | use: env === 'production' ?
19 | ExtractTextPlugin.extract({
20 | fallback: "style-loader",
21 | use: ["css-loader", "postcss-loader", "sass-loader"]
22 | }) :
23 | ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'],
24 | exclude: path.join(__dirname, '../node_modules')
25 | }];
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/notFoundPage/NotFoundPage.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { Link } from 'react-router';
3 | import './notFountPage.scss';
4 | import image404 from '../../images/404.gif';
5 | import image500 from '../../images/500.jpg';
6 |
7 | export default class NotFoundPage extends Component {
8 | getImg() {
9 | if (this.props.params.splat === '500') {
10 | return
;
11 | } else {
12 | return
;
13 | }
14 | }
15 | render() {
16 | return (
17 |
18 |
19 | {this.getImg()}
20 |
21 |
22 | );
23 | }
24 | }
25 |
26 | NotFoundPage.propTypes = {
27 | type: PropTypes.string,
28 | params: PropTypes.object
29 | };
30 |
--------------------------------------------------------------------------------
/src/containers/Root.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import {Provider} from 'mobx-react';
3 | import {Router, RouterContext} from 'react-router';
4 |
5 | export default class Root extends Component {
6 | static propTypes = {
7 | states: PropTypes.object,
8 | history: PropTypes.object,
9 | routes: PropTypes.node,
10 | type: PropTypes.string,
11 | renderProps: PropTypes.object,
12 | onUpdate: PropTypes.func
13 | };
14 |
15 | componentDidMount() {
16 | // window.__INITIAL_STATE__ = null;
17 | }
18 |
19 | render() {
20 | const {states, history, routes, type, renderProps, onUpdate} = this.props;
21 | return (
22 |
23 | {type === 'server'
24 | ?
25 | :
26 | }
27 |
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/actions/account.js:
--------------------------------------------------------------------------------
1 | import {action, toJS} from 'mobx';
2 | import {message} from 'antd';
3 | import fetch from 'isomorphic-fetch';
4 | import {progressStart, progressDone} from '../helpers/progress';
5 | import {UP_API_SERVER, WWW_SERVER} from '../../config.json';
6 | import {isClient, getFetchObj} from '../helpers/utils';
7 | import {browserHistory} from 'react-router';
8 | import qs from 'qs';
9 |
10 | export default {
11 | @action sendCodeMsg(obj) {
12 | //发送验证码信息
13 | },
14 |
15 | //创建账号
16 | @action createAccount(obj) {
17 | message.success('注册成功');
18 | browserHistory.push('/company');
19 | },
20 |
21 | //登录
22 | @action login(obj) {
23 | message.success('登录成功');
24 | let history = qs.parse(window.location.search.substring(1)).return || '/';
25 | browserHistory.push(history);
26 | },
27 |
28 | //登出
29 | @action logout() {
30 | message.success('大爷再来玩儿啊~');
31 | browserHistory.push('/');
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/src/actions/job.js:
--------------------------------------------------------------------------------
1 | import {action, toJS} from 'mobx';
2 | import {message} from 'antd';
3 | import fetch from 'isomorphic-fetch';
4 | import {progressStart, progressDone} from '../helpers/progress';
5 | import {redirect} from '../helpers/location';
6 | import {UP_API_SERVER} from '../../config.json';
7 | import qs from 'qs';
8 | import {isClient, getFetchObj} from '../helpers/utils';
9 | import {browserHistory} from 'react-router';
10 |
11 | export default {
12 | @action fetchJobList(states, obj) {
13 | states.job.jobList.datas = [{
14 | id: '1',
15 | name: '小白',
16 | age: 18,
17 | address: '地球路111号'
18 | }, {
19 | id: '2',
20 | name: '小黑',
21 | age: 22,
22 | address: '宇宙区银河路222号'
23 | }];
24 | },
25 |
26 | @action addList(states, obj) {
27 | return new Promise((resolve) => {
28 | obj.id = Math.random();
29 | states.jobList.datas.push(obj);
30 |
31 | resolve();
32 | });
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/src/helpers/onEnter.js:
--------------------------------------------------------------------------------
1 | import async from 'async';
2 |
3 | export default (renderProps, states) => {
4 | const params = renderProps.params;
5 | const query = renderProps.location.query;
6 | const pathname = renderProps.location.pathname;
7 |
8 | let onEnterArr = renderProps.components.filter(c => c.onEnter);
9 | return new Promise((resolve, reject) => {
10 | async.eachOfSeries(onEnterArr, function(c, key, callback) {
11 | let enterFn = c.onEnter({states, query, params, pathname});
12 | if (enterFn) {
13 | enterFn.then(res => {
14 | if (res) {
15 | //处理Promise回调执行,比如登陆
16 | res.forEach((fn) => {
17 | if (Object.prototype.toString.call(fn) === '[object Function]') {
18 | fn();
19 | }
20 | });
21 | }
22 |
23 | if (key === (onEnterArr.length - 1)) {
24 | resolve();
25 | }
26 |
27 | callback();
28 | }).catch(err => {
29 | reject(err);
30 | });
31 | } else {
32 | callback();
33 | }
34 | });
35 | });
36 | };
37 |
--------------------------------------------------------------------------------
/src/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Route} from 'react-router';
3 |
4 | import App from './containers/App';
5 | import AppEnterLogin from './containers/AppEnterLogin';
6 | import AppEnterWithoutLogin from './containers/AppEnterWithoutLogin';
7 |
8 | import JobList from './containers/job/List';
9 |
10 | import Login from './containers/account/Login';
11 | import Signup from './containers/account/Signup';
12 |
13 | import NotFoundPage from './components/notFoundPage/NotFoundPage';
14 | import HelloPage from './components/notFoundPage/Hello';
15 |
16 | export default function getRoutes() {
17 | return (
18 |
19 | {/*这里的AppEnter是为了在这之下的route中都做一次登录判断*/}
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/src/styles/font/iconfont.css:
--------------------------------------------------------------------------------
1 |
2 | @font-face {font-family: "iconfont";
3 | src: url('iconfont.eot?t=1492569153676'); /* IE9*/
4 | src: url('iconfont.eot?t=1492569153676#iefix') format('embedded-opentype'), /* IE6-IE8 */
5 | url('iconfont.woff?t=1492569153676') format('woff'), /* chrome, firefox */
6 | url('iconfont.ttf?t=1492569153676') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
7 | url('iconfont.svg?t=1492569153676#iconfont') format('svg'); /* iOS 4.1- */
8 | }
9 |
10 | .iconfont {
11 | font-family:"iconfont" !important;
12 | font-size:16px;
13 | font-style:normal;
14 | -webkit-font-smoothing: antialiased;
15 | -moz-osx-font-smoothing: grayscale;
16 | }
17 |
18 | .icon-weibiaoti2:before { content: "\e600"; }
19 |
20 | .icon-yaoqing:before { content: "\e6b9"; }
21 |
22 | .icon-tongyi:before { content: "\e602"; }
23 |
24 | .icon-collect:before { content: "\e603"; }
25 |
26 | .icon-uncollect:before { content: "\e604"; }
27 |
28 | .icon-tuoyuan:before { content: "\e607"; }
29 |
30 | .icon-ziyuan:before { content: "\e60a"; }
31 |
32 | .icon-xingzhuang-:before { content: "\e60b"; }
33 |
34 | .icon-xingzhuangkaobei:before { content: "\e60c"; }
35 |
36 | .icon-collect1:before { content: "\e60d"; }
37 |
38 | .icon-logo_title:before { content: "\e60e"; }
39 |
40 | .icon-logo:before { content: "\e60f"; }
41 |
42 |
--------------------------------------------------------------------------------
/tools/prodServer.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const express = require('express');
5 | const bodyParser = require('body-parser');
6 | const cookieParser = require('cookie-parser');
7 | const helmet = require('helmet');
8 | const morgan = require('morgan');
9 | const compression = require('compression');
10 | const favicon = require('serve-favicon');
11 | const ua = require('../server/middlewares/ua');
12 | const serverRender = require('../dist/serverRender');
13 | const cors = require('cors');
14 | const uri = require('urijs');
15 | const CONFIG = require('../config.json');
16 |
17 | const app = new express();
18 | // will replace qbt logging tools
19 | const logger = console;
20 |
21 | app.use(favicon(path.resolve(__dirname, '../public/favicon.ico')));
22 | app.use(morgan('combined'));
23 | app.use(cors());
24 | app.use(helmet());
25 | app.use(compression());
26 | app.use(cookieParser());
27 | app.use(bodyParser.json());
28 | app.use(bodyParser.urlencoded({
29 | extended: true
30 | }));
31 |
32 | const publicPath = uri(CONFIG.STATIC_PREFIX).path() || '/';
33 |
34 | app.use(publicPath, express.static(path.resolve(__dirname, '../dist')));
35 |
36 | app.use(ua);
37 |
38 | app.use(serverRender);
39 |
40 | const PORT = process.env.PORT || 20000;
41 | const HOST = process.env.HOST || '0.0.0.0';
42 |
43 | app.listen(PORT, HOST, () => {
44 | logger.info(`Server listening on ${HOST} port ${PORT}`);
45 | });
46 |
--------------------------------------------------------------------------------
/src/containers/account/account.scss:
--------------------------------------------------------------------------------
1 | #particles-js {
2 | width: 100%;
3 | height: 100%;
4 | .particles-js-canvas-el {
5 | max-height: 85vh;
6 | }
7 | z-index: 1;
8 | }
9 |
10 | #gee {
11 | padding: 8px 0;
12 | margin-bottom: 20px;
13 | .gt_holder {
14 | &.gt_float {
15 | width: 100%;
16 | }
17 | .gt_slider {
18 | width: 100%;
19 | }
20 | .gt_ajax_tip {
21 | right: 0;
22 | }
23 | }
24 | }
25 |
26 | .account {
27 | position: absolute;
28 | left: 50%;
29 | margin-left: -220px;
30 | z-index: 2;
31 | }
32 | .account__wrapper {
33 | width: 440px;
34 | background: #fff;
35 | border-radius: 5px;
36 | margin: 18vh auto;
37 | padding: 0 50px;
38 | overflow: hidden;
39 | box-shadow: 0 0 40px rgba(0, 0, 0, 0.08);
40 | }
41 |
42 | .account-title {
43 | text-align: center;
44 | font-size: 18px;
45 | color: #666;
46 | margin-top: 30px;
47 | }
48 |
49 | .account-form {
50 | margin-top: 20px;
51 | .account-create {
52 | margin: 0 auto;
53 | display: block;
54 | }
55 | .account-code__btn {
56 | font-size: 24px;
57 | vertical-align: -4px;
58 | margin-left: 10px;
59 | cursor: pointer;
60 | &:hover {
61 | color: #0e77ca;
62 | }
63 | }
64 | }
65 |
66 | .account-signup__phone {
67 | .ant-input-group-addon {
68 | width: 35%;
69 | }
70 | }
71 |
72 | .account-login__input_prefix {
73 | .ant-input-group-addon {
74 | width: 10%;
75 | font-size: 16px;
76 | }
77 | }
78 |
79 | .account-form__forget {
80 | float: right;
81 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {render} from 'react-dom';
3 | import {browserHistory} from 'react-router';
4 | import Root from './containers/Root';
5 | import getRoutes from './routes';
6 | import './styles/main.scss';
7 | import {createClientState} from './states';
8 | import async from 'async';
9 | import {progressStart, progressDone} from './helpers/progress';
10 |
11 | let states = createClientState();
12 |
13 | let ignoreFirstLoad = true;
14 | function onRouterUpdate() {
15 | if (ignoreFirstLoad && window.__INITIAL_STATE__) {
16 | ignoreFirstLoad = false;
17 | return;
18 | }
19 | // Page changed, executing onEnter
20 | let params = this.state.params;
21 | let query = this.state.location.query;
22 | let pathname = this.state.location.pathname.slice(1);
23 | async.eachSeries(this.state.components.filter(c => c.onEnter), function(c, callback) {
24 | let enterFn = c.onEnter({states, query, params, pathname});
25 | if (enterFn) {
26 | enterFn.then(res => {
27 | progressDone();
28 | if (res) {
29 | //处理Promise回调执行,比如登陆
30 | res.forEach((fn) => {
31 | if (Object.prototype.toString.call(fn) === '[object Function]') {
32 | fn();
33 | }
34 | });
35 | }
36 |
37 | callback();
38 | });
39 | } else {
40 | callback();
41 | }
42 | });
43 | }
44 |
45 | render(, document.getElementById('app'));
47 |
--------------------------------------------------------------------------------
/src/containers/App.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes, Component} from 'react';
2 | import MenuHeader from '../components/menuHeader/MenuHeader';
3 | import {action} from 'mobx';
4 | import {observer, inject} from 'mobx-react';
5 | import {Row, Col} from "antd";
6 | import Helmet from "react-helmet";
7 | import menuActions from '../actions/menu';
8 |
9 | @inject("menu")
10 | @observer
11 | export default class App extends Component {
12 | @action
13 | static onEnter({states, pathname, query, params}) {
14 | let refinePathname = pathname.startsWith('/') ? pathname.substring(1) : pathname; //因为客户端渲染和服务端渲染的pathname不同,会多一个/
15 | refinePathname = refinePathname.split('/')[0]; //这个项目为了能让比如/job/new,/job/1都进入一个tab,所以做了这个处理
16 | states.menu.pathname = refinePathname;
17 |
18 | return Promise.all([
19 | menuActions.setTDK(states, 'L-x-C', 'a demo', 'lol')
20 | ]);
21 | }
22 |
23 | render() {
24 | return (
25 |
26 |
31 |
32 |
33 |
34 | {this.props.children}
35 |
36 |
37 |
38 |
39 | ( ◕‿‿◕ )
40 |
41 |
42 | );
43 | }
44 | }
45 |
46 | App.propTypes = {
47 | children: PropTypes.element,
48 | menu: PropTypes.object
49 | };
50 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # isomorphic-react-with-mobx
2 | React + MobX + React-Router (CSR & SSR)
3 |
4 | ## How to use
5 |
6 | ```
7 | git clone git@github.com:L-x-C/isomorphic-react-with-mobx.git
8 | cd isomorphic-react-with-mobx
9 | npm install
10 | ```
11 |
12 | ## Dev (client-side rendering)
13 |
14 | ```
15 | npm start
16 | open http://localhost:3000
17 | ```
18 |
19 | ## Production (server-side rendering)
20 | ```
21 | npm run server
22 | open http://localhost:20000
23 | ```
24 |
25 | ## Preview
26 | #### Login, Register
27 | 
28 |
29 | #### Add item to list
30 | 
31 |
32 | #### 404 page when routes not match
33 | 
34 |
35 |
36 | ## F.A.Q
37 | ## How to fetch data on the server side?
38 |
39 | Adding a `onEnter` function to a component
40 |
41 | ```
42 | @action
43 | static onEnter({states, pathname, query, params}) {
44 | progressStart();
45 | return Promise.all([
46 | menuActions.setTDK(states, '列表'),
47 | jobActions.fetchJobList(states, query)
48 | ]);
49 | }
50 | ```
51 |
52 | ## How to redirect on the server side?
53 |
54 | In `src/helpers/location.js`, there is a `redirect` function, you can just import it and use.
55 | The `catchErr` in `src/serverRender.js` will catch the redirect command and redirect as you wish.
56 | It works on both server and client side.
57 |
58 | ```
59 | import {redirect} from './helpers/location';
60 |
61 | @action
62 | static onEnter({states, query, params}) {
63 | redirect('http://www.xxx.com');
64 | }
65 | ```
66 |
--------------------------------------------------------------------------------
/src/helpers/location.js:
--------------------------------------------------------------------------------
1 | import {CV_SERVER, UP_API_SERVER, STATIC_PREFIX} from '../../config.json';
2 | import isEmpty from 'lodash/isEmpty';
3 | import {isClient} from './utils';
4 | import {browserHistory} from 'react-router';
5 |
6 | export class RedirectException {
7 | constructor(location, options) {
8 | this.location = location;
9 | this.options = options;
10 | }
11 | }
12 |
13 | // status code is ignored when redirect from client side
14 | export function redirect(url, options) {
15 | if (isClient()) {
16 | const {back, history} = options || {};
17 | if (history) {
18 | //如果有这个字段,就直接单页面路由跳转
19 | let finalUrl = back ? appendParam(url, {return: (typeof back === 'string') ? back : window.location.pathname}) : url;
20 | browserHistory.push(finalUrl);
21 | } else {
22 | window.location.href = back ? appendParam(url, {return: (typeof back === 'string') ? back : window.location.href}) : url;
23 | }
24 | }
25 | else {
26 | if (options.history) {
27 | //如果有这个字段,说明是站内,加url前缀
28 | throw new RedirectException(STATIC_PREFIX + url, options);
29 | } else {
30 | throw new RedirectException(url, options);
31 | }
32 | }
33 | }
34 |
35 | export function appendParam(url, params) {
36 | const pairs = [];
37 | for (let name in params) {
38 | pairs.push(`${name}=${encodeURIComponent(params[name])}`);
39 | }
40 |
41 | if (isEmpty(pairs)) {
42 | return url;
43 | }
44 |
45 | let result = url;
46 | if (!url.endsWith('?')) {
47 | result += url.includes('?') ? '&' : '?';
48 | }
49 | result += pairs.join('&');
50 | return result;
51 | }
52 |
53 | export function login() {
54 | redirect(`/account/login`, {history: true, back: true});
55 | }
56 |
--------------------------------------------------------------------------------
/tools/build.js:
--------------------------------------------------------------------------------
1 | // More info on Webpack's Node API here: https://webpack.github.io/docs/node.js-api.html
2 | // Allowing console calls below since this is a build file.
3 | /*eslint-disable no-console */
4 | import webpack from 'webpack';
5 | import {getProdConfigs} from '../webpack/webpack.config';
6 | import 'colors';
7 | import { argv as args } from 'yargs';
8 | import async from 'async';
9 |
10 | const webpackConfigs = getProdConfigs();
11 |
12 | const callback = (err, stats, config) => {
13 | const inSilentMode = args.s; // set to true when -s is passed on the command
14 |
15 | if (!inSilentMode) {
16 | console.log('Generating minified bundle for production use via Webpack...'.bold.blue);
17 | }
18 |
19 | if (err) { // so a fatal error occurred. Stop here.
20 | console.log(err.bold.red);
21 |
22 | return 1;
23 | }
24 |
25 | const jsonStats = stats.toJson();
26 |
27 | if (jsonStats.hasErrors) {
28 | return jsonStats.errors.map(error => console.log(error.red));
29 | }
30 |
31 | if (jsonStats.hasWarnings && !inSilentMode) {
32 | console.log('Webpack generated the following warnings: '.bold.yellow);
33 | jsonStats.warnings.map(warning => console.log(warning.yellow));
34 | }
35 |
36 | if (!inSilentMode) {
37 | console.log(`Webpack stats: ${stats}`.green);
38 | }
39 |
40 | // if we got this far, the build succeeded.
41 | console.log(`Your app has been compiled in production mode for entry ${Object.keys(config.entry)} and written to ${config.output.path}. It's ready to roll!`.green.bold);
42 |
43 | return 0;
44 | };
45 |
46 | async.eachSeries(webpackConfigs, function(config, callback) {
47 | webpack(config).run((err, stats) => {
48 | callback(err, stats, config);
49 | });
50 | });
--------------------------------------------------------------------------------
/src/styles/vender/nprogress.css:
--------------------------------------------------------------------------------
1 | /* Make clicks pass-through */
2 | #nprogress {
3 | pointer-events: none;
4 | }
5 |
6 | #nprogress .bar {
7 | background: #29d;
8 |
9 | position: fixed;
10 | z-index: 1031;
11 | top: 0;
12 | left: 0;
13 |
14 | width: 100%;
15 | height: 2px;
16 | }
17 |
18 | /* Fancy blur effect */
19 | #nprogress .peg {
20 | display: block;
21 | position: absolute;
22 | right: 0px;
23 | width: 100px;
24 | height: 100%;
25 | box-shadow: 0 0 10px #29d, 0 0 5px #29d;
26 | opacity: 1.0;
27 |
28 | -webkit-transform: rotate(3deg) translate(0px, -4px);
29 | -ms-transform: rotate(3deg) translate(0px, -4px);
30 | transform: rotate(3deg) translate(0px, -4px);
31 | }
32 |
33 | /* Remove these to get rid of the spinner */
34 | #nprogress .spinner {
35 | display: block;
36 | position: fixed;
37 | z-index: 1031;
38 | top: 15px;
39 | right: 15px;
40 | }
41 |
42 | #nprogress .spinner-icon {
43 | width: 18px;
44 | height: 18px;
45 | box-sizing: border-box;
46 |
47 | border: solid 2px transparent;
48 | border-top-color: #29d;
49 | border-left-color: #29d;
50 | border-radius: 50%;
51 |
52 | -webkit-animation: nprogress-spinner 400ms linear infinite;
53 | animation: nprogress-spinner 400ms linear infinite;
54 | }
55 |
56 | .nprogress-custom-parent {
57 | overflow: hidden;
58 | position: relative;
59 | }
60 |
61 | .nprogress-custom-parent #nprogress .spinner,
62 | .nprogress-custom-parent #nprogress .bar {
63 | position: absolute;
64 | }
65 |
66 | @-webkit-keyframes nprogress-spinner {
67 | 0% { -webkit-transform: rotate(0deg); }
68 | 100% { -webkit-transform: rotate(360deg); }
69 | }
70 | @keyframes nprogress-spinner {
71 | 0% { transform: rotate(0deg); }
72 | 100% { transform: rotate(360deg); }
73 | }
74 |
75 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": "eslint:recommended",
4 | "plugins": [
5 | "react"
6 | ],
7 | "env": {
8 | "es6": true,
9 | "browser": true,
10 | "node": true,
11 | "jquery": true,
12 | "mocha": true
13 | },
14 | "parserOptions": {
15 | "sourceType": "module"
16 | },
17 | "rules": {
18 | "quotes": 0,
19 | "no-console": 0,
20 | "no-constant-condition": 0,
21 | "no-debugger": 1,
22 | "no-var": 1,
23 | "semi": [1, "always"],
24 | "no-trailing-spaces": 0,
25 | "eol-last": 0,
26 | "no-unused-vars": 0,
27 | "no-underscore-dangle": 0,
28 | "no-alert": 0,
29 | "no-lone-blocks": 0,
30 | "no-class-assign": 0,
31 | "jsx-quotes": 1,
32 | "react/display-name": [ 1, {"ignoreTranspilerName": false }],
33 | "react/forbid-prop-types": [1, {"forbid": ["any"]}],
34 | "react/jsx-boolean-value": 1,
35 | "react/jsx-closing-bracket-location": 0,
36 | "react/jsx-curly-spacing": 1,
37 | "react/jsx-indent-props": 0,
38 | "react/jsx-key": 1,
39 | "react/jsx-max-props-per-line": 0,
40 | "react/jsx-no-bind": 0,
41 | "react/jsx-no-duplicate-props": 1,
42 | "react/jsx-no-literals": 0,
43 | "react/jsx-no-undef": 1,
44 | "react/jsx-pascal-case": 1,
45 | "react/jsx-sort-prop-types": 0,
46 | "react/jsx-sort-props": 0,
47 | "react/jsx-uses-react": 1,
48 | "react/jsx-uses-vars": 1,
49 | "react/no-danger": 1,
50 | "react/no-did-mount-set-state": 0,
51 | "react/no-did-update-set-state": 0,
52 | "react/no-direct-mutation-state": 1,
53 | "react/no-multi-comp": 0,
54 | "react/no-set-state": 0,
55 | "react/no-unknown-property": 1,
56 | "react/prefer-es6-class": 1,
57 | "react/prop-types": 1,
58 | "react/react-in-jsx-scope": 1,
59 | "react/self-closing-comp": 1,
60 | "react/sort-comp": 1
61 | },
62 | "globals": {
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/tools/devServer.js:
--------------------------------------------------------------------------------
1 | // This file configures the development web server
2 | // which supports hot reloading and synchronized testing.
3 |
4 | // Require Browsersync along with webpack and middleware for it
5 | import browserSync from 'browser-sync';
6 | // Required for react-router browserHistory
7 | // see https://github.com/BrowserSync/browser-sync/issues/204#issuecomment-102623643
8 | import historyApiFallback from 'connect-history-api-fallback';
9 | import webpack from 'webpack';
10 | import webpackDevMiddleware from 'webpack-dev-middleware';
11 | import webpackHotMiddleware from 'webpack-hot-middleware';
12 | import {getDevConfig} from '../webpack/webpack.config';
13 |
14 | const webpackConfig = getDevConfig();
15 | const bundler = webpack(webpackConfig);
16 |
17 | const bs = browserSync.create();
18 |
19 | // Run Browsersync and use middleware for Hot Module Replacement
20 | bs.init({
21 | port: 3000,
22 | server: {
23 | baseDir: 'src'
24 | },
25 | // disable for https://github.com/ant-design/ant-design/issues/2744
26 | ghostMode: false,
27 | middleware: [
28 | webpackDevMiddleware(bundler, {
29 | // Dev middleware can't access config, so we provide publicPath
30 | publicPath: webpackConfig.output.publicPath,
31 |
32 | // pretty colored output
33 | stats: { colors: true },
34 |
35 | // Set to false to display a list of each file that is being bundled.
36 | noInfo: true
37 |
38 | // for other settings see
39 | // http://webpack.github.io/docs/webpack-dev-middleware.html
40 | }),
41 |
42 | // bundler should be the same as above
43 | webpackHotMiddleware(bundler),
44 |
45 | historyApiFallback()
46 | ],
47 |
48 | // use proxy for server-render
49 |
50 | // no need to watch '*.js' here, webpack will take care of it for us,
51 | // including full page reloads if HMR won't work
52 | files: [
53 | 'src/*.html'
54 | ]
55 | });
56 |
--------------------------------------------------------------------------------
/webpack/plugins.js:
--------------------------------------------------------------------------------
1 | import webpack from 'webpack';
2 | import path from 'path';
3 | import ExtractTextPlugin from 'extract-text-webpack-plugin';
4 | import AssetsPlugin from 'assets-webpack-plugin';
5 | import WebpackMd5Hash from 'webpack-md5-hash';
6 |
7 | export default function getPlugins(env, isServerRender) {
8 | const GLOBALS = {
9 | 'process.env.NODE_ENV': JSON.stringify(env),
10 | __DEV__: env === 'development'
11 | };
12 | const plugins = [
13 | //Tells React to build in prod mode. https://facebook.github.io/react/downloads.html
14 | new webpack.DefinePlugin(GLOBALS),
15 | new ExtractTextPlugin('/styles.[contenthash].css'),
16 | require('autoprefixer')
17 | ];
18 |
19 | if (isServerRender) {
20 | return plugins;
21 | }
22 |
23 | switch (env) {
24 | case 'production':
25 | plugins.push(new webpack.optimize.UglifyJsPlugin());
26 | plugins.push(new AssetsPlugin({
27 | path: path.join(__dirname, '..', 'dist'),
28 | filename: 'assets.json',
29 | fullPath: false
30 | }));
31 | plugins.push(new webpack.optimize.CommonsChunkPlugin({
32 | names: [ "manifest", "vendor"],
33 | minChunks: function(module, count) {
34 | // any required modules inside node_modules are extracted to vendor
35 | return (
36 | module.resource &&
37 | /\.js$/.test(module.resource) &&
38 | module.resource.indexOf(
39 | path.join(__dirname, '../node_modules')
40 | ) === 0
41 | )
42 | }
43 | }));
44 | plugins.push(new WebpackMd5Hash());
45 | plugins.push(new webpack.optimize.ModuleConcatenationPlugin());
46 |
47 | break;
48 |
49 | case 'development':
50 | plugins.push(new webpack.HotModuleReplacementPlugin());
51 | plugins.push(new webpack.NoEmitOnErrorsPlugin());
52 | break;
53 | }
54 |
55 | return plugins;
56 | }
57 |
--------------------------------------------------------------------------------
/webpack/webpack.config.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import getLoaders from './loaders';
3 | import getPlugins from './plugins';
4 | import nodeExternals from 'webpack-node-externals';
5 | import {STATIC_PREFIX} from '../config.json';
6 |
7 | function getBaseConfig(env) {
8 | return {
9 | devtool: env === 'production' ? 'source-map' : 'cheap-module-eval-source-map', // more info:https://webpack.github.io/docs/build-performance.html#sourcemaps and https://webpack.github.io/docs/configuration.html#devtool
10 | target: 'web',
11 | plugins: getPlugins(env, false),
12 | module: {
13 | rules: getLoaders(env)
14 | }
15 | };
16 | }
17 |
18 | export function getDevConfig() {
19 | return Object.assign({}, getBaseConfig('development'), {
20 | entry: {
21 | index: ['babel-polyfill', 'webpack-hot-middleware/client?reload=true', './src/index.js']
22 | },
23 | output: {
24 | path: "/", // NO real path is required, just pass "/"
25 | filename: '[name].js',
26 | publicPath: '/'
27 | }
28 | });
29 | }
30 |
31 | export function getProdConfigs() {
32 | const env = 'production';
33 | const config = Object.assign({}, getBaseConfig(env), {
34 | entry: {
35 | index: ['babel-polyfill', './src/index.js']
36 | },
37 | output: {
38 | path: path.join(__dirname, '..', '/dist'), // Note: Physical files are only output by the production build task `npm run build`.
39 | filename: '[name].[chunkhash].js',
40 | chunkFilename: "[chunkhash].js",
41 | publicPath: `${STATIC_PREFIX}/`
42 | }
43 | });
44 |
45 | return [
46 | config,
47 | Object.assign({}, config, {
48 | entry: {
49 | serverRender: ['babel-polyfill', './src/serverRender.js']
50 | },
51 | output: Object.assign({}, config.output, {
52 | filename: '[name].js',
53 | libraryTarget: 'commonjs2'
54 | }),
55 | plugins: getPlugins(env, true),
56 | target: 'node',
57 | externals: [nodeExternals()]
58 | })
59 | ];
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/menuHeader/MenuHeader.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import {Link} from 'react-router';
3 | import {observer, inject} from 'mobx-react';
4 | import {Row, Col, Menu, Button, Dropdown} from 'antd';
5 | import accountActions from '../../actions/account';
6 | import './menuHeader.scss';
7 | import {isClient} from '../../helpers/utils';
8 | import LogoPic from '../../images/logo.jpg';
9 |
10 | @inject("menu")
11 | @observer
12 | export default class MenuHeader extends Component {
13 | render() {
14 | const accountMenu = (
15 |
24 | );
25 | if (this.props.menu.pathname || this.props.hello) {
26 | return (
27 |
28 |
29 |
30 |
31 |
32 |
33 |

34 |
35 |
36 |
37 |
38 |
39 |
47 |
48 |
49 | {this.props.menu.name &&
50 |
51 |
52 | {this.props.menu.name}
53 |
54 | }
55 | {!this.props.menu.name &&
56 |
57 |
登录
58 |
注册
59 | }
60 |
61 |
62 | );
63 | } else {
64 | return null;
65 | }
66 | }
67 | }
68 |
69 | MenuHeader.propTypes = {
70 | menu: PropTypes.object,
71 | hello: PropTypes.string
72 | };
73 |
--------------------------------------------------------------------------------
/src/components/menuHeader/menuHeader.scss:
--------------------------------------------------------------------------------
1 | $headerHeight: 76px;
2 | $headerBg: #fff;
3 | .up-header {
4 | &.ant-row {
5 | //height: $headerHeight;
6 | line-height: $headerHeight;
7 | background: $headerBg;
8 | }
9 | }
10 | .up-logo {
11 | width: 40px;
12 | height: 40px;
13 | border-radius: 100%;
14 | }
15 |
16 | .up-header__content {
17 | width: 1200px;
18 | margin: 0 auto;
19 | }
20 |
21 | .up-header__title_wrapper {
22 | text-align: center;
23 | height: $headerHeight;
24 | }
25 |
26 | .up-header__name {
27 | font-size: 16px;
28 | color: #64b2f2;
29 | font-weight: 200;
30 | &:hover {
31 | color: #64b2f2;
32 | }
33 | }
34 |
35 | .up-header__title {
36 | &.iconfont {
37 | font-size: 22px;
38 | color: #283033;
39 | vertical-align: 5px;
40 | margin-left: 7px;
41 | }
42 | }
43 |
44 | .up-header__logo_wrapper {
45 | display: block;
46 | line-height: 80px;
47 | }
48 | .up-header__logo {
49 | display: inline-block;
50 | width: 40px;
51 | height: 40px;
52 | line-height: 47px;
53 | text-align: center;
54 | border-radius: 100%;
55 | color: #fff;
56 | position: relative;
57 | .icon-logo {
58 | font-size: 30px;
59 | }
60 | }
61 |
62 | .up-header__name {
63 | text-align: center;
64 | cursor: pointer;
65 | }
66 |
67 | .up-header__login_wrapper {
68 | text-align: center;
69 | color: #fff;
70 | font-size: 16px;
71 | a {
72 | color: #91a5af;
73 | margin-right: 10px;
74 | &:hover {
75 | color: #64b2f2;
76 | }
77 | }
78 | }
79 |
80 | .up-header__menu.ant-menu {
81 | background: $headerBg;
82 | border-bottom: none;
83 | .ant-menu-item {
84 | height: $headerHeight;
85 | line-height: $headerHeight + 2;
86 | margin-right: 70px;
87 | a {
88 | font-size: 16px;
89 | color: #91a5af;
90 | }
91 | &.ant-menu-item-selected {
92 | border-bottom: 2px solid #10a1f9;
93 | a {
94 | color: #10a1f9;
95 | }
96 | }
97 | &.ant-menu-item-active {
98 | border-bottom: 2px solid #10a1f9;
99 | a {
100 | color: #10a1f9;
101 | }
102 | }
103 | }
104 | }
105 |
106 | .admin-footer {
107 | height: 65px;
108 | background: #fff;
109 | color: #1eb6f8;
110 | position: absolute;
111 | bottom: 0;
112 | width: 100%;
113 | left: 0;
114 | .admin-footer__content {
115 | text-align: center;
116 | font-size: 20px;
117 | }
118 | }
119 |
120 | .menu-account.ant-dropdown-menu {
121 | width: 60%;
122 | margin: -15px auto 0;
123 | box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08);
124 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "isomorphic-react-with-mobx",
3 | "version": "0.1.0",
4 | "description": "",
5 | "scripts": {
6 | "lint:tools": "eslint webpack.config.js tools",
7 | "remove-dist": "node_modules/.bin/rimraf ./dist",
8 | "prestart": "npm run remove-dist",
9 | "start": "npm-run-all --parallel lint:tools development",
10 | "development": "babel-node tools/devServer.js",
11 | "server": "npm-run-all build production",
12 | "production": "node tools/prodServer.js",
13 | "prebuild": "npm run remove-dist && mkdir dist",
14 | "build": "babel-node tools/build.js"
15 | },
16 | "author": "20001",
17 | "dependencies": {
18 | "animejs": "2.0.2",
19 | "antd": "2.11.0",
20 | "isomorphic-fetch": "2.2.1",
21 | "load-script": "1.0.0",
22 | "lodash": "4.17.2",
23 | "mobx": "2.6.3",
24 | "mobx-react": "4.0.3",
25 | "nprogress": "0.2.0",
26 | "particles.js": "2.0.0",
27 | "querystring": "0.2.0",
28 | "react": "15.4.1",
29 | "react-dom": "15.4.1",
30 | "react-helmet": "3.1.0",
31 | "react-router": "3.0.0"
32 | },
33 | "devDependencies": {
34 | "assets-webpack-plugin": "3.5.1",
35 | "autoprefixer": "7.1.1",
36 | "babel-cli": "6.24.1",
37 | "babel-core": "6.25.0",
38 | "babel-eslint": "7.2.3",
39 | "babel-loader": "7.0.0",
40 | "babel-plugin-import": "1.2.1",
41 | "babel-plugin-react-transform": "2.0.2",
42 | "babel-plugin-transform-decorators-legacy": "1.3.4",
43 | "babel-polyfill": "6.23.0",
44 | "babel-preset-env": "1.5.2",
45 | "babel-preset-es2015": "6.24.1",
46 | "babel-preset-react": "6.24.1",
47 | "babel-preset-stage-1": "6.24.1",
48 | "body-parser": "1.17.2",
49 | "browser-sync": "2.18.12",
50 | "cheerio": "1.0.0-rc.1",
51 | "colors": "1.1.2",
52 | "compression": "1.6.2",
53 | "connect-history-api-fallback": "1.3.0",
54 | "cookie-parser": "1.4.3",
55 | "cors": "2.8.3",
56 | "css-loader": "0.28.4",
57 | "eslint": "4.0.0",
58 | "eslint-loader": "1.8.0",
59 | "eslint-plugin-react": "7.1.0",
60 | "express": "4.15.3",
61 | "extract-text-webpack-plugin": "2.1.2",
62 | "file-loader": "0.11.2",
63 | "helmet": "3.6.1",
64 | "morgan": "1.8.2",
65 | "node-sass": "4.5.3",
66 | "npm-run-all": "4.0.2",
67 | "postcss-loader": "2.0.6",
68 | "react-hot-loader": "3.0.0-beta.7",
69 | "rimraf": "2.6.1",
70 | "sass-loader": "6.0.6",
71 | "serialize-javascript": "1.3.0",
72 | "serve-favicon": "2.4.3",
73 | "style-loader": "0.18.2",
74 | "urijs": "1.18.10",
75 | "url-loader": "0.5.9",
76 | "webpack": "3.0.0",
77 | "webpack-dev-middleware": "1.10.2",
78 | "webpack-hot-middleware": "2.18.0",
79 | "webpack-md5-hash": "0.0.5",
80 | "webpack-node-externals": "1.6.0",
81 | "yargs": "8.0.2"
82 | },
83 | "repository": "git@github.com:L-x-C/isomorphic-react-with-mobx.git"
84 | }
85 |
--------------------------------------------------------------------------------
/src/helpers/utils.js:
--------------------------------------------------------------------------------
1 | import {toJS} from 'mobx';
2 | import {isNumber} from 'lodash';
3 |
4 | export function isClient() {
5 | return !!((typeof window !== 'undefined') && window.document);
6 | }
7 |
8 | export function getFetchObj(states) {
9 | return {
10 | credentials: 'include',
11 | headers: {
12 | Cookie: states.cookie
13 | }
14 | };
15 | }
16 |
17 | export function generateParticle() {
18 | require(['particles.js'], function() {
19 | let el = document.createElement('div');
20 | el.id = "particles-js";
21 | document.querySelector('.app-content').appendChild(el);
22 | window.particlesJS("particles-js", {
23 | "particles": {
24 | "number": {
25 | "value": 60,
26 | "density": {
27 | "enable": true,
28 | "value_area": 1000
29 | }
30 | },
31 | "color": {
32 | "value": ["#d1d1d1"]
33 | },
34 |
35 | "shape": {
36 | "type": "circle",
37 | "stroke": {
38 | "width": 0,
39 | "color": "#fff"
40 | },
41 | "polygon": {
42 | "nb_sides": 5
43 | }
44 | },
45 | "opacity": {
46 | "value": 1,
47 | "random": false,
48 | "anim": {
49 | "enable": false,
50 | "speed": 1,
51 | "opacity_min": 1,
52 | "sync": false
53 | }
54 | },
55 | "size": {
56 | "value": 4,
57 | "random": true,
58 | "anim": {
59 | "enable": false,
60 | "speed": 40,
61 | "size_min": 0.1,
62 | "sync": false
63 | }
64 | },
65 | "line_linked": {
66 | "enable": true,
67 | "distance": 150,
68 | "color": "#d1d1d1",
69 | "opacity": 1,
70 | "width": 1
71 | }
72 | },
73 | "interactivity": {
74 | "detect_on": "canvas",
75 | "events": {
76 | "onhover": {
77 | "enable": true,
78 | "mode": "grab"
79 | },
80 | "onclick": {
81 | "enable": false
82 | },
83 | "resize": true
84 | },
85 | "modes": {
86 | "grab": {
87 | "distance": 140,
88 | "line_linked": {
89 | "opacity": 1
90 | }
91 | },
92 | "bubble": {
93 | "distance": 400,
94 | "size": 40,
95 | "duration": 2,
96 | "opacity": 8,
97 | "speed": 3
98 | },
99 | "repulse": {
100 | "distance": 200,
101 | "duration": 0.4
102 | },
103 | "push": {
104 | "particles_nb": 4
105 | },
106 | "remove": {
107 | "particles_nb": 2
108 | }
109 | }
110 | },
111 | "retina_detect": true
112 | });
113 | });
114 |
115 | }
116 |
117 | export function removeParticle() {
118 | let el = document.querySelector('#particles-js');
119 | el.parentNode.removeChild(el);
120 | }
121 |
--------------------------------------------------------------------------------
/src/serverRender.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {renderToString} from 'react-dom/server';
3 | import serialize from 'serialize-javascript';
4 | import {match} from 'react-router';
5 | import Root from './containers/Root';
6 | import getRoutes from './routes';
7 | import ASSETS from '../dist/assets.json';
8 | import {STATIC_PREFIX} from '../config.json';
9 | import {createServerState} from './states';
10 | import onEnter from './helpers/onEnter';
11 | import {RedirectException, appendParam} from './helpers/location';
12 |
13 | function renderFullPage(renderedContent, initialState, inWechat) {
14 | return `
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | Up
23 |
24 |
25 |
26 | ${renderedContent}
27 |
30 |
31 |
32 |
33 | `;
34 | }
35 |
36 | module.exports = (req, res) => {
37 | const interceptRedirectException = (e) => {
38 | if (e instanceof RedirectException) {
39 | const {location, options} = e;
40 | const {status, back} = options || {};
41 | const url = back ? appendParam(location, {return: (typeof back === 'string') ? back : (req.protocol + '://' + req.get('host') + req.originalUrl)}) : location;
42 | if (status) {
43 | res.redirect(status, url);
44 | }
45 | else {
46 | res.redirect(url);
47 | }
48 | return true;
49 | }
50 | return false;
51 | };
52 | const catchErr = (e) => {
53 | if (!interceptRedirectException(e)) {
54 | res.status(500).send(e.message);
55 | }
56 | };
57 |
58 | const inWechat = new RegExp('MicroMessenger', 'i').test(req.headers['user-agent']);
59 | let state = createServerState();
60 | //添加cookie
61 | state['cookie'] = req.headers.cookie;
62 |
63 | match({
64 | routes: getRoutes(),
65 | location: req.originalUrl
66 | }, (error, redirectLocation, renderProps) => {
67 | if (error) {
68 | return res.status(500).send(error.message);
69 | } else if (redirectLocation) {
70 | return res.redirect(302, encodeURI(redirectLocation.pathname + redirectLocation.search));
71 | } else if (renderProps && renderProps.components) {
72 | return onEnter(renderProps, state).then(() => {
73 | const rootComp = ;
74 | res.status(200).send(renderFullPage(renderToString(rootComp), state, inWechat));
75 | }).catch(catchErr);
76 | } else {
77 | res.status(404).send('Not found');
78 | }
79 | });
80 | };
81 |
--------------------------------------------------------------------------------
/src/containers/account/Login.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes, Component} from 'react';
2 | import {action, toJS} from 'mobx';
3 | import {observer, inject} from 'mobx-react';
4 | import {Link} from 'react-router';
5 | import accountActions from '../../actions/account';
6 | import menuActions from '../../actions/menu';
7 | import {generateParticle, removeParticle} from '../../helpers/utils';
8 | import {Form, Input, Button, Icon, Checkbox} from 'antd';
9 | import './account.scss';
10 |
11 | const FormItem = Form.Item;
12 |
13 | export default class Login extends Component {
14 | @action
15 | static onEnter({states, pathname, query, params}) {
16 | return Promise.all([
17 | menuActions.setTDK(states, '登录')
18 | ]);
19 | }
20 |
21 | componentDidMount() {
22 | generateParticle();
23 | }
24 |
25 | componentWillUnmount() {
26 | removeParticle();
27 | }
28 |
29 | render() {
30 | return (
31 |
37 | );
38 | }
39 | }
40 |
41 | @inject("account")
42 | @observer
43 | class LoginForm extends React.Component {
44 | handleSubmit = (e) => {
45 | e.preventDefault();
46 | this.props.form.validateFields((err, values) => {
47 | if (!err) {
48 | accountActions.login(this.props.form.getFieldsValue());
49 | }
50 | });
51 | };
52 |
53 | render() {
54 | const {getFieldDecorator} = this.props.form;
55 |
56 | return (
57 |
88 | );
89 | }
90 | }
91 |
92 | let WrappedLoginFormForm = Form.create()(LoginForm);
93 |
94 |
95 | Login.propTypes = {
96 | job: PropTypes.object
97 | };
98 |
99 | LoginForm.propTypes = {
100 | form: PropTypes.object,
101 | account: PropTypes.object,
102 | geetestChallenge: PropTypes.string,
103 | geetestValidate: PropTypes.string,
104 | geetestSeccode: PropTypes.string
105 | };
--------------------------------------------------------------------------------
/server/middlewares/ua.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const mobileUaArry = [
4 | "iPhone", //iPhone是否也转wap?不管它,先区分出来再说。Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_1 like Mac OS X; zh-cn) AppleWebKit/532.9 (KHTML, like Gecko) Mobile/8B117
5 | "Android", //Android是否也转wap?Mozilla/5.0 (Linux; U; Android 2.1-update1; zh-cn; XT800 Build/TITA_M2_16.22.7) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17
6 | 'MicroMessenger',
7 | "Nokia", //诺基亚,有山寨机也写这个的,总还算是手机,Mozilla/5.0 (Nokia5800 XpressMusic)UC AppleWebkit(like Gecko) Safari/530
8 | "SAMSUNG", //三星手机 SAMSUNG-GT-B7722/1.0+SHP/VPP/R5+Dolfin/1.5+Nextreaming+SMM-MMS/1.2.0+profile/MIDP-2.1+configuration/CLDC-1.1
9 | "MIDP-2", //j2me2.0,Mozilla/5.0 (SymbianOS/9.3; U; Series60/3.2 NokiaE75-1 /110.48.125 Profile/MIDP-2.1 Configuration/CLDC-1.1 ) AppleWebKit/413 (KHTML, like Gecko) Safari/413
10 | "CLDC1.1", //M600/MIDP2.0/CLDC1.1/Screen-240X320
11 | "SymbianOS", //塞班系统的,
12 | "MAUI", //MTK山寨机默认ua
13 | "UNTRUSTED/1.0", //疑似山寨机的ua,基本可以确定还是手机
14 | "Windows CE", //Windows CE,Mozilla/4.0 (compatible; MSIE 6.0; Windows CE; IEMobile 7.11)
15 | //"iPad",iPad的ua,Mozilla/5.0 (iPad; U; CPU OS 3_2 like Mac OS X; zh-cn) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B367 Safari/531.21.10
16 | "BlackBerry", //BlackBerry8310/2.7.0.106-4.5.0.182
17 | "UCWEB", //ucweb是否只给wap页面? Nokia5800 XpressMusic/UCWEB7.5.0.66/50/999
18 | "ucweb", //小写的ucweb,貌似是uc的代理服务器,Mozilla/6.0 (compatible; MSIE 6.0;) Opera ucweb-squid
19 | "BREW", //很奇怪的ua,例如:REW-Applet/0x20068888 (BREW/3.1.5.20; DeviceId: 40105; Lang: zhcn) ucweb-squid
20 | "J2ME", //,很奇怪的ua,只有J2ME四个字母
21 | "YULONG", //宇龙手机,YULONG-CoolpadN68/10.14 IPANEL/2.0 CTC/1.0
22 | "YuLong", //还是宇龙
23 | "COOLPAD", //宇龙酷派,YL-COOLPADS100/08.10.S100 POLARIS/2.9 CTC/1.0
24 | "TIANYU", //天语手机,TIANYU-KTOUCH/V209/MIDP2.0/CLDC1.1/Screen-240X320
25 | "TY-", //天语,TY-F6229/701116_6215_V0230 JUPITOR/2.2 CTC/1.0
26 | "K-Touch", //还是天语,K-Touch_N2200_CMCC/TBG110022_1223_V0801 MTK/6223 Release/30.07.2008 Browser/WAP2.0
27 | "Haier", //海尔手机,Haier-HG-M217_CMCC/3.0 Release/12.1.2007 Browser/WAP2.0
28 | "DOPOD", //多普达手机,
29 | "Lenovo", //联想手机,Lenovo-P650WG/S100 LMP/LML Release/2010.02.22 Profile/MIDP2.0 Configuration/CLDC1.1
30 | "LENOVO", //联想手机,比如:LENOVO-P780/176A
31 | "HUAQIN", //华勤手机
32 | "AIGO-", //爱国者居然也出过手机,AIGO-800C/2.04 TMSS-BROWSER/1.0.0 CTC/1.0
33 | "CTC/1.0", //含义不明
34 | "CTC/2.0", //含义不明
35 | "CMCC", //移动定制手机,K-Touch_N2200_CMCC/TBG110022_1223_V0801 MTK/6223 Release/30.07.2008 Browser/WAP2.0
36 | "DAXIAN", //大显手机,DAXIAN X180 UP.Browser/6.2.3.2(GUI) MMP/2.0
37 | "MOT-", //摩托罗拉,MOT-MOTOROKRE6/1.0 LinuxOS/2.4.20 Release/8.4.2006 Browser/Opera8.00 Profile/MIDP2.0 Configuration/CLDC1.1 Software/R533_G_11.10.54R
38 | "SonyEricsson", //索爱手机,SonyEricssonP990i/R100 Mozilla/4.0 (compatible; MSIE 6.0; Symbian OS; 405) Opera 8.65 [zh-CN]
39 | "GIONEE", //金立手机
40 | "HTC", //HTC手机
41 | "ZTE", //中兴手机,ZTE-A211/P109A2V1.0.0/WAP2.0 Profile
42 | "HUAWEI", //华为手机,
43 | "webOS", //palm手机,Mozilla/5.0 (webOS/1.4.5; U; zh-CN) AppleWebKit/532.2 (KHTML, like Gecko) Version/1.0 Safari/532.2 Pre/1.0
44 | "GoBrowser", //3g GoBrowser.User-Agent=Nokia5230/GoBrowser/2.0.290 Safari
45 | "IEMobile", //Windows CE手机自带浏览器,
46 | "WAP2.0" //支持wap 2.0的
47 | ];
48 |
49 | const redirectMapping = {};
50 |
51 | module.exports = (req, res, next) => {
52 | //这是为了PC端有一个切换PC版M版的强制跳转,不根据ua
53 | let viewPort = req.cookies['VIEW_PORT'];
54 | if (viewPort && viewPort == 'M') {
55 | req.isMobile = true;
56 | } else if (viewPort && viewPort == 'PC') {
57 | req.isMobile = false;
58 | } else {
59 | const ua = req.headers['user-agent'];
60 | if (!ua) {
61 | return next();
62 | }
63 | for (let keyword of mobileUaArry) {
64 | if (ua.includes(keyword)) {
65 | req.isMobile = true;
66 | break;
67 | }
68 | }
69 | }
70 |
71 |
72 | if (!req.isMobile) {
73 | const reqPath = req.path;
74 | for (let path of Object.keys(redirectMapping)) {
75 | if (reqPath.startsWith(path)) {
76 | return res.redirect(reqPath.replace(path, redirectMapping[path]));
77 | }
78 | }
79 | }
80 | next();
81 | };
82 |
--------------------------------------------------------------------------------
/src/containers/job/List.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes, Component} from 'react';
2 | import {action, toJS} from 'mobx';
3 | import {observer, inject} from 'mobx-react';
4 | import {Link} from 'react-router';
5 | import {progressStart, progressDone} from '../../helpers/progress';
6 | import jobActions from '../../actions/job';
7 | import menuActions from '../../actions/menu';
8 | import {Table, Button, Modal, Input, InputNumber, Form} from 'antd';
9 | import {browserHistory} from 'react-router';
10 | import './job.scss';
11 |
12 | const FormItem = Form.Item;
13 |
14 | @inject("job")
15 | @observer
16 | export default class JobList extends Component {
17 | @action
18 | static onEnter({states, pathname, query, params}) {
19 | progressStart();
20 | return Promise.all([
21 | menuActions.setTDK(states, '列表'),
22 | jobActions.fetchJobList(states, query)
23 | ]);
24 | }
25 |
26 | state = {
27 | loading: false,
28 | visible: false
29 | };
30 |
31 | handleTableChange = (pagination) => {
32 | browserHistory.push(`/job?current=${pagination.current}&skip=${(pagination.current - 1) * pagination.pageSize}`);
33 | };
34 |
35 | showModal = () => {
36 | this.form.resetFields();
37 | this.setState({
38 | visible: true
39 | });
40 | };
41 |
42 | saveFormRef = (form) => {
43 | this.form = form;
44 | };
45 |
46 | handleOk = () => {
47 | this.form.validateFields((err, value) => {
48 | if (err) {
49 | return;
50 | }
51 | jobActions.addList(this.props.job, this.form.getFieldsValue()).then(res => {
52 | this.setState({
53 | visible: false
54 | });
55 | });
56 | });
57 | };
58 |
59 | handleCancel = () => {
60 | this.setState({
61 | visible: false
62 | });
63 | };
64 |
65 | render() {
66 | const columns = [{
67 | title: '姓名',
68 | dataIndex: 'name',
69 | key: 'name'
70 | }, {
71 | title: '年龄',
72 | dataIndex: 'age',
73 | key: 'age'
74 | }, {
75 | title: '地址',
76 | dataIndex: 'address',
77 | key: 'address'
78 | }];
79 |
80 | return (
81 |
82 |
83 |
84 |
87 |
88 |
94 |
95 | );
96 | }
97 | }
98 |
99 | class PopupModal extends React.Component {
100 | render() {
101 | let {visible, onCancel, onCreate, form} = this.props;
102 | let {getFieldDecorator} = form;
103 | const formItemLayout = {
104 | labelCol: {span: 5},
105 | wrapperCol: {span: 16}
106 | };
107 | return (
108 |
109 |
130 |
131 | );
132 | }
133 | }
134 |
135 | let WrappedPopupModal = Form.create()(PopupModal);
136 |
137 |
138 | JobList.propTypes = {
139 | job: PropTypes.object
140 | };
141 | PopupModal.propTypes = {
142 | form: PropTypes.object,
143 | visible: PropTypes.bool,
144 | onCancel: PropTypes.func,
145 | onCreate: PropTypes.func
146 | };
--------------------------------------------------------------------------------
/src/containers/account/Signup.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes, Component} from 'react';
2 | import {action, toJS} from 'mobx';
3 | import {observer, inject} from 'mobx-react';
4 | import accountActions from '../../actions/account';
5 | import menuActions from '../../actions/menu';
6 | import {progressStart, progressDone} from '../../helpers/progress';
7 | import {generateParticle, removeParticle} from '../../helpers/utils';
8 | import {Form, Input, Select, Button, Icon, Tooltip, message} from 'antd';
9 | import './account.scss';
10 |
11 | const Option = Select.Option;
12 | const FormItem = Form.Item;
13 |
14 | export default class Signup extends Component {
15 | @action
16 | static onEnter({states, pathname, query, params}) {
17 | progressStart();
18 | return Promise.all([
19 | menuActions.setTDK(states, '注册')
20 | ]);
21 | }
22 |
23 | state = {
24 | geetestChallenge: '',
25 | geetestValidate: '',
26 | geetestSeccode: ''
27 | };
28 |
29 | componentDidMount() {
30 | generateParticle();
31 | }
32 |
33 | componentWillUnmount() {
34 | removeParticle();
35 | }
36 |
37 | render() {
38 | return (
39 |
45 | );
46 | }
47 | }
48 |
49 | @inject("account")
50 | @observer
51 | class SignupForm extends React.Component {
52 | state = {
53 | code_msg: true, //可否发送短信验证
54 | code_msg_timer: 0,
55 | code_tel: true, //可否发送电话验证
56 | code_tel_timer: 0
57 | };
58 |
59 | handleSubmit = (e) => {
60 | this.props.form.validateFields((err, values) => {
61 | if (!err) {
62 | accountActions.createAccount({
63 | phone: this.props.form.getFieldValue('phone'),
64 | pin: this.props.form.getFieldValue('pin'),
65 | password: this.props.form.getFieldValue('password')
66 | });
67 | }
68 | });
69 | };
70 |
71 | sendCodeMsg = (isPhone) => {
72 | let form = this.props.form;
73 | form.validateFields(['phone'], (err, values) => {
74 | if (!err) {
75 | let obj = {
76 | phone: form.getFieldValue('prefix') + ' ' + form.getFieldValue('phone'),
77 | target: 'register'
78 | };
79 |
80 | if (isPhone) {
81 | obj['isVoiceMessage'] = 'true';
82 | this.setState({
83 | code_tel: false
84 | });
85 | this.startTimer('code_tel');
86 | } else {
87 | this.setState({
88 | code_msg: false
89 | });
90 | this.startTimer('code_msg');
91 | }
92 |
93 | accountActions.sendCodeMsg(obj).then(res => {
94 | if (!res.success) {
95 | if (isPhone) {
96 | this.setState({
97 | code_tel: true
98 | });
99 | } else {
100 | this.setState({
101 | code_msg: true
102 | });
103 | }
104 | }
105 | });
106 | }
107 | });
108 | };
109 |
110 | startTimer = (type) => {
111 | this.setState({
112 | [`${type}_timer`]: 60
113 | });
114 | let timer = setInterval(() => {
115 | let time = this.state[`${type}_timer`];
116 | if (time === 0) {
117 | this.setState({
118 | [type]: true
119 | });
120 | clearInterval(timer);
121 | } else {
122 | this.setState({
123 | [`${type}_timer`]: --time
124 | });
125 | }
126 | }, 1000);
127 | };
128 |
129 | render() {
130 | const {getFieldDecorator} = this.props.form;
131 |
132 | return (
133 |
166 | );
167 | }
168 | }
169 |
170 | let WrappedSignupFormForm = Form.create()(SignupForm);
171 |
172 |
173 | Signup.propTypes = {
174 | job: PropTypes.object
175 | };
176 |
177 | SignupForm.propTypes = {
178 | form: PropTypes.object,
179 | account: PropTypes.object,
180 | geetestChallenge: PropTypes.string,
181 | geetestValidate: PropTypes.string,
182 | geetestSeccode: PropTypes.string
183 | };
184 |
--------------------------------------------------------------------------------
/src/containers/job/job.scss:
--------------------------------------------------------------------------------
1 | .job-container {
2 | width: 1200px;
3 | margin: 30px auto 0;
4 | padding-bottom: 60px;
5 | overflow: hidden;
6 | &.job-list__container {
7 | width: 1000px;
8 | }
9 | }
10 | .peoplelist-container {
11 | width: 760px;
12 | margin: 30px auto 0;
13 | padding-bottom: 80px;
14 | overflow: hidden;
15 | }
16 | .job-new__container {
17 | width: 505px;
18 | float: left;
19 | &.job-new__container_new {
20 | width: 700px;
21 | float: none;
22 | margin: 0 auto;
23 | }
24 | }
25 |
26 | .job-forms {
27 | margin-bottom: 30px;
28 | .job-forms__submit {
29 | width: 320px;
30 | height: 40px;
31 | line-height: 40px;
32 | padding: 0;
33 | margin: 0 auto 20px;
34 | display: block;
35 | }
36 | .job-forms__add {
37 | margin: 20px auto 0;
38 | width: 100px;
39 | display: block;
40 | }
41 | &.ant-form {
42 | .ant-collapse-item {
43 | border-bottom: 1px solid #f5f5f5;
44 | .ant-collapse-header {
45 | height: 40px;
46 | line-height: 40px;
47 | font-size: 16px;
48 | padding-left: 34px;
49 | &:hover {
50 | background: #ecf6fd;
51 | }
52 | .arrow {
53 | &:before {
54 | content: "\E604";
55 | }
56 | }
57 | }
58 | &.ant-collapse-item-active {
59 | .ant-collapse-header {
60 | color: #11a0f8;
61 | background: #cfecfe;
62 | .arrow {
63 | color: #11a0f8;
64 | }
65 | }
66 | }
67 | }
68 | .ant-collapse-borderless > .ant-collapse-item > .ant-collapse-content {
69 | border-top: none;
70 | .ant-collapse-content-box {
71 | padding-top: 10px;
72 | }
73 | }
74 | }
75 | &.job-forms__info {
76 | .job-forms__title {
77 | font-size: 15px;
78 | color: #90a4ae;
79 | }
80 | .job-forms__field_wrapper {
81 | padding: 0;
82 | }
83 | .job-forms__field {
84 | margin-bottom: 15px;
85 | .ant-form-item-label {
86 | margin-bottom: 0;
87 | }
88 | }
89 | .job-forms__list {
90 | border-bottom: 1px dashed #e2e7eb;
91 | margin-bottom: 20px;
92 | padding-bottom: 30px;
93 | &:last-of-type {
94 | border-bottom: none;
95 | }
96 | }
97 | }
98 | }
99 | .job-forms__list {
100 | margin-bottom: 25px;
101 | .job-forms__title {
102 | font-size: 16px;
103 | color: #55595a;
104 | margin-bottom: 7px;
105 | }
106 | .job-forms__field {
107 | margin-bottom: 25px;
108 | &:last-of-type {
109 | margin-bottom: 0;
110 | }
111 | .ant-form-item-label {
112 | label {
113 | color: #404648;
114 | font-size: 13px;
115 | }
116 | text-align: left;
117 | padding: 0;
118 | margin-bottom: 5px;
119 | }
120 | &.job-forms__field_disabled {
121 | //filter: blur(5px);
122 | opacity: 0.3;
123 | }
124 | }
125 | }
126 | .job-forms__instance_wrapper {
127 | background: #fff;
128 | border-radius: 3px;
129 | }
130 | .job-forms__field_wrapper {
131 | position: relative;
132 | background: #fff;
133 | padding: 20px 23px 25px;
134 | border: 1px solid #fff;
135 | transition: all 0.3s;
136 | border-radius: 3px;
137 | }
138 | .people-new__container {
139 | margin-left: 530px;
140 | }
141 | .job-list__tab {
142 | height: 70px;
143 | background: #fff;
144 | border-radius: 5px;
145 | }
146 | .job-list__tab__content {
147 | padding: 0 28px;
148 | height: 35px;
149 | line-height: 35px;
150 | display: inline-block;
151 | margin-left: 20px;
152 | background: #cfecfe;
153 | color: #2ba6f8;
154 | text-align: center;
155 | margin-top: 18px;
156 | cursor: pointer;
157 | border-radius: 3px;
158 | transition: all 0.3s;
159 | font-size: 14px;
160 | &:hover {
161 | background: darken(#cfecfe, 5%);
162 | }
163 | &.job-list__tab__content_create {
164 | background: #11a0f8;
165 | color: #fff;
166 | &:hover {
167 | background: darken(#11a0f8, 10%);
168 | }
169 | }
170 | }
171 | .link-badge {
172 | .ant-badge-count {
173 | top: -9px;
174 | margin-left: 5px;
175 | min-width: 19px;
176 | text-align: center;
177 | font-weight: 100;
178 | }
179 | &.ant-badge {
180 | line-height: 2;
181 | }
182 | }
183 | .form-instance__delete {
184 | position: absolute;
185 | right: 33px;
186 | top: 27px;
187 | cursor: pointer;
188 | font-size: 13px;
189 | z-index: 2;
190 | &:hover {
191 | color: #04a6ff;
192 | }
193 | }
194 |
195 | .job-list__table {
196 | margin-bottom: 30px;
197 | thead {
198 | tr {
199 | th {
200 | background: #f4f8f9;
201 | }
202 | th:first-of-type {
203 | padding-left: 20px;
204 | }
205 | }
206 | }
207 | tbody {
208 | tr {
209 | td:first-of-type {
210 | padding-left: 20px;
211 | }
212 | }
213 | }
214 | }
215 | .job-list__dropdown {
216 | .ant-btn-default:first-of-type {
217 | background: #11a0f8;
218 | color: #fff;
219 | border: none;
220 | border-radius: 2px;
221 | padding: 5px 10px;
222 | }
223 | .ant-dropdown-trigger {
224 | background: #41b3f9;
225 | color: #fff;
226 | border: none;
227 | padding: 5px 15px;
228 | border-radius: 2px;
229 | &:hover, &:active, &:focus {
230 | background: #41b3f9;
231 | color: #fff;
232 | }
233 | .anticon-down:before {
234 | content: "\E606";
235 | }
236 | }
237 | }
238 | .job-list__dropdown_menu {
239 | &.ant-dropdown-menu {
240 | width: 92px;
241 | text-align: center;
242 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
243 | margin-top: -4px;
244 | .ant-dropdown-menu-item {
245 | padding: 9px 0;
246 | }
247 | .anticon {
248 | margin-right: 11px;
249 | font-size: 14px;
250 | color: #97a9b3;
251 | font-weight: bold;
252 | }
253 | }
254 | }
255 | .job-list__open_btn {
256 | &.ant-btn {
257 | background: rgba(17, 160, 248, 0.2);
258 | color: #41b3f9;
259 | border-radius: 2px;
260 | border: none;
261 | width: 93px;
262 | padding: 5px 15px;
263 | &:hover {
264 | background: rgba(17, 160, 248, 0.6);
265 | color: #fff;
266 | }
267 | }
268 | }
269 |
270 | .job-match__error {
271 | text-align: center;
272 | padding-top: 40px;
273 | transform-style: preserve-3d;
274 | }
--------------------------------------------------------------------------------
/src/styles/antd_qbt.scss:
--------------------------------------------------------------------------------
1 | $inputBgColor: #f4f8f9;
2 | $inputFontColor: #414749;
3 | .ant-input, .ant-time-picker-input {
4 | border-radius: 5px;
5 | border: none;
6 | color: $inputFontColor;
7 | background: $inputBgColor;
8 | height: 40px;
9 | &:active, &:focus {
10 | box-shadow: none;
11 | }
12 | }
13 | .ant-form-explain {
14 | color: #ff5959;
15 | margin-top: 5px;
16 | }
17 | .ant-input-number {
18 | width: 100%;
19 | border: none;
20 | border-radius: 5px;
21 | height: 40px;
22 | &.ant-input-number-focused {
23 | box-shadow: none;
24 | border-bottom: 1px solid #10a1f9;
25 | }
26 | .ant-input-number-input {
27 | border: none;
28 | color: $inputFontColor;
29 | height: 40px;
30 | background: $inputBgColor;
31 | &::-webkit-input-placeholder { /* WebKit browsers */
32 | color: #cfcfcd;
33 | }
34 | &:-moz-placeholder { /* Mozilla Firefox 4 to 18 */
35 | color: #cfcfcd;
36 | }
37 | &::-moz-placeholder { /* Mozilla Firefox 19+ */
38 | color: #cfcfcd;
39 | }
40 | &:-ms-input-placeholder { /* Internet Explorer 10+ */
41 | color: #cfcfcd;
42 | }
43 | }
44 | .ant-input-number-handler-wrap {
45 | opacity: 1;
46 | right: 5px;
47 | top: 15%;
48 | border-left: 0;
49 | height: 70%;
50 | background: $inputBgColor;
51 | }
52 | .ant-input-number-handler-up-inner {
53 | &:before {
54 | content: "\E607";
55 | font-size: 16px;
56 | }
57 | }
58 | .ant-input-number-handler-up {
59 | &:hover {
60 | .ant-input-number-handler-up-inner {
61 | margin-top: -6px;
62 | color: #000;
63 | }
64 | }
65 | }
66 | .ant-input-number-handler-down {
67 | border-top: none;
68 | &:hover {
69 | margin-top: 0;
70 | .ant-input-number-handler-down-inner {
71 | color: #000;
72 | }
73 | }
74 | .ant-input-number-handler-down-inner {
75 | &:before {
76 | content: "\E606";
77 | font-size: 16px;
78 | }
79 | }
80 | }
81 | }
82 | .ant-calendar-picker {
83 | width: 100%;
84 | .ant-calendar-range-picker-input {
85 | height: 100%;
86 | }
87 | }
88 |
89 | .ant-table {
90 | color: #000;
91 | table {
92 | border-collapse: separate;
93 | border-spacing: 0 10px;
94 | }
95 | .ant-checkbox-wrapper {
96 | border-spacing: 0;
97 | .ant-checkbox-inner {
98 | top: 1px;
99 | width: 20px;
100 | height: 20px;
101 | border: 2px solid #c4cfd4;
102 | border-radius: 5px;
103 | &:after {
104 | width: 7px;
105 | height: 12px;
106 | left: 5px;
107 | top: 0;
108 | }
109 | }
110 | .ant-checkbox-checked {
111 | .ant-checkbox-inner {
112 | border: 2px solid #27a9f8;
113 | background: #27a9f8;
114 | }
115 | }
116 | .ant-checkbox-indeterminate {
117 | .ant-checkbox-inner {
118 | border: 2px solid #27a9f8;
119 | background: #27a9f8;
120 | &:after {
121 | width: 10px;
122 | height: 0;
123 | top: 7px;
124 | left: 3px;
125 | }
126 | }
127 | }
128 | }
129 | }
130 | .ant-table-thead {
131 | span {
132 | color: #90a4ae;
133 | font-size: 12px;
134 | font-weight: normal;
135 | }
136 | }
137 | .ant-table-tbody {
138 | .ant-table-row {
139 | height: 70px;
140 | background: #fff;
141 | margin-bottom: 10px;
142 | }
143 | & > tr {
144 | box-shadow: 0 1px 4px rgba(122, 122, 122, 0.03);
145 | & > td {
146 | border-bottom: none;
147 | }
148 | }
149 | }
150 | .ant-pagination-prev, .ant-pagination-next, .ant-pagination-jump-prev, .ant-pagination-jump-next, .ant-pagination-item {
151 | min-width: 25px;
152 | height: 25px;
153 | line-height: 25px;
154 | border: 1px solid #fff;
155 | }
156 | .ant-pagination-item {
157 | &:hover {
158 | border: 1px solid #108ee9;
159 | }
160 | }
161 | .ant-pagination-item-active {
162 | background: #dde5e8;
163 | border: 1px solid #dde5e8;
164 | a {
165 | color: #5d666a;
166 | }
167 | &:hover {
168 | border: 1px solid #dde5e8;
169 | }
170 | }
171 | .ant-pagination-next a:after{
172 | content: "\E604";
173 | line-height: 24px;
174 | }
175 | .ant-pagination-prev a:after {
176 | content: "\E605";
177 | line-height: 24px;
178 | }
179 |
180 | .ant-cascader-menu-item-loading:after {
181 | content: "\E64D";
182 | animation: loadingCircle 1s infinite linear;
183 | }
184 | .ant-cascader-picker {
185 | background: $inputBgColor;
186 | .anticon-down:before {
187 | content: "\E606";
188 | }
189 | .ant-cascader-input {
190 | &:active, &:focus {
191 | box-shadow: none;
192 | }
193 | }
194 | &:focus {
195 | outline: none;
196 | }
197 | .ant-cascader-picker-label {
198 | color: $inputFontColor;
199 | }
200 | }
201 | .ant-select-selection {
202 | background: $inputBgColor;
203 | height: 40px;
204 | border: none;
205 | .ant-select-selection__rendered {
206 | height: 40px;
207 | line-height: 40px;
208 | }
209 | &:active, &:focus {
210 | box-shadow: none;
211 | }
212 | .ant-select-arrow:before {
213 | content: "\E606";
214 | }
215 | .ant-select-selection-selected-value {
216 | color: $inputFontColor;
217 | }
218 | &.ant-select-selection--multiple {
219 | height: auto;
220 | }
221 | }
222 | .ant-select-selection--multiple > ul > li, .ant-select-selection--multiple .ant-select-selection__rendered > ul > li {
223 | &.ant-select-selection__choice {
224 | height: 28px;
225 | line-height: 28px;
226 | background: #e0e7ea;
227 | margin-top: 6px;
228 | }
229 | .ant-select-search__field {
230 | height: 32px;
231 | }
232 | }
233 |
234 | .ant-select {
235 | .ant-select-selection {
236 | border: none;
237 | box-shadow: none;
238 | }
239 | }
240 | .ant-input-group-lg .ant-input, .ant-input-group-lg > .ant-input-group-addon {
241 | height: 40px;
242 | }
243 | .ant-input-group-addon {
244 | padding: 0 2px;
245 | background: $inputBgColor;
246 | border: none;
247 | }
248 | .ant-select-arrow {
249 | &:before {
250 | color: #555757;
251 | }
252 | }
253 |
254 | .ant-form-item {
255 | &.form_hidden {
256 | display: none;
257 | }
258 | &.ant-form-item-with-help {
259 | .ant-form-item-label {
260 | label {
261 | color: #ff5d5d;
262 | }
263 | }
264 | .ant-form-explain {
265 | color: #ff5d5d
266 | }
267 | }
268 | }
269 | .ant-badge-count {
270 | background: #ff0400;
271 | line-height: 19px;
272 | height: 19px;
273 | min-width: 15px;
274 | padding: 0 6px;
275 | box-shadow: none;
276 | }
277 | .ant-scroll-number-only {
278 | height: 15px;
279 | & > p {
280 | height: 15px;
281 | }
282 | }
283 | .has-error {
284 | .ant-input-group-addon {
285 | background: $inputBgColor;
286 | }
287 | }
288 |
289 | .ant-time-picker {
290 | width: 100%;
291 | }
292 |
293 | .ant-btn-primary {
294 | background: #11a0f8;
295 | &:hover {
296 | background: darken(#11a0f8, 5%);
297 | }
298 | }
299 |
300 | .ant-select-tree {
301 | .ant-select-tree-checkbox-disabled {
302 | display: none;
303 | }
304 | }
305 | .ant-card {
306 | &.qbt-card__plain {
307 | border: none;
308 | .ant-card-body {
309 | padding: 0;
310 | }
311 | &:hover {
312 | box-shadow: none;
313 | }
314 | }
315 | }
316 |
317 |
--------------------------------------------------------------------------------
/src/styles/font/iconfont.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
91 |
--------------------------------------------------------------------------------
/src/styles/font/iconfont.js:
--------------------------------------------------------------------------------
1 | ;(function(window) {
2 |
3 | var svgSprite = ''
86 | var script = function() {
87 | var scripts = document.getElementsByTagName('script')
88 | return scripts[scripts.length - 1]
89 | }()
90 | var shouldInjectCss = script.getAttribute("data-injectcss")
91 |
92 | /**
93 | * document ready
94 | */
95 | var ready = function(fn) {
96 | if (document.addEventListener) {
97 | if (~["complete", "loaded", "interactive"].indexOf(document.readyState)) {
98 | setTimeout(fn, 0)
99 | } else {
100 | var loadFn = function() {
101 | document.removeEventListener("DOMContentLoaded", loadFn, false)
102 | fn()
103 | }
104 | document.addEventListener("DOMContentLoaded", loadFn, false)
105 | }
106 | } else if (document.attachEvent) {
107 | IEContentLoaded(window, fn)
108 | }
109 |
110 | function IEContentLoaded(w, fn) {
111 | var d = w.document,
112 | done = false,
113 | // only fire once
114 | init = function() {
115 | if (!done) {
116 | done = true
117 | fn()
118 | }
119 | }
120 | // polling for no errors
121 | var polling = function() {
122 | try {
123 | // throws errors until after ondocumentready
124 | d.documentElement.doScroll('left')
125 | } catch (e) {
126 | setTimeout(polling, 50)
127 | return
128 | }
129 | // no errors, fire
130 |
131 | init()
132 | };
133 |
134 | polling()
135 | // trying to always fire before onload
136 | d.onreadystatechange = function() {
137 | if (d.readyState == 'complete') {
138 | d.onreadystatechange = null
139 | init()
140 | }
141 | }
142 | }
143 | }
144 |
145 | /**
146 | * Insert el before target
147 | *
148 | * @param {Element} el
149 | * @param {Element} target
150 | */
151 |
152 | var before = function(el, target) {
153 | target.parentNode.insertBefore(el, target)
154 | }
155 |
156 | /**
157 | * Prepend el to target
158 | *
159 | * @param {Element} el
160 | * @param {Element} target
161 | */
162 |
163 | var prepend = function(el, target) {
164 | if (target.firstChild) {
165 | before(el, target.firstChild)
166 | } else {
167 | target.appendChild(el)
168 | }
169 | }
170 |
171 | function appendSvg() {
172 | var div, svg
173 |
174 | div = document.createElement('div')
175 | div.innerHTML = svgSprite
176 | svgSprite = null
177 | svg = div.getElementsByTagName('svg')[0]
178 | if (svg) {
179 | svg.setAttribute('aria-hidden', 'true')
180 | svg.style.position = 'absolute'
181 | svg.style.width = 0
182 | svg.style.height = 0
183 | svg.style.overflow = 'hidden'
184 | prepend(svg, document.body)
185 | }
186 | }
187 |
188 | if (shouldInjectCss && !window.__iconfont__svg__cssinject__) {
189 | window.__iconfont__svg__cssinject__ = true
190 | try {
191 | document.write("");
192 | } catch (e) {
193 | console && console.log(e)
194 | }
195 | }
196 |
197 | ready(appendSvg)
198 |
199 |
200 | })(window)
--------------------------------------------------------------------------------