├── .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 | ![Login](https://github.com/L-x-C/isomorphic-react-with-mobx/blob/master/gifs/account.gif) 28 | 29 | #### Add item to list 30 | ![List](https://github.com/L-x-C/isomorphic-react-with-mobx/blob/master/gifs/list.gif) 31 | 32 | #### 404 page when routes not match 33 | ![404](https://github.com/L-x-C/isomorphic-react-with-mobx/blob/master/gifs/404.gif) 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 | 16 | 17 | 修改密码 18 | 19 | 20 | 21 |

登出

22 |
23 |
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 | 40 | 41 | 列表 42 | 43 | 44 | 某页 45 | 46 | 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 |
32 |
33 |

登录

34 | 35 |
36 |
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 |
58 | 59 | {getFieldDecorator('identity', { 60 | rules: [{required: true, message: '必填'}] 61 | })( 62 | } placeholder="请输入手机号"/> 63 | )} 64 | 65 | 66 | {getFieldDecorator('password', { 67 | rules: [{required: true, message: '必填'}] 68 | })( 69 | } placeholder="请输入密码" type="password"/> 70 | )} 71 | 72 | 73 | {getFieldDecorator('remember', { 74 | valuePropName: 'checked', 75 | initialValue: true 76 | })( 77 | 记住我,下次自动登录 78 | )} 79 | 忘记密码 80 | 81 | 82 | 83 | 84 | 85 |

还没有账号?现在注册

86 |
87 |
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 |
110 | 111 | {getFieldDecorator('name', { 112 | rules: [ 113 | {required: true, message: '必填', whitespace: true} 114 | ] 115 | })( 116 | 117 | )} 118 | 119 | 120 | {getFieldDecorator('age', {})( 121 | 122 | )} 123 | 124 | 125 | {getFieldDecorator('address', {})( 126 | 127 | )} 128 | 129 | 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 |
40 |
41 |

注册

42 | 43 |
44 |
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 |
134 | 135 | {getFieldDecorator('phone', { 136 | validateTrigger: 'onBlur', 137 | rules: [{required: true, message: '必填'}, {pattern: /^1[345789]\d{9}$/, message: '手机格式不正确'}] 138 | })( 139 | 140 | )} 141 | 142 | 143 | {getFieldDecorator('pin', { 144 | rules: [{required: true, message: '必填'}] 145 | })( 146 | 147 | )} 148 | 149 | {this.state.code_msg && this.sendCodeMsg();}}/> 150 | 151 | 152 | {this.state.code_tel && this.sendCodeMsg(true);}}/> 153 | 154 | 155 | 156 | {getFieldDecorator('password', { 157 | rules: [{required: true, message: '必填'}] 158 | })( 159 | 160 | )} 161 | 162 | 163 | 164 | 165 | 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 | 4 | 5 | Created by FontForge 20120731 at Wed Apr 19 10:32:33 2017 6 | By admin 7 | 8 | 9 | 10 | 24 | 26 | 28 | 30 | 32 | 34 | 38 | 43 | 47 | 51 | 55 | 60 | 63 | 68 | 71 | 76 | 80 | 86 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /src/styles/font/iconfont.js: -------------------------------------------------------------------------------- 1 | ;(function(window) { 2 | 3 | var svgSprite = '' + 4 | '' + 5 | '' + 6 | '' + 7 | '' + 8 | '' + 9 | '' + 10 | '' + 11 | '' + 12 | '' + 13 | '' + 14 | '' + 15 | '' + 16 | '' + 17 | '' + 18 | '' + 19 | '' + 20 | '' + 21 | '' + 22 | '' + 23 | '' + 24 | '' + 25 | '' + 26 | '' + 27 | '' + 28 | '' + 29 | '' + 30 | '' + 31 | '' + 32 | '' + 33 | '' + 34 | '' + 35 | '' + 36 | '' + 37 | '' + 38 | '' + 39 | '' + 40 | '' + 41 | '' + 42 | '' + 43 | '' + 44 | '' + 45 | '' + 46 | '' + 47 | '' + 48 | '' + 49 | '' + 50 | '' + 51 | '' + 52 | '' + 53 | '' + 54 | '' + 55 | '' + 56 | '' + 57 | '' + 58 | '' + 59 | '' + 60 | '' + 61 | '' + 62 | '' + 63 | '' + 64 | '' + 65 | '' + 66 | '' + 67 | '' + 68 | '' + 69 | '' + 70 | '' + 71 | '' + 72 | '' + 73 | '' + 74 | '' + 75 | '' + 76 | '' + 77 | '' + 78 | '' + 79 | '' + 84 | '' + 85 | '' 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) --------------------------------------------------------------------------------