├── .gitignore ├── src ├── utils │ ├── getOS.js │ ├── urlPrefix.js │ ├── routePrefix.js │ ├── getStrLength.js │ ├── getSize.js │ ├── transformDate.js │ ├── getPosition.js │ └── myFetch.js ├── components │ ├── common │ │ ├── LinkToLogin │ │ │ ├── styles.scss │ │ │ └── LinkToLogin.js │ │ ├── Header │ │ │ ├── styles.scss │ │ │ └── Header.js │ │ ├── Snackbar.js │ │ ├── CircleLoading.js │ │ ├── Profile │ │ │ ├── styles.scss │ │ │ └── Profile.js │ │ ├── AsyncContainer.js │ │ ├── Dialog.js │ │ └── react-pullrefresh.js │ ├── HomePage │ │ ├── Header │ │ │ ├── styles.scss │ │ │ └── Header.js │ │ ├── Drawer │ │ │ ├── styles.scss │ │ │ └── Drawer.js │ │ ├── FloatingActionButton.js │ │ └── Lists │ │ │ ├── styles.scss │ │ │ └── Lists.js │ ├── PublishTopic │ │ └── Form │ │ │ ├── styles.scss │ │ │ └── Form.js │ ├── Message │ │ └── Content │ │ │ ├── styles.scss │ │ │ └── Content.js │ └── Article │ │ ├── Reply │ │ ├── styles.scss │ │ └── Reply.js │ │ └── Content │ │ ├── styles.scss │ │ └── Content.js ├── styles │ ├── iconfont │ │ ├── iconfont.eot │ │ ├── iconfont.ttf │ │ ├── iconfont.woff │ │ ├── iconfont.css │ │ ├── demo_fontclass.html │ │ ├── iconfont.svg │ │ ├── demo_unicode.html │ │ ├── demo_symbol.html │ │ ├── iconfont.js │ │ └── demo.css │ └── index.css ├── actions │ ├── hashUrl.js │ └── fetchError.js ├── reducers │ ├── publishTopic.js │ ├── collectedTopics.js │ ├── hashUrl.js │ ├── index.js │ ├── message.js │ ├── profile.js │ ├── login.js │ ├── fetchError.js │ ├── article.js │ └── homePage.js ├── configureStore.js ├── index.js ├── containers │ ├── Profile.js │ ├── Message.js │ ├── App.js │ ├── PublishTopic.js │ ├── Article.js │ ├── Login.js │ └── HomePage.js ├── routes.js └── actions.js ├── stateTree └── stateTree.png ├── redBoxBlackStyle.js ├── index.html ├── .babelrc ├── README.md ├── package.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ -------------------------------------------------------------------------------- /src/utils/getOS.js: -------------------------------------------------------------------------------- 1 | export const os = 'win32';export const host = '192.168.6.183' -------------------------------------------------------------------------------- /stateTree/stateTree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumia2046/cnode/HEAD/stateTree/stateTree.png -------------------------------------------------------------------------------- /src/components/common/LinkToLogin/styles.scss: -------------------------------------------------------------------------------- 1 | .linkToLogin{ 2 | display: block; 3 | text-align: center; 4 | } -------------------------------------------------------------------------------- /src/styles/iconfont/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumia2046/cnode/HEAD/src/styles/iconfont/iconfont.eot -------------------------------------------------------------------------------- /src/styles/iconfont/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumia2046/cnode/HEAD/src/styles/iconfont/iconfont.ttf -------------------------------------------------------------------------------- /src/styles/iconfont/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumia2046/cnode/HEAD/src/styles/iconfont/iconfont.woff -------------------------------------------------------------------------------- /src/components/HomePage/Header/styles.scss: -------------------------------------------------------------------------------- 1 | .header{ 2 | position: fixed; 3 | overflow: hidden; 4 | z-index: 10; 5 | // width: 100%; 6 | transition: all 0.3s ; 7 | } 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/utils/urlPrefix.js: -------------------------------------------------------------------------------- 1 | import { os, host } from './getOS' 2 | 3 | let prefix = os == 'win32' ? `http://${host}:8081` : '/api' 4 | // let prefix = 'http://192.168.30.90:8080/s'; 5 | 6 | // let prefix = '/api' 7 | 8 | export default prefix -------------------------------------------------------------------------------- /src/components/common/Header/styles.scss: -------------------------------------------------------------------------------- 1 | .header{ 2 | // position: fixed; 3 | width: 100%; 4 | z-index: 10; 5 | transition: all 0.5s ease-out; 6 | } 7 | 8 | .title{ 9 | padding-right: 30px; 10 | text-align: center; 11 | } 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/HomePage/Drawer/styles.scss: -------------------------------------------------------------------------------- 1 | .header{ 2 | text-align: center; 3 | padding: 100px 10px 50px; 4 | background: #DCE775; 5 | p{ 6 | margin: 10px; 7 | } 8 | } 9 | 10 | .link{ 11 | vertical-align: center; 12 | display: block; 13 | border-bottom: 1px solid #DCE775; 14 | } -------------------------------------------------------------------------------- /src/components/PublishTopic/Form/styles.scss: -------------------------------------------------------------------------------- 1 | .form{ 2 | text-align: center; 3 | margin-top: 80px; 4 | } 5 | 6 | .errorInfo{ 7 | color: red; 8 | margin: 5px; 9 | overflow: hidden; 10 | transition: all 0.3s; 11 | } 12 | 13 | .content{ 14 | margin: 10px; 15 | } 16 | .title{ 17 | position: relative; 18 | top: -20px; 19 | /*vertical-align: top;*/ 20 | } -------------------------------------------------------------------------------- /src/components/Message/Content/styles.scss: -------------------------------------------------------------------------------- 1 | .content{ 2 | margin-top: 64px; 3 | overflow: hidden; 4 | } 5 | 6 | .link{ 7 | display: block; 8 | } 9 | .msg{ 10 | padding: 20px; 11 | text-align: center; 12 | font-size: 24px; 13 | } 14 | 15 | .oneline{ 16 | font-weight: bold; 17 | overflow: hidden; 18 | text-overflow: ellipsis; 19 | white-space: nowrap; 20 | } -------------------------------------------------------------------------------- /src/utils/routePrefix.js: -------------------------------------------------------------------------------- 1 | // 使用browserHistory需要进行判断,在生产环境下,如果编译的文件不是根目录文件,而是在子文件夹内,子文件夹的地址部分会被browserHistory解析成路由 2 | // 比如:编译的文件放在站点www文件夹的cnode文件夹里,访问时用github.com/cnode/,但是此时cnode/被browserHistory解析成路由,这个路由不存在所以会出问题 3 | // let prefix = process.env.NODE_ENV === 'production' ? '/cnode' : ''; 4 | 5 | // 使用hashHistory则不需要这样的判断,因为router被放在hash中,而浏览器能正确解析hash 6 | let prefix = ''; 7 | export default prefix; -------------------------------------------------------------------------------- /src/actions/hashUrl.js: -------------------------------------------------------------------------------- 1 | export const SET_TRANSITION = 'SET_TRANSITION' 2 | export const SET_HASH_URL = 'SET_HASH_URL' 3 | 4 | 5 | export const setHashUrl = (hashUrl) => { 6 | return { 7 | type:SET_HASH_URL, 8 | data:hashUrl 9 | } 10 | } 11 | 12 | export const setTransition = (transiton) => { 13 | return { 14 | type:SET_TRANSITION, 15 | data:transiton 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/utils/getStrLength.js: -------------------------------------------------------------------------------- 1 | // GBK字符集实际长度计算 2 | const getStrLength = str => { 3 | let realLength = 0; 4 | let len = str.length; 5 | let charCode = -1; 6 | for(let i = 0; i < len; i++){ 7 | charCode = str.charCodeAt(i); 8 | if (charCode >= 0 && charCode <= 128) { 9 | realLength += 1; 10 | }else{ 11 | // 如果是中文则长度加2 12 | realLength += 2; 13 | } 14 | } 15 | return realLength; 16 | } 17 | 18 | export default getStrLength -------------------------------------------------------------------------------- /src/reducers/publishTopic.js: -------------------------------------------------------------------------------- 1 | import { 2 | PUBLISH_TOPIC 3 | } from '../actions' 4 | 5 | const initState = sessionStorage.getItem('store') ? JSON.parse(sessionStorage.getItem('store')).publishTopic : { 6 | success: false 7 | } 8 | 9 | 10 | const publishTopic = (state = initState, action) => { 11 | switch (action.type) { 12 | case PUBLISH_TOPIC: 13 | return { ...state, success: action.success, topicId: action.topicId } 14 | default: 15 | return state 16 | } 17 | } 18 | 19 | export default publishTopic 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/reducers/collectedTopics.js: -------------------------------------------------------------------------------- 1 | import { 2 | GET_COLLECTED_TOPICS 3 | } from '../actions' 4 | 5 | const initState = sessionStorage.getItem('store') ? JSON.parse(sessionStorage.getItem('store')).collectedTopics : { 6 | success: false 7 | } 8 | 9 | 10 | const collectedTopics = (state = initState, action) => { 11 | switch (action.type) { 12 | case GET_COLLECTED_TOPICS: 13 | return { ...state, success: action.success, data: action.data, userName: action.userName } 14 | default: 15 | return state 16 | } 17 | } 18 | 19 | export default collectedTopics -------------------------------------------------------------------------------- /src/reducers/hashUrl.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_HASH_URL, SET_TRANSITION 3 | } from '../actions/hashUrl' 4 | 5 | const initState = sessionStorage.getItem('store') ? JSON.parse(sessionStorage.getItem('store')).hashUrl : { 6 | oldUrl: '/', currentUrl: '/', transition: 'none' 7 | } 8 | 9 | const hashUrl = (state = initState, action) => { 10 | switch (action.type) { 11 | case SET_HASH_URL: 12 | case SET_TRANSITION: 13 | return { ...state, ...action.data } 14 | default: 15 | return state 16 | } 17 | 18 | } 19 | 20 | export default hashUrl; -------------------------------------------------------------------------------- /src/actions/fetchError.js: -------------------------------------------------------------------------------- 1 | import urlPrefix from '../utils/urlPrefix' 2 | 3 | export const FETCH_ERROR = 'FETCH_ERROR' 4 | export const CLEAR_ERROR = 'CLEAR_ERROR' 5 | export const FETCH_START = 'FETCH_START' 6 | export const FETCH_END = 'FETCH_END' 7 | 8 | export const fetchStart = () => ({ 9 | type: FETCH_START 10 | }) 11 | 12 | export const fetchEnd = () => ({ 13 | type: FETCH_END 14 | }) 15 | 16 | export const fetchError = e => ({ 17 | type: FETCH_ERROR, 18 | data: e 19 | }) 20 | 21 | export const clearError = () => ({ 22 | type: CLEAR_ERROR 23 | }) -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import homePage from './homePage' 3 | import article from './article' 4 | import login from './login' 5 | import profile from './profile' 6 | import message from './message' 7 | import publishTopic from './publishTopic' 8 | import hashUrl from './hashUrl' 9 | import fetchError from './fetchError' 10 | 11 | const rootReducer = combineReducers({ 12 | homePage, 13 | article, 14 | login, 15 | profile, 16 | publishTopic, 17 | message, 18 | hashUrl, 19 | fetchError 20 | }) 21 | 22 | export default rootReducer -------------------------------------------------------------------------------- /src/components/common/Snackbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Snackbar from 'material-ui/Snackbar'; 3 | import RaisedButton from 'material-ui/RaisedButton'; 4 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 5 | 6 | const SnackbarExample = (props) => { 7 | 8 | return ( 9 | 10 | 16 | 17 | ); 18 | } 19 | 20 | export default SnackbarExample -------------------------------------------------------------------------------- /redBoxBlackStyle.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | style: { 3 | redbox: { 4 | boxSizing: 'border-box', 5 | fontFamily: 'sans-serif', 6 | position: 'fixed', 7 | padding: 10, 8 | top: '0px', 9 | left: '0px', 10 | bottom: '0px', 11 | right: '0px', 12 | width: '100%', 13 | background: 'rgba(0, 0, 0, 0.75)', 14 | color: 'red', 15 | zIndex: 9999, 16 | textAlign: 'left', 17 | fontSize: '16px', 18 | lineHeight: 1.2 19 | } 20 | } 21 | }; -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Cnode 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/components/common/CircleLoading.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' 3 | import CircularProgress from 'material-ui/CircularProgress' 4 | import getSize from '../../utils/getSize' 5 | 6 | const { windowH, windowW } = getSize() 7 | const style = { 8 | position:'relative', 9 | paddingTop: 0.5 * windowH - 40, 10 | textAlign: 'center' 11 | 12 | } 13 | const CircleLoading = () => ( 14 |
15 | 16 | 17 | 18 |
19 | 20 | ); 21 | 22 | export default CircleLoading; -------------------------------------------------------------------------------- /src/utils/getSize.js: -------------------------------------------------------------------------------- 1 | const getSize = () => { 2 | let windowW,windowH,contentH,contentW,scrollT; 3 | windowH = window.innerHeight; 4 | windowW = window.innerWidth; 5 | scrollT = document.documentElement.scrollTop || document.body.scrollTop; 6 | contentH = (document.documentElement.scrollHeight > document.body.scrollHeight) ? document.documentElement.scrollHeight : document.body.scrollHeight; 7 | contentW = (document.documentElement.scrollWidth > document.body.scrollWidth) ? document.documentElement.scrollWidth : document.body.scrollWidth; 8 | return {windowW,windowH,contentH,contentW,scrollT} 9 | } 10 | 11 | export default getSize; -------------------------------------------------------------------------------- /src/reducers/message.js: -------------------------------------------------------------------------------- 1 | import { 2 | FETCH_MESSAGE, MARK_ALL_MESSAGES 3 | } from '../actions' 4 | 5 | const initState = sessionStorage.getItem('store') ? JSON.parse(sessionStorage.getItem('store')).message : { 6 | isMarked: false, hasReadMessage: [], hasNotReadMessage: [] 7 | } 8 | 9 | const message = (state = initState, action) => { 10 | switch (action.type) { 11 | case FETCH_MESSAGE: 12 | return { ...state, hasReadMessage: action.hasReadMessage, hasNotReadMessage: action.hasNotReadMessage } 13 | case MARK_ALL_MESSAGES: 14 | return { ...state, isMarked: action.isMarked } 15 | default: 16 | return state 17 | } 18 | } 19 | 20 | export default message -------------------------------------------------------------------------------- /src/reducers/profile.js: -------------------------------------------------------------------------------- 1 | import { 2 | REQUEST_PROFILE, RECEIVE_PROFILE, GET_COLLECTED_TOPICS 3 | } from '../actions' 4 | 5 | const initState = sessionStorage.getItem('store') ? JSON.parse(sessionStorage.getItem('store')).profile : { 6 | isFetching: false, collectedTopics: [] 7 | } 8 | 9 | const profile = (state = initState, action) => { 10 | switch (action.type) { 11 | case REQUEST_PROFILE: 12 | return { ...state, isFetching: true } 13 | case RECEIVE_PROFILE: 14 | return { ...state, ...action.profile, isFetching: false } 15 | case GET_COLLECTED_TOPICS: 16 | return { ...state, collectedTopics: action.data } 17 | default: 18 | return state 19 | } 20 | } 21 | 22 | export default profile -------------------------------------------------------------------------------- /src/reducers/login.js: -------------------------------------------------------------------------------- 1 | import { 2 | LOGIN_SUCCESS, LOGIN_FAILED, LOGOUT 3 | } from '../actions' 4 | 5 | 6 | const initState = sessionStorage.getItem('store') ? JSON.parse(sessionStorage.getItem('store')).login : { 7 | succeed: false 8 | } 9 | 10 | const login = (state = initState, action) => { 11 | switch (action.type) { 12 | case LOGIN_SUCCESS: 13 | return { ...state, succeed: true, loginName: action.loginName, loginId: action.loginId, accessToken: action.accessToken } 14 | case LOGIN_FAILED: 15 | return { ...state, succeed: false, failedMessage: action.failedMessage } 16 | case LOGOUT: 17 | return { succeed: false } 18 | default: 19 | return state 20 | } 21 | } 22 | 23 | export default login -------------------------------------------------------------------------------- /src/components/Article/Reply/styles.scss: -------------------------------------------------------------------------------- 1 | .reply{ 2 | li{ 3 | padding: 10px 20px; 4 | overflow: hidden; 5 | border-bottom: 1px solid #ccc; 6 | } 7 | h2{ 8 | padding: 10px 20px; 9 | font-size: 18px; 10 | background: #ccc; 11 | } 12 | img{ 13 | width: 50px; 14 | height: 50px; 15 | border-radius: 50%; 16 | } 17 | 18 | b{ 19 | display: block; 20 | text-align: right; 21 | } 22 | } 23 | 24 | .author{ 25 | float: left; 26 | text-align: center; 27 | padding-top: 10px; 28 | } 29 | 30 | .main{ 31 | padding: 0 0 0 60px; 32 | } 33 | 34 | .item{ 35 | padding: 5px; 36 | overflow: hidden; 37 | } 38 | 39 | .form{ 40 | padding: 20px; 41 | } 42 | 43 | .textarea{ 44 | transition: all 0.3s ease-out; 45 | } -------------------------------------------------------------------------------- /src/reducers/fetchError.js: -------------------------------------------------------------------------------- 1 | import { 2 | FETCH_START, FETCH_END, FETCH_ERROR, CLEAR_ERROR 3 | } from '../actions/fetchError' 4 | 5 | const initState = sessionStorage.getItem('store') ? JSON.parse(sessionStorage.getItem('store')).fetchError : { 6 | error: null, fetched: null 7 | } 8 | 9 | const fetchError = (state = initState, action) => { 10 | switch (action.type) { 11 | case FETCH_START: 12 | return { ...state, fetched: 'start' } 13 | case FETCH_END: 14 | return { ...state, fetched: 'end' } 15 | case FETCH_ERROR: 16 | return { ...state, error: action.data, fetched: 'failed' } 17 | case CLEAR_ERROR: 18 | return { ...state, error: null } 19 | default: 20 | return state 21 | } 22 | 23 | } 24 | 25 | export default fetchError -------------------------------------------------------------------------------- /src/utils/transformDate.js: -------------------------------------------------------------------------------- 1 | export default function (date) { 2 | var createAt = new Date(date); 3 | var time = new Date().getTime() - createAt.getTime(); //现在的时间-传入的时间 = 相差的时间(单位 = 毫秒) 4 | if (time < 0) { 5 | return ''; 6 | } else if (time / 1000 < 60) { 7 | return '刚刚'; 8 | } else if ((time / 60000) < 60) { 9 | return parseInt((time / 60000)) + '分钟前'; 10 | } else if ((time / 3600000) < 24) { 11 | return parseInt(time / 3600000) + '小时前'; 12 | } else if ((time / 86400000) < 31) { 13 | return parseInt(time / 86400000) + '天前'; 14 | } else if ((time / 2592000000) < 12) { 15 | return parseInt(time / 2592000000) + '月前'; 16 | } else { 17 | return parseInt(time / 31536000000) + '年前'; 18 | } 19 | } -------------------------------------------------------------------------------- /src/components/Article/Content/styles.scss: -------------------------------------------------------------------------------- 1 | .head{ 2 | margin-top: 64px; 3 | background: #ccc; 4 | overflow: hidden; 5 | padding: 10px; 6 | img{ 7 | width: 50px; 8 | height: 50px; 9 | border-radius: 50px; 10 | } 11 | } 12 | 13 | .imgbox{ 14 | float: left; 15 | text-align: center; 16 | margin-right: 10px; 17 | } 18 | .info{ 19 | height: 25px; 20 | line-height: 25px; 21 | } 22 | .title{ 23 | text-align: center; 24 | font-weight: bold; 25 | font-size: 24px; 26 | line-height: 32px; 27 | } 28 | .main{ 29 | padding: 20px; 30 | p{ 31 | text-indent: 2em; 32 | } 33 | img{ 34 | display: block; 35 | margin: 20px auto; 36 | width: 600px; 37 | } 38 | } 39 | 40 | @media (max-width: 750px){ 41 | .main{ 42 | img{ 43 | width: 80%; 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import { composeWithDevTools } from 'redux-devtools-extension'; 3 | import thunk from 'redux-thunk'; 4 | import { createLogger } from 'redux-logger'; 5 | import { ConnectedRouter, routerReducer, routerMiddleware, push } from 'react-router-redux'; 6 | import createHistory from 'history/createBrowserHistory'; 7 | import rootReducer from './reducers/index' 8 | 9 | const history = createHistory(); 10 | const middleware = routerMiddleware(history) 11 | const logger = createLogger({ collapsed: true }) 12 | const middlewares = [thunk, middleware] 13 | 14 | if(process.env.NODE_ENV === 'development') { 15 | middlewares.push(logger) 16 | } 17 | const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(...middlewares))) 18 | 19 | export default store 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/components/common/Profile/styles.scss: -------------------------------------------------------------------------------- 1 | .header{ 2 | text-align: center; 3 | padding: 0 20px 20px; 4 | img{ 5 | width: 50px; 6 | height: 50px; 7 | border-radius: 50%; 8 | } 9 | p{ 10 | margin: 10px; 11 | } 12 | } 13 | 14 | .boxs{ 15 | padding: 20px; 16 | overflow: hidden; 17 | } 18 | 19 | .box{ 20 | float: left; 21 | margin: 1.6%; 22 | width: 30%; 23 | /*padding: 0 20px 20px;*/ 24 | box-shadow: 0 0 5px grey; 25 | background: white; 26 | h2{ 27 | padding: 10px; 28 | } 29 | } 30 | .title{ 31 | font-weight:bold; 32 | padding: 5px 10px; 33 | border-bottom: 1px solid grey; 34 | text-align: left; 35 | overflow: hidden; 36 | text-overflow:ellipsis; 37 | white-space: nowrap; 38 | } 39 | .link{ 40 | display: block; 41 | } 42 | @media only screen and (max-width: 768px) { 43 | .box { 44 | margin: 5%; 45 | width: 90%; 46 | } 47 | } -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | //babel配置文件,不需要做修改,因为都配置好了 2 | { 3 | "presets": [ 4 | ["env", {"modules": false}], 5 | "react", 6 | "stage-0" 7 | ], 8 | "plugins": [ 9 | 10 | "react-hot-loader/babel", 11 | 12 | ["transform-runtime", { 13 | "helpers": false, 14 | "polyfill": false, 15 | "regenerator": true, 16 | "moduleName": "babel-runtime" 17 | }], 18 | // ['import', {libraryName: 'antd', style:true}], 19 | "transform-decorators-legacy", 20 | "transform-async-to-generator", 21 | "transform-do-expressions", 22 | "syntax-do-expressions", 23 | ["babel-plugin-react-transform", { 24 | transforms: [ 25 | { 26 | transform: 'react-transform-catch-errors', 27 | imports: ['react','redbox-react','redBoxBlackStyle'] 28 | } 29 | ], 30 | }] 31 | ] 32 | } -------------------------------------------------------------------------------- /src/components/common/AsyncContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import CircleLoading from './CircleLoading' 3 | 4 | class AsyncContainer extends Component { 5 | state = { mounted: false } 6 | 7 | componentDidMount() { 8 | this.timeOut = setTimeout(() => { 9 | this.setState({ mounted: true }) 10 | }, this.props.delay || 500) 11 | } 12 | 13 | componentWillUnmount() { 14 | clearTimeout(this.timeOut) 15 | } 16 | 17 | render() { 18 | return ( 19 |
20 |
21 | 22 |
23 | {this.state.mounted && this.props.children} 24 |
25 | ) 26 | } 27 | componentWillUnmount() { 28 | clearTimeout(this.timeOut) 29 | } 30 | } 31 | 32 | export default AsyncContainer -------------------------------------------------------------------------------- /src/styles/iconfont/iconfont.css: -------------------------------------------------------------------------------- 1 | 2 | @font-face {font-family: "iconfont"; 3 | src: url('iconfont.eot?t=1481708482838'); /* IE9*/ 4 | src: url('iconfont.eot?t=1481708482838#iefix') format('embedded-opentype'), /* IE6-IE8 */ 5 | url('iconfont.woff?t=1481708482838') format('woff'), /* chrome, firefox */ 6 | url('iconfont.ttf?t=1481708482838') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ 7 | url('iconfont.svg?t=1481708482838#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-xiai:before { content: "\e600"; } 19 | 20 | .icon-ding:before { content: "\e610"; } 21 | 22 | .icon-user:before { content: "\e60f"; } 23 | 24 | .icon-back:before { content: "\e611"; } 25 | 26 | .icon-informatiom:before { content: "\e617"; } 27 | 28 | .icon-huifu:before { content: "\e63f"; } 29 | 30 | .icon-chakanguo:before { content: "\e6ae"; } 31 | 32 | -------------------------------------------------------------------------------- /src/components/HomePage/FloatingActionButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { setTransition } from '../../actions/hashUrl' 4 | import FloatingActionButton from 'material-ui/FloatingActionButton' 5 | import ContentAdd from 'material-ui/svg-icons/content/add' 6 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' 7 | import { Link } from 'react-router-dom' 8 | import prefix from '../../utils/routePrefix' 9 | 10 | const style = { 11 | position: 'fixed', 12 | bottom: 50, 13 | right: 50 14 | } 15 | 16 | @connect() 17 | class FloatActionButton extends React.Component { 18 | render() { 19 | return ( 20 | this.props.dispatch(setTransition({ transition: 'up' }))}> 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | } 29 | 30 | } 31 | 32 | 33 | export default FloatActionButton -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom' 4 | import store from './configureStore' 5 | import { Provider } from 'react-redux' 6 | // import { Router,hashHistory } from 'react-router' 7 | import Routes from './Routes' 8 | import './styles/index.css' 9 | import { AppContainer } from 'react-hot-loader' 10 | 11 | import injectTapEventPlugin from 'react-tap-event-plugin' 12 | injectTapEventPlugin() 13 | 14 | 15 | const render = (Component) => { 16 | ReactDOM.render( 17 | 18 | 19 | 20 | 21 | , 22 | document.getElementById('root') 23 | ) 24 | } 25 | 26 | render(Routes) 27 | 28 | if(module.hot) { 29 | module.hot.accept('./Routes', () => { render(Routes) }); 30 | } 31 | 32 | // ReactDOM.render( 33 | // 34 | // 35 | //
aaaaaaaa
36 | //
37 | //
, 38 | // document.getElementById('root') 39 | // ) 40 | -------------------------------------------------------------------------------- /src/containers/Profile.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import {fetchArticle} from '../actions' 4 | import Header from '../components/common/Header/Header' 5 | import ProfileComponent from '../components/common/Profile/Profile' 6 | import getSize from '../utils/getSize' 7 | 8 | 9 | class Profile extends Component { 10 | render(){ 11 | const {profile} = this.props 12 | return ( 13 |
14 |
15 |
16 | {profile.loginname && 17 |
18 | 19 |
20 | 21 | } 22 |
23 |
24 | ) 25 | } 26 | } 27 | 28 | // HomePage.propTypes = { 29 | // selectedTab: PropTypes.string.isRequired, 30 | // topics: PropTypes.array.isRequired, 31 | // isFetching: PropTypes.bool.isRequired, 32 | // page:PropTypes.number.isRequired, 33 | // scrollT:PropTypes.number.isRequired, 34 | // dispatch: PropTypes.func.isRequired 35 | // } 36 | 37 | function mapStateToProps(state) { 38 | const {profile,article} = state; 39 | const {collectedTopics} = profile; 40 | return {profile,article,collectedTopics} 41 | } 42 | 43 | 44 | export default connect(mapStateToProps)(Profile) -------------------------------------------------------------------------------- /src/containers/Message.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import {fetchMessage,fetchArticle} from '../actions' 4 | import Content from '../components/Message/Content/Content' 5 | import LinkToLogin from '../components/common/LinkToLogin/LinkToLogin' 6 | import Header from '../components/common/Header/Header' 7 | import getSize from '../utils/getSize' 8 | 9 | 10 | class Message extends Component { 11 | 12 | componentDidMount(){ 13 | const {login,dispatch,message} = this.props 14 | if(login.accessToken && message.hasReadMessage.length === 0){ 15 | dispatch(fetchMessage(login.accessToken)) 16 | } 17 | } 18 | render(){ 19 | const {dispatch,currentRouter,message,article,login} = this.props; 20 | return ( 21 |
22 |
23 |
24 |
25 | {login.succeed && } 26 | {!login.succeed && } 27 |
28 |
29 |
30 | ) 31 | } 32 | } 33 | 34 | 35 | 36 | function mapStateToProps(state) { 37 | const {login,message,article} = state; 38 | return {login,message,article} 39 | } 40 | 41 | export default connect(mapStateToProps)(Message) -------------------------------------------------------------------------------- /src/components/common/Dialog.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // import { hashHistory,browserHistory } from 'react-router'; 3 | import Dialog from 'material-ui/Dialog'; 4 | import FlatButton from 'material-ui/FlatButton'; 5 | import RaisedButton from 'material-ui/RaisedButton'; 6 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 7 | 8 | const DialogExample = (props) => { 9 | const handleClose = () => { 10 | props.close() 11 | }; 12 | const handleJump = () => { 13 | props.close() 14 | if(props.link){ 15 | // hashHistory.push(props.link) 16 | // browserHistory.push(props.link) 17 | // pros.location = props.link 18 | alert('a') 19 | } 20 | if(props.action){ 21 | props.action() 22 | } 23 | 24 | } 25 | const actions = props.singleButton ? [] : 26 | [, 27 | ]; 28 | return ( 29 | 30 | 36 | {props.children} 37 | 38 | 39 | ); 40 | } 41 | 42 | export default DialogExample -------------------------------------------------------------------------------- /src/reducers/article.js: -------------------------------------------------------------------------------- 1 | import { 2 | REQUEST_ARTICLE, RECEIVE_ARTICLE, CHANGE_CURRENT_TOPICID, SWITCH_SUPPORT, FETCH_COMMENT, RECORD_ARTICLE_SCROLLT 3 | } from '../actions' 4 | 5 | const initState = sessionStorage.getItem('store') ? JSON.parse(sessionStorage.getItem('store')).article : { 6 | currentTopicId: '' 7 | } 8 | 9 | const article = (state = initState, action) => { 10 | let stateItem = state[action.topicId] || {} 11 | switch (action.type) { 12 | case CHANGE_CURRENT_TOPICID: 13 | return { ...state, currentTopicId: action.topicId } 14 | case SWITCH_SUPPORT: 15 | return { ...state, switchSupportInfo: { replyId: action.replyId, index: action.index, success: action.success, action: action.action } } 16 | case FETCH_COMMENT: 17 | return { ...state, isCommented: action.success } 18 | case RECORD_ARTICLE_SCROLLT: 19 | stateItem = { ...stateItem, scrollT: action.scrollT } 20 | return { ...state, [action.topicId]: stateItem, currentTopicId: action.topicId } 21 | case REQUEST_ARTICLE: 22 | stateItem = { ...stateItem, isFetching: true } 23 | return { ...state, [action.topicId]: stateItem, currentTopicId: action.topicId, isCommented: false } 24 | case RECEIVE_ARTICLE: 25 | stateItem = { ...stateItem, isFetching: false, article: action.article } 26 | return { ...state, [action.topicId]: stateItem } 27 | default: 28 | return state 29 | } 30 | } 31 | 32 | export default article -------------------------------------------------------------------------------- /src/components/common/LinkToLogin/LinkToLogin.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { Link } from 'react-router-dom' 3 | import { setTransition } from '../../../actions/hashUrl' 4 | import prefix from '../../../utils/routePrefix' 5 | import classnames from 'classnames' 6 | import { setCurrentRouter } from '../../../actions' 7 | import styles from './styles.scss' 8 | import FlatButton from 'material-ui/FlatButton'; 9 | import RefreshIndicator from 'material-ui/RefreshIndicator'; 10 | import CircularProgress from 'material-ui/CircularProgress'; 11 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 12 | 13 | 14 | const LinkToLogin = props => { 15 | let { dispatch } = props 16 | const masterInfo = window.localStorage.getItem('masterInfo') ? true : false 17 | return ( 18 |
19 | {!masterInfo && 20 | { 21 | dispatch(setTransition({ transition: 'up' })) 22 | dispatch(setCurrentRouter('login')) 23 | }}> 24 | 25 | 26 | 27 | 28 | } 29 | {masterInfo && 30 |
31 | 32 | 33 | 34 |
35 | } 36 |
37 | ) 38 | } 39 | 40 | export default LinkToLogin -------------------------------------------------------------------------------- /src/components/HomePage/Lists/styles.scss: -------------------------------------------------------------------------------- 1 | .lists{ 2 | position: relative; 3 | margin-top: 100px; 4 | } 5 | .li{ 6 | position: relative; 7 | top:0; 8 | height: 61px; 9 | padding: 5px 15px; 10 | border-bottom: 1px solid #ccc; 11 | 12 | } 13 | 14 | .link{ 15 | display: block; 16 | } 17 | 18 | .text{ 19 | margin: 0 0 10px; 20 | span{ 21 | padding-right: 10px; 22 | vertical-align: middle; 23 | font-weight:bold; 24 | } 25 | 26 | .title{ 27 | display: inline-block; 28 | padding: 0; 29 | width:80%; 30 | overflow: hidden; 31 | text-overflow:ellipsis; 32 | white-space: nowrap; 33 | } 34 | } 35 | .spinner { 36 | margin: 24px auto; 37 | width: 100%; 38 | text-align: center; 39 | } 40 | 41 | .spinner > div { 42 | width: 30px; 43 | height: 30px; 44 | margin: 0 20px; 45 | background-color: #00BCD4; 46 | 47 | border-radius: 100%; 48 | display: inline-block; 49 | -webkit-animation: bouncedelay 1.4s infinite ease-in-out; 50 | animation: bouncedelay 1.4s infinite ease-in-out; 51 | /* Prevent first frame from flickering when animation starts */ 52 | -webkit-animation-fill-mode: both; 53 | animation-fill-mode: both; 54 | } 55 | 56 | .spinner .bounce1 { 57 | -webkit-animation-delay: -0.32s; 58 | animation-delay: -0.32s; 59 | } 60 | 61 | .spinner .bounce2 { 62 | -webkit-animation-delay: -0.16s; 63 | animation-delay: -0.16s; 64 | } 65 | 66 | @-webkit-keyframes bouncedelay { 67 | 0%, 80%, 100% { -webkit-transform: scale(0.0) } 68 | 40% { -webkit-transform: scale(1.0) } 69 | } 70 | 71 | @keyframes bouncedelay { 72 | 0%, 80%, 100% { 73 | transform: scale(0.0); 74 | -webkit-transform: scale(0.0); 75 | } 40% { 76 | transform: scale(1.0); 77 | -webkit-transform: scale(1.0); 78 | } 79 | } -------------------------------------------------------------------------------- /src/containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import {fetchAccess,fetchMessage,fetchProfile} from '../actions' 4 | import Header from '../components/common/Header/Header' 5 | import Content from '../components/Article/Content/Content' 6 | import Reply from '../components/Article/Reply/Reply' 7 | import getSize from '../utils/getSize' 8 | 9 | 10 | class App extends Component { 11 | componentWillMount(){ 12 | // console.log('componentWillMount') 13 | const {dispatch} = this.props; 14 | const LoadingAction = (accessToken,loginName) => { 15 | dispatch(fetchAccess(accessToken)) 16 | dispatch(fetchMessage(accessToken)) 17 | dispatch(fetchProfile(loginName)) 18 | } 19 | if(window.localStorage.getItem('masterInfo')){ 20 | let masterInfo = window.localStorage.getItem('masterInfo') 21 | masterInfo = JSON.parse(masterInfo) 22 | const accessToken = masterInfo.accessToken 23 | const loginName = masterInfo.loginName 24 | LoadingAction(accessToken,loginName) 25 | }else{ 26 | // const accessToken = '1cbc2a58-6c1b-426f-971d-070676fb849d' 27 | // const loginName = 'lumia2046' 28 | // LoadingAction(accessToken,loginName) 29 | } 30 | } 31 | 32 | render() { 33 | return ( 34 |
{this.props.children}
35 | ) 36 | } 37 | } 38 | 39 | App.propTypes = { 40 | // currentTopicId: PropTypes.string.isRequired, 41 | // article: PropTypes.object.isRequired, 42 | // isFetching: PropTypes.bool.isRequired, 43 | // dispatch: PropTypes.func.isRequired 44 | } 45 | 46 | function mapStateToProps(state) { 47 | const {login,profile} = state 48 | return {login,profile} 49 | } 50 | 51 | 52 | export default connect(mapStateToProps)(App) -------------------------------------------------------------------------------- /src/utils/getPosition.js: -------------------------------------------------------------------------------- 1 | const getPosition = (direction, DOMNode, className) => { 2 | switch(document.getElementsByClassName(className).length){ 3 | case 0: 4 | alert('注意:传入的className对应的元素不存在!') 5 | break 6 | case 1: 7 | break 8 | default: 9 | alert('注意:传入的className对应多个元素!请给该元素唯一className') 10 | 11 | } 12 | let offset = DOMNode[`offset${direction}`] 13 | let parent = DOMNode.offsetParent 14 | if (parent) { 15 | if (className) { 16 | if (parent.className != className) { 17 | offset += getPosition(direction, parent, className); 18 | } 19 | } else { 20 | if (parent.nodeName != 'BODY') { 21 | offset += getPosition(direction, parent); 22 | } 23 | } 24 | } 25 | 26 | return offset 27 | } 28 | 29 | export const getTop = (DOMNode, className) => { 30 | return getPosition('Top', DOMNode, className) 31 | } 32 | 33 | export const getLeft = (DOMNode, className) => { 34 | return getPosition('Left', DOMNode, className) 35 | } 36 | 37 | export const getBottom = (DOMNode, className) => { 38 | let containerHeight = 0; 39 | let selfHeight = DOMNode.offsetHeight 40 | if (className) { 41 | containerHeight = document.getElementsByClassName(className)[0].offsetHeight 42 | } else { 43 | containerHeight = document.getElementsByTagName('body')[0].offsetHeight 44 | } 45 | return containerHeight - selfHeight - getTop(DOMNode, className) 46 | } 47 | 48 | export const getRight = (DOMNode, className) => { 49 | let containerWidth = 0; 50 | let selfWidth = DOMNode.offsetWidth 51 | if (className) { 52 | containerWidth = document.getElementsByClassName(className)[0].offsetWidth 53 | } else { 54 | containerWidth = document.getElementsByTagName('body')[0].offsetWidth 55 | } 56 | return containerWidth - selfWidth - getLeft(DOMNode, className) 57 | } -------------------------------------------------------------------------------- /src/components/common/Header/Header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import { withRouter } from 'react-router-dom' 4 | import { setTransition } from '../../../actions/hashUrl' 5 | import styles from './styles.scss' 6 | import AppBar from 'material-ui/AppBar' 7 | import IconButton from 'material-ui/IconButton' 8 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' 9 | import getSize from '../../../utils/getSize' 10 | 11 | // @withRouter 12 | @connect(store => ({ hashUrl: store.hashUrl })) 13 | class Header extends Component { 14 | constructor() { 15 | super() 16 | this.state = { position: 'static' } 17 | 18 | } 19 | componentWillMount() { 20 | if (this.props.position) { 21 | this.state.position = 'absolute' 22 | this.state.left = '100%' 23 | } 24 | } 25 | 26 | componentDidMount() { 27 | if (this.props.position) { 28 | this.setState({ left: 0 }) 29 | setTimeout(() => this.setState({ position: 'fixed' }), 500) 30 | } 31 | } 32 | 33 | componentWillUnmount() { 34 | 35 | } 36 | render() { 37 | const { isFetching, title, history, hashUrl } = this.props 38 | return ( 39 |
40 | 41 | 43 | {isFetching ? '加载中' : title} 44 |

} 45 | iconElementLeft={ 46 | 47 | } 48 | onLeftIconButtonClick={() => { 49 | this.props.dispatch(setTransition({ transition: 'left' })) 50 | history.goBack() 51 | if (this.props.position) { 52 | setTimeout(() => this.setState({ left: '100%' }), 450) 53 | } 54 | // window.location.href = `${window.location.href.split('#')[0]}#${hashUrl.oldUrl}` 55 | }} 56 | /> 57 |
58 |
59 | ) 60 | } 61 | } 62 | 63 | 64 | export default withRouter(Header) -------------------------------------------------------------------------------- /src/reducers/homePage.js: -------------------------------------------------------------------------------- 1 | import { 2 | SELECT_TAB, RECORD_SCROLLT, 3 | REQUEST_TOPICS, RECEIVE_TOPICS 4 | } from '../actions' 5 | 6 | 7 | const selectedTab = (state, action) => { 8 | switch (action.type) { 9 | case SELECT_TAB: 10 | return action.tab 11 | default: 12 | return state 13 | } 14 | } 15 | 16 | // 当组件第一次发出REQUEST_TOPICS后,需要对其返回的state进行初始化,否则没有topics等属性会报错 17 | function tabDataItem(state = { isFetching: false, page: 0, scrollT: 0, topics: [] }, action) { 18 | switch (action.type) { 19 | case REQUEST_TOPICS: 20 | return { 21 | ...state, 22 | isFetching: true 23 | } 24 | case RECEIVE_TOPICS: 25 | if (state.page < action.page) { 26 | let topics = state.topics 27 | action.topics = topics.concat(action.topics) 28 | } 29 | return { 30 | ...state, 31 | isFetching: false, 32 | page: action.page, 33 | topics: action.topics, 34 | limit: action.limit 35 | } 36 | case RECORD_SCROLLT: 37 | return { 38 | ...state, 39 | scrollT: action.scrollT 40 | } 41 | default: 42 | return state 43 | } 44 | } 45 | 46 | const tabData = (state = {}, action) => { 47 | switch (action.type) { 48 | case RECEIVE_TOPICS: 49 | case REQUEST_TOPICS: 50 | case RECORD_SCROLLT: 51 | return { 52 | ...state, 53 | [action.tab]: tabDataItem(state[action.tab], action) 54 | } 55 | default: 56 | return state 57 | } 58 | } 59 | 60 | const initState = sessionStorage.getItem('store') ? JSON.parse(sessionStorage.getItem('store')).homePage : { 61 | selectedTab: 'all', tabData: {} 62 | } 63 | 64 | const homePage = (state = initState, action) => { 65 | if (state) { 66 | const sTab = selectedTab(state.selectedTab, action); 67 | const tData = tabData(state.tabData, action); 68 | // 返回的一定要是一个新的对象,如果只是改变原来的state,返回的还是原来的state对象,就无法被store.subscribe监听到,从而不会对组件状态进行更新 69 | return { ...state, selectedTab: sTab, tabData: tData } 70 | } 71 | return state 72 | 73 | } 74 | 75 | export default homePage; 76 | 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 项目简介 2 | 一个WebApp版的cnode客户端,项目采用react技术栈构建。组件选用的是[Material-UI](http://www.material-ui.com/),让界面更适合触控操作。 3 | - 感谢来自[cnodejs论坛](https://cnodejs.org/)官方提供的api! 4 | 5 | ## 功能 6 | - 首页列表,下拉时自动加载下一页,在顶端上拉刷新 7 | - 主题详情,登陆后能够收藏,评论和点赞 8 | - 消息提醒,能查看消息详情和清空所有未读消息 9 | - 个人主页,包括最近参与,回复,以及收藏的主题 10 | - 发表主题,成功后能跳转到相应主题页面 11 | - 页面后退,能还原数据和滚动位置 12 | 13 | ## 运用的技术主要有: 14 | - 采用react技术栈,通过Redux来管理页面状态,通过Router来设置页面路由 15 | - 组件选用的是Material-UI,不再自己造轮子,既美观又能方便触控操作 16 | - 使用react-route v4 和 react原生的react-transition-group v2 来实现路由切换动画 17 | - 使用react-flip-move插件来实现list的加载动画 18 | - 应用`isomorphic-fetch`库代替`XMLHttpRequest`实现网络请求 19 | - 使用`PostCSS`对CSS进行预处理 20 | - 通过`CSSModules`处理模块内部的类名 21 | 22 | ## 预览 23 | [DEMO](https://lumia2046.github.io/cnode/) 24 | 25 | ## 运行项目 26 | ``` 27 | git clone https://github.com/lumia2046/cnode.git 28 | cd cnode 29 | npm install 30 | npm start 31 | 打开浏览器访问:http://localhost:5678 32 | ``` 33 | 34 | ## 生产项目 35 | ``` 36 | windows下 37 | npm run build-win 38 | linux、mac下 39 | npm run build-win 40 | ``` 41 | 71 | -------------------------------------------------------------------------------- /src/components/PublishTopic/Form/Form.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes,Component } from 'react' 2 | import getStrLength from '../../../utils/getStrLength' 3 | import styles from './styles.scss' 4 | import classnames from 'classnames' 5 | import TextField from 'material-ui/TextField'; 6 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' 7 | import RaisedButton from 'material-ui/RaisedButton' 8 | import DropDownMenu from 'material-ui/DropDownMenu'; 9 | import MenuItem from 'material-ui/MenuItem'; 10 | 11 | class Form extends Component { 12 | constructor(props) { 13 | super(props); 14 | this.state = {value: "ask"}; 15 | } 16 | handleChange = (event, index, value) => this.setState({value}); 17 | render(){ 18 | const {ifTitleErr,ifContentErr,showDialog,fetchPublishTopic,dispatch,login,state} = this.props 19 | return ( 20 | 21 |
22 |
23 | 请选择主题类别: 24 | 25 | 26 | 27 | 28 | 29 |
30 |
31 | { 32 | let titleErr = getStrLength(e.target.value)<10 ? true:false 33 | ifTitleErr(titleErr) 34 | }}/> 35 |
标题不得少于十个字符!
36 |
37 |
38 | { 40 | let contentErr = getStrLength(e.target.value)===0 ? true:false 41 | ifContentErr(contentErr) 42 | }}/> 43 |
内容不能为空!
44 |
45 | { 46 | e.preventDefault() 47 | const input = this.refs.input.input.value 48 | const textarea = this.refs.textarea.input.refs.input.value 49 | const select = this.refs.select.props.value 50 | if(getStrLength(input) < 10 || !textarea.trim()){ 51 | return null 52 | } 53 | dispatch(fetchPublishTopic(login.accessToken,select,input,textarea)) 54 | showDialog() 55 | }} /> 56 | 57 |
58 | ) 59 | } 60 | } 61 | 62 | export default Form -------------------------------------------------------------------------------- /src/utils/myFetch.js: -------------------------------------------------------------------------------- 1 | import originFetch from 'isomorphic-fetch' 2 | import { fetchError, fetchStart, fetchEnd } from '../actions/fetchError' 3 | 4 | const getData = async (url, option) => { 5 | try { 6 | let response = await originFetch(url, option) 7 | if (response.ok) { 8 | return response 9 | } else { 10 | let error = await response.json() 11 | throw error 12 | } 13 | } catch (error) { 14 | myDispatch(fetchError(error)) 15 | } 16 | return new Promise(() => { }) 17 | } 18 | 19 | const myFetch = (url, option) => { 20 | 21 | let myOption = { 22 | credentials: 'include', 23 | headers: { 24 | "Content-type": "application/json;charset=UTF-8" 25 | } 26 | } 27 | option = option || {} 28 | option = { ...myOption, ...option } 29 | 30 | // 方案1 31 | // return new Promise((resolve, reject) => { 32 | // originFetch(url, option) 33 | // .then(response => { 34 | // if (response.ok) { 35 | // resolve(response) 36 | // } else { 37 | // response.json().then(json => { 38 | // //response的then里产生或抛出的异常,无法直接抛出到该链式调用外面去被originFetch的catch捕获,除非获取到了originFetch的reject 39 | // reject(json)//直接拿到这个return的new Promise的reject方法,能被它的catch方法(即最外面那个catch方法)捕捉到 40 | // }) 41 | // } 42 | // }) 43 | // .catch(error => myDispatch(fetchError(error)))//获取originFetch链式调用里产生的异常,这里能获取到的是fetch请求失败,无返回response,服务器无响应的异常 44 | // }) 45 | // .catch(error => { //获取的是reject(json)里的json数据 46 | // myDispatch(fetchError(error)) 47 | // return new Promise(() => { }) //catch后面如果有then,then里面的方法执行可能会报错,要想其不执行的办法是返回一个promise对象携带空的方法 48 | // }) 49 | 50 | 51 | //方案2 52 | // return new Promise((resolve, reject) => { 53 | // originFetch(url, option) 54 | // .then(response => { 55 | // if (response.ok) { 56 | // resolve(response) 57 | // } else { 58 | // response.json().then(json => { 59 | // throw json 60 | // }) 61 | // .catch(error => myDispatch(fetchError(error)))//也可以直接在这里获取response里抛出的错误 62 | // //promise对象如果不调用resolve方法,后面的then链路里的函数是不会被执行的 63 | // } 64 | // }) 65 | // .catch(error => myDispatch(fetchError(error)))//获取originFetch链式调用里产生的异常,这里能获取到的是fetch请求失败,无返回response,服务器无响应的异常 66 | // }) 67 | 68 | //方案3 69 | //getData方法在最上面定义 70 | return getData(url, option) 71 | } 72 | 73 | export default myFetch -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cnode", 3 | "version": "2.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server", 8 | "lint": "eslint src", 9 | "build-mac": "export NODE_ENV=production && webpack --progress --hide-modules --config webpack.config.js", 10 | "build-win": "set NODE_ENV=production && webpack --progress --hide-modules --config webpack.config.js", 11 | "test": "jest" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "babel-polyfill": "^6.20.0", 18 | "github-markdown-css": "^2.4.1", 19 | "history": "^4.6.1", 20 | "isomorphic-fetch": "^2.2.1", 21 | "lazy-load-component": "^1.1.2", 22 | "material-ui": "^0.20.0", 23 | "normalize.css": "^7.0.0", 24 | "pullhelper": "^1.1.8", 25 | "react": "^16.0.0", 26 | "react-dom": "^16.0.0", 27 | "react-flip-move": "^3.0.0", 28 | "react-modal": "^3.1.0", 29 | "react-motion": "^0.5.2", 30 | "react-redux": "^5.0.6", 31 | "react-router": "^4.2.0", 32 | "react-router-dom": "^4.1.2", 33 | "react-router-redux": "^4.0.8", 34 | "react-swipeable-views": "^0.12.9", 35 | "react-tap-event-plugin": "^3.0.2", 36 | "react-transition-group": "^2.2.1", 37 | "redux": "^3.6.0", 38 | "redux-logger": "^3.0.6", 39 | "redux-thunk": "^2.1.0" 40 | }, 41 | "devDependencies": { 42 | "autoprefixer": "^7.1.6", 43 | "babel-cli": "^6.24.1", 44 | "babel-core": "^6.25.0", 45 | "babel-loader": "^7.1.1", 46 | "babel-plugin-import": "^1.2.1", 47 | "babel-plugin-react-transform": "^3.0.0", 48 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 49 | "babel-plugin-transform-runtime": "^6.23.0", 50 | "babel-polyfill": "^6.23.0", 51 | "babel-preset-env": "^1.6.1", 52 | "babel-preset-react": "^6.24.1", 53 | "babel-preset-react-hmre": "^1.1.1", 54 | "babel-preset-stage-0": "^6.16.0", 55 | "babel-preset-stage-1": "^6.16.0", 56 | "babel-preset-stage-2": "^6.17.0", 57 | "babel-preset-stage-3": "^6.17.0", 58 | "cross-env": "^5.1.1", 59 | "css-loader": "^0.28.7", 60 | "eventsource-polyfill": "^0.9.6", 61 | "express": "^4.13.3", 62 | "extract-text-webpack-plugin": "^3.0.2", 63 | "file-loader": "^1.1.5", 64 | "html-webpack-plugin": "^2.24.1", 65 | "node-sass": "^4.6.0", 66 | "open": "0.0.5", 67 | "postcss-loader": "^2.0.8", 68 | "postcss-scss": "^1.0.2", 69 | "react-hot-loader": "^4.0.0-beta.14", 70 | "react-transform-catch-errors": "^1.0.2", 71 | "redbox-react": "^1.3.3", 72 | "redux-devtools-extension": "^2.13.2", 73 | "rimraf": "^2.4.3", 74 | "sass-loader": "^6.0.6", 75 | "style-loader": "^0.19.0", 76 | "url-loader": "^0.6.2", 77 | "webpack": "^3.8.1", 78 | "webpack-dev-server": "^2.9.4", 79 | "moment": "^2.18.1" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/components/HomePage/Lists/Lists.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import FlipMove from 'react-flip-move' 3 | import transformDate from '../../../utils/transformDate' 4 | import styles from './styles.scss' 5 | import { setTransition } from '../../../actions/hashUrl' 6 | import { Link } from 'react-router-dom' 7 | import prefix from '../../../utils/routePrefix' 8 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' 9 | import { List, ListItem } from 'material-ui/List' 10 | import Divider from 'material-ui/Divider' 11 | import Avatar from 'material-ui/Avatar' 12 | import getSize from '../../../utils/getSize' 13 | 14 | const Lists = props => { 15 | const tabChn = { all: '全部', good: '精华', share: '分享', ask: '问答', job: '招聘' } 16 | const { topics, fetchArticle, dispatch, article, isFetching, selectedTab, history } = props 17 | let disableAllAnimations = topics.length === 20 ? false : true 18 | // disableAllAnimations从启用到禁用时enterAnimation设定的动画会不起作用,原因不明。 19 | let enterAnimation = { 20 | from: { transform: 'translateY(-80px)', opacity: 0 }, 21 | to: { transform: 'translateY(0)', opacity: 1 } 22 | } 23 | return ( 24 |
25 |
26 | 27 | 28 | 30 | {topics.map((topic, i) => 31 | { 32 | dispatch(setTransition({ transition: 'move' })) 33 | if (!article[topic.id]) { 34 | dispatch(fetchArticle(topic.id)) 35 | } else if (article.currentTopicId !== topic.id) { 36 | dispatch(fetchArticle(topic.id, false)) 37 | } 38 | }}> 39 | } 41 | primaryText={ 42 |
43 | {topic.top && } 44 | {topic.good && } 45 | {topic.title} 46 |
47 | } 48 | secondaryText={ 49 |
50 | {topic.reply_count + '/' + topic.visit_count} 51 | {tabChn[topic.tab]} 52 | {transformDate(topic.create_at)} 53 |
54 | } 55 | /> 56 | 57 | 58 | )} 59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | ) 70 | } 71 | 72 | // Lists.propTypes = { 73 | // topics: PropTypes.array.isRequired, 74 | // fetchArticle: PropTypes.func.isRequired 75 | // } 76 | 77 | export default Lists 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /src/components/common/Profile/Profile.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Link} from 'react-router-dom' 3 | import prefix from '../../../utils/routePrefix' 4 | import styles from './styles.scss' 5 | import { setTransition } from '../../../actions/hashUrl' 6 | import transformDate from '../../../utils/transformDate' 7 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 8 | import Avatar from 'material-ui/Avatar'; 9 | import {List, ListItem} from 'material-ui/List'; 10 | import Subheader from 'material-ui/Subheader'; 11 | import Divider from 'material-ui/Divider'; 12 | 13 | const Profile = props => { 14 | let {collectedTopics,profile} = props 15 | let {avatar_url,create_at,loginname,recent_replies,recent_topics,score} = profile; 16 | recent_replies = recent_replies ? recent_replies : []; 17 | recent_topics = recent_topics ? recent_topics : []; 18 | 19 | return ( 20 |
21 |
22 | {loginname}/ 23 |

{loginname}

24 |

积分:{score}

25 |

注册于:{transformDate(create_at)}

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 | const TopicList = props => { 61 | const {dispatch,article,fetchArticle,topics} = props; 62 | 63 | return ( 64 |
65 | {topics.length === 0 && } 66 | {topics.length > 0 && 67 | topics.map((topic,index) => 68 | { 69 | dispatch(setTransition({ transition: 'up' })) 70 | if(!article[topic.id]){ 71 | dispatch(fetchArticle(topic.id)) 72 | }else if(article.currentTopicId !== topic.id){ 73 | dispatch(fetchArticle(topic.id,false)) 74 | } 75 | }}> 76 | }/> 77 | 78 | 79 | 80 | ) 81 | } 82 |
83 | ) 84 | } 85 | 86 | 87 | const ListExampleChat = () => ( 88 | 89 | Recent chats 90 | } 93 | /> 94 | 95 | 96 | ); 97 | 98 | 99 | export default Profile -------------------------------------------------------------------------------- /src/styles/index.css: -------------------------------------------------------------------------------- 1 | /*Normalize.css 只是一个很小的CSS文件,但它在默认的HTML元素样式上提供了跨浏览器的高度一致性*/ 2 | @import '~normalize.css/normalize.css'; 3 | @import "~github-markdown-css/github-markdown.css"; 4 | 5 | @font-face {font-family: "iconfont"; 6 | src: url('iconfont/iconfont.eot'); /* IE9*/ 7 | src: url('iconfont/iconfont.eot#iefix') format('embedded-opentype'), /* IE6-IE8 */ 8 | url('iconfont/iconfont.woff') format('woff'), /* chrome, firefox */ 9 | url('iconfont/iconfont.ttf') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ 10 | url('iconfont/iconfont.svg#iconfont') format('svg'); /* iOS 4.1- */ 11 | } 12 | 13 | .iconfont { 14 | font-family:"iconfont" !important; 15 | font-size:20px; 16 | font-style:normal; 17 | -webkit-font-smoothing: antialiased; 18 | -webkit-text-stroke-width: 0.2px; 19 | -moz-osx-font-smoothing: grayscale; 20 | } 21 | 22 | body,h2,p,ul,li{ 23 | margin:0; 24 | padding:0; 25 | } 26 | ul{ 27 | padding-left:0; 28 | } 29 | li{ 30 | list-style-type: none; 31 | } 32 | a:hover,a:link,a:visited,a:active{ 33 | text-decoration: none; 34 | } 35 | 36 | /* .fade-in{ 37 | animation: fadeIn 0.5s ease-out; 38 | } 39 | 40 | @keyframes fadeIn{ 41 | 0%{opacity:0;} 42 | 100%{opacity:1;} 43 | } */ 44 | 45 | 46 | /* .move-appear { 47 | transform: scale(50); 48 | background: grey; 49 | } 50 | 51 | .move-appear.move-appear-active { 52 | transform: scale(1); 53 | background: grey; 54 | transition: 60s; 55 | } */ 56 | 57 | .move-enter { 58 | position: absolute; 59 | z-index: 100; 60 | width: 100%; 61 | height: 100%; 62 | background: white; 63 | top: 0; 64 | left: 100%; 65 | 66 | } 67 | 68 | .move-enter.move-enter-active { 69 | left: 0%; 70 | transition: 0.5s; 71 | } 72 | 73 | .move-exit { 74 | position: absolute; 75 | z-index: 50; 76 | width: 100%; 77 | height: 100%; 78 | opacity: 50; 79 | background: white; 80 | top: 0; 81 | left: 0; 82 | } 83 | 84 | .move-exit.move-exit-active { 85 | opacity: 50; 86 | left: -100%; 87 | transition: 0.5s; 88 | } 89 | 90 | .left-enter { 91 | position: absolute; 92 | z-index: 100; 93 | width: 100%; 94 | height: 100%; 95 | background: white; 96 | top: 0; 97 | left: 0; 98 | 99 | } 100 | 101 | .left-enter.left-enter-active { 102 | left: 0; 103 | } 104 | 105 | .left-exit { 106 | position: absolute; 107 | z-index: 100; 108 | width: 100%; 109 | height: 100%; 110 | background: white; 111 | top: 0; 112 | left: 0; 113 | } 114 | 115 | .left-exit.left-exit-active { 116 | left: 100%; 117 | transition: 0.5s; 118 | } 119 | 120 | 121 | 122 | .up-enter { 123 | position: absolute; 124 | z-index: 10000; 125 | width: 100%; 126 | height: 100%; 127 | background: white; 128 | top: 100%; 129 | left: 0; 130 | 131 | } 132 | 133 | .up-enter.up-enter-active { 134 | top: 0; 135 | transition: 0.5s; 136 | } 137 | 138 | .up-exit { 139 | position: absolute; 140 | z-index: 100; 141 | width: 100%; 142 | height: 100%; 143 | background: white; 144 | top: 0; 145 | left: 0; 146 | 147 | } 148 | 149 | .up-exit.up-exit-active { 150 | top: 0; 151 | } -------------------------------------------------------------------------------- /src/components/HomePage/Header/Header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import styles from './styles.scss' 4 | import { setTransition } from '../../../actions/hashUrl' 5 | import { Link } from 'react-router-dom' 6 | import prefix from '../../../utils/routePrefix' 7 | import { Tabs, Tab } from 'material-ui/Tabs' 8 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' 9 | import AppBar from 'material-ui/AppBar' 10 | import Badge from 'material-ui/Badge' 11 | import NotificationsIcon from 'material-ui/svg-icons/social/notifications' 12 | import IconButton from 'material-ui/IconButton' 13 | import SwipeableViews from 'react-swipeable-views' 14 | import getSize from '../../../utils/getSize' 15 | 16 | @connect() 17 | class Header extends Component { 18 | constructor(props) { 19 | super(props); 20 | this.state = { 21 | slideIndex: 0 22 | } 23 | 24 | } 25 | componentWillMount() { 26 | const { tabs, filter } = this.props; 27 | let slideIndex; 28 | tabs.forEach((tab, index) => { 29 | if (tab.filter === filter) { 30 | slideIndex = index; 31 | } 32 | }) 33 | this.setState({ 34 | slideIndex: slideIndex 35 | }) 36 | } 37 | handleChange = (value) => { 38 | this.setState({ 39 | slideIndex: value, 40 | }); 41 | this.props.onClick(this.props.tabs[value].filter) 42 | }; 43 | 44 | render() { 45 | // this.props.tabs.forEach(tab => { 46 | // tab.cn = classnames({[styles.active] : tab.filter === this.props.filter}) 47 | // }) 48 | return ( 49 | 50 |
51 |
52 | NodeJS论坛

} onLeftIconButtonClick={this.props.toggleDrawer} 53 | iconElementRight={
54 | 55 | this.props.dispatch(setTransition({ transition: 'up' }))}> 56 | 57 |
58 | 59 |
60 |
61 | 62 |
63 |
} /> 64 | 65 | {this.props.tabs.map((tab, i) => 66 | 67 | 68 | )} 69 | 70 |
71 | 72 | {this.props.children} 73 | 74 |
75 |
76 | ) 77 | } 78 | } 79 | // Header.propTypes = { 80 | // filter: PropTypes.string.isRequired, 81 | // onClick: PropTypes.func.isRequired 82 | // } 83 | 84 | export default Header -------------------------------------------------------------------------------- /src/styles/iconfont/demo_fontclass.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | IconFont 7 | 8 | 9 | 10 | 11 |
12 |

IconFont 图标

13 |
    14 | 15 |
  • 16 | 17 |
    喜爱
    18 |
    .icon-xiai
    19 |
  • 20 | 21 |
  • 22 | 23 |
    24 |
    .icon-ding
    25 |
  • 26 | 27 |
  • 28 | 29 |
    用户
    30 |
    .icon-user
    31 |
  • 32 | 33 |
  • 34 | 35 |
    返回
    36 |
    .icon-back
    37 |
  • 38 | 39 |
  • 40 | 41 |
    信息
    42 |
    .icon-informatiom
    43 |
  • 44 | 45 |
  • 46 | 47 |
    回复
    48 |
    .icon-huifu
    49 |
  • 50 | 51 |
  • 52 | 53 |
    查看过
    54 |
    .icon-chakanguo
    55 |
  • 56 | 57 |
58 | 59 |

font-class引用

60 |
61 | 62 |

font-class是unicode使用方式的一种变种,主要是解决unicode书写不直观,语意不明确的问题。

63 |

与unicode使用方式相比,具有如下特点:

64 |
    65 |
  • 兼容性良好,支持ie8+,及所有现代浏览器。
  • 66 |
  • 相比于unicode语意明确,书写更直观。可以很容易分辨这个icon是什么。
  • 67 |
  • 因为使用class来定义图标,所以当要替换图标时,只需要修改class里面的unicode引用。
  • 68 |
  • 不过因为本质上还是使用的字体,所以多色图标还是不支持的。
  • 69 |
70 |

使用步骤如下:

71 |

第一步:引入项目下面生成的fontclass代码:

72 | 73 | 74 |
<link rel="stylesheet" type="text/css" href="./iconfont.css">
75 |

第二步:挑选相应图标并获取类名,应用于页面:

76 |
<i class="iconfont icon-xxx"></i>
77 |
78 |

"iconfont"是你项目下的font-family。可以通过编辑项目查看,默认是"iconfont"。

79 |
80 |
81 | 82 | 83 | -------------------------------------------------------------------------------- /src/containers/PublishTopic.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import {Link} from 'react-router-dom' 4 | import prefix from '../utils/routePrefix' 5 | import {fetchArticle,fetchPublishTopic} from '../actions' 6 | import Header from '../components/common/Header/Header' 7 | import Dialog from '../components/common/Dialog' 8 | import LinkToLogin from '../components/common/LinkToLogin/LinkToLogin' 9 | import Form from '../components/PublishTopic/Form/Form' 10 | 11 | 12 | class PublishTopic extends Component { 13 | constructor(){ 14 | super(); 15 | this.state = { 16 | isOpen:false, 17 | isFetching: false, 18 | titleErr:false, 19 | contentErr:false 20 | } 21 | } 22 | 23 | componentWillReceiveProps(newProps){ 24 | const {publishTopic,dispatch} = newProps; 25 | if(!this.props.publishTopic.topicId || this.props.publishTopic.topicId !== publishTopic.topicId){ 26 | this.setState({isFetching:false}) 27 | dispatch(fetchArticle(publishTopic.topicId)) 28 | } 29 | } 30 | 31 | showDialog() { 32 | this.setState({ 33 | isOpen:true, 34 | isFetching:true 35 | }) 36 | } 37 | close = () => { 38 | this.setState({ 39 | isOpen:false 40 | }) 41 | } 42 | 43 | ifTitleErr(boolean) { 44 | this.setState({ 45 | titleErr: boolean 46 | }) 47 | } 48 | ifContentErr(boolean) { 49 | this.setState({ 50 | contentErr: boolean 51 | }) 52 | } 53 | render(){ 54 | const {dispatch,publishTopic,currentRouter,login} = this.props; 55 | const ifTitleErr = this.ifTitleErr.bind(this) 56 | const ifContentErr = this.ifContentErr.bind(this) 57 | const showDialog = this.showDialog.bind(this) 58 | const state = this.state 59 | return ( 60 |
61 |
62 |
63 | {login.succeed &&
} 64 | {!login.succeed && } 65 |
66 | 67 | {state.isFetching &&
加载中
} 68 | {!state.isFetching &&
发送成功,去查看
} 69 |
70 |
71 | ) 72 | } 73 | } 74 | 75 | // HomePage.propTypes = { 76 | // selectedTab: PropTypes.string.isRequired, 77 | // topics: PropTypes.array.isRequired, 78 | // isFetching: PropTypes.bool.isRequired, 79 | // page:PropTypes.number.isRequired, 80 | // scrollT:PropTypes.number.isRequired, 81 | // dispatch: PropTypes.func.isRequired 82 | // } 83 | 84 | function mapStateToProps(state) { 85 | const {publishTopic,login} = state; 86 | // const {selectedTab,tabData} = state.homePage; 87 | // // 当组件第一次render时,还未进行任何action,初始化的state里没有tabData[selectedTab],所以这里需要初始化 88 | // const {isFetching,page,scrollT,topics} = tabData[selectedTab] || {isFetching:false,page:0,scrollT:0,topics:[]} 89 | return {publishTopic,login} 90 | } 91 | 92 | // 用connect方法连接HomePage组件,实际上是在HomePage组件上加上了Connect(HomePage)这个父组件,HomePage里有关Connect的state变化的props就是通过mapStateToProps设置的 93 | // connect方法的第二个参数如果不传的话就会默认将store.dispatch默认作为了dispatch参数传进HomePage的props,所以HomePage的props里就有一个dispatch 94 | export default connect(mapStateToProps)(PublishTopic) -------------------------------------------------------------------------------- /src/containers/Article.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { switchSupport, fetchComment, fetchArticle, recordArticleScrollT, fetchProfile } from '../actions' 4 | import Header from '../components/common/Header/Header' 5 | import CircleLoading from '../components/common/CircleLoading' 6 | import AsyncContainer from '../components/common/AsyncContainer' 7 | import Content from '../components/Article/Content/Content' 8 | import Reply from '../components/Article/Reply/Reply' 9 | import getSize from '../utils/getSize' 10 | 11 | 12 | class Article extends Component { 13 | constructor() { 14 | super() 15 | this.state = { 16 | fadeIn: true 17 | } 18 | } 19 | componentWillMount() { 20 | 21 | const { scrollT, dispatch, article, isFetching } = this.props 22 | 23 | if (scrollT) { 24 | // window.scrollTo(0, scrollT) 25 | } 26 | // window.scrollTo(0, scrollT) 27 | 28 | if (!article.author && !isFetching) { 29 | const topicId = window.location.href.split('topic/')[1].split('?_')[0] 30 | dispatch(fetchArticle(topicId)) 31 | } 32 | } 33 | 34 | componentWillReceiveProps(newProps) { 35 | const { scrollT } = newProps 36 | document.getElementById('articleContainer').scrollTop = scrollT 37 | } 38 | 39 | componentDidMount() { 40 | const { scrollT } = this.props 41 | document.getElementById('articleContainer').scrollTop = scrollT 42 | } 43 | 44 | componentWillUnmount() { 45 | const { currentTopicId, dispatch, profile, login } = this.props 46 | dispatch(recordArticleScrollT(currentTopicId, document.getElementById('articleContainer').scrollTop)) 47 | if (!window.sessionStorage.masterProfile && login.loginName === profile.loginname) { 48 | window.sessionStorage.masterProfile = JSON.stringify(profile) 49 | } 50 | } 51 | 52 | render() { 53 | let { isFetching, article, currentTopicId, login, switchSupportInfo, isCommented, dispatch, collectedTopics, profile } = this.props 54 | if (login.loginName !== profile.loginname && window.sessionStorage.masterProfile) { 55 | collectedTopics = JSON.parse(window.sessionStorage.masterProfile).collectedTopics 56 | } 57 | return ( 58 | 59 |
60 |
61 | {Object.keys(article).length === 0 && } 62 | {Object.keys(article).length !== 0 && 63 |
64 | 65 | 66 | 68 | 69 |
70 | } 71 |
72 | 73 | ) 74 | } 75 | } 76 | 77 | function mapStateToProps(state) { 78 | const { currentRouter, login, profile } = state; 79 | const { currentTopicId, switchSupportInfo, isCommented } = state.article; 80 | const { collectedTopics } = profile 81 | const isFetching = state.article[currentTopicId] ? state.article[currentTopicId].isFetching : false; 82 | const scrollT = state.article[currentTopicId] ? state.article[currentTopicId].scrollT : '0'; 83 | const article = state.article[currentTopicId] && state.article[currentTopicId].article ? state.article[currentTopicId].article : {}; 84 | return { isFetching, scrollT, article, currentTopicId, login, switchSupportInfo, currentRouter, collectedTopics, profile, isCommented } 85 | } 86 | 87 | 88 | export default connect(mapStateToProps)(Article) -------------------------------------------------------------------------------- /src/styles/iconfont/iconfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Created by FontForge 20120731 at Wed Dec 14 17:41:22 2016 6 | By admin 7 | 8 | 9 | 10 | 24 | 26 | 28 | 30 | 32 | 34 | 38 | 41 | 44 | 46 | 48 | 50 | 52 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/components/HomePage/Drawer/Drawer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom' 3 | import { connect } from 'react-redux' 4 | import prefix from '../../../utils/routePrefix' 5 | import Drawer from 'material-ui/Drawer' 6 | import MenuItem from 'material-ui/MenuItem' 7 | import RaisedButton from 'material-ui/RaisedButton' 8 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' 9 | import Divider from 'material-ui/Divider' 10 | import Avatar from 'material-ui/Avatar' 11 | import { logout, fetchProfile } from '../../../actions' 12 | import { setTransition } from '../../../actions/hashUrl' 13 | import styles from './styles.scss' 14 | import getSize from '../../../utils/getSize' 15 | import transformDate from '../../../utils/transformDate' 16 | import Dialog from '../../common/Dialog' 17 | 18 | @connect() 19 | export default class myDrawer extends React.Component { 20 | constructor() { 21 | super(); 22 | this.state = { 23 | isOpen: false 24 | } 25 | } 26 | 27 | excuteLogout = () => { 28 | const { dispatch } = this.props 29 | window.localStorage.removeItem('masterInfo') 30 | window.sessionStorage.removeItem('masterProfile') 31 | dispatch(logout()) 32 | } 33 | 34 | close = () => { 35 | this.setState({ 36 | isOpen: false 37 | }) 38 | } 39 | 40 | componentWillUpdate(nextProps) { 41 | 42 | } 43 | 44 | render() { 45 | let { contentW } = getSize() 46 | if (contentW > 800) { 47 | contentW = 640 48 | } else { 49 | contentW = 0.8 * contentW 50 | } 51 | let { login, profile } = this.props 52 | const { succeed } = login 53 | if (login.loginName !== profile.loginname && window.sessionStorage.masterProfile) { 54 | profile = JSON.parse(window.sessionStorage.masterProfile) 55 | } 56 | const { avatar_url, loginname, score, create_at } = profile 57 | return ( 58 | 59 | 65 | {succeed &&
66 |
67 | 68 | 69 |

{loginname}

70 | 71 |

积分:{score}

72 |

注册于:{transformDate(create_at)}

73 | { 74 | this.setState({ 75 | isOpen: true 76 | }) 77 | }} /> 78 | 79 | 确定要注销登陆? 80 | 81 |
82 | 83 | 84 | 85 | 个人主页 86 | 87 | 88 | 89 | 90 | 91 | 92 | 消息 93 | 94 | 95 | 96 |
} 97 | {!succeed &&
98 | this.props.dispatch(setTransition({ transition: 'up' }))}> 99 | 100 | 101 | 102 | 103 |

点击头像登陆

104 |
} 105 |
106 |
107 | ); 108 | } 109 | } -------------------------------------------------------------------------------- /src/containers/Login.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import {fetchAccess,fetchMessage,logout,fetchArticle,fetchProfile} from '../actions' 4 | import Header from '../components/common/Header/Header' 5 | import Profile from '../components/common/Profile/Profile' 6 | import getSize from '../utils/getSize' 7 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 8 | import CircularProgress from 'material-ui/CircularProgress'; 9 | import RaisedButton from 'material-ui/RaisedButton'; 10 | import TextField from 'material-ui/TextField'; 11 | import Toggle from 'material-ui/Toggle'; 12 | 13 | class Login extends Component { 14 | constructor(){ 15 | super(); 16 | this.state = { 17 | toggleOn:true 18 | } 19 | } 20 | componentWillReceiveProps(newProps){ 21 | let {succeed,loginName,accessToken,dispatch,profile} = newProps; 22 | if(succeed && !profile.isFetching && profile.loginname !== loginName){ 23 | if(this.state.toggleOn && !window.localStorage.getItem('masterInfo')){ 24 | accessToken = accessToken.trim() 25 | loginName = loginName.trim() 26 | let masterInfo = {accessToken,loginName} 27 | masterInfo = JSON.stringify(masterInfo) 28 | window.localStorage.setItem('masterInfo',masterInfo) 29 | } 30 | dispatch(fetchProfile(loginName)) 31 | dispatch(fetchMessage(accessToken)) 32 | } 33 | } 34 | onToggle = () => { 35 | this.setState({ 36 | toggleOn:!this.state.toggleOn 37 | }) 38 | } 39 | render(){ 40 | let {dispatch,article,profile,failedMessage,succeed,loginName,loginId,accessToken,collectedTopics} = this.props; 41 | if(loginName !== profile.loginname && window.sessionStorage.masterProfile){ 42 | profile = JSON.parse(window.sessionStorage.masterProfile) 43 | collectedTopics = profile.collectedTopics 44 | } 45 | const masterInfo = window.localStorage.getItem('masterInfo') ? true : false 46 | return ( 47 |
48 |
49 |
50 | {!masterInfo && !succeed && 51 | 52 |
53 |
54 | 55 |
56 |
57 | 58 |
59 |
60 | { 61 | const input = this.refs.input.input.value 62 | if(!input.trim()){ 63 | return null; 64 | } 65 | dispatch(fetchAccess(input)) 66 | }}/> 67 |
68 |
69 |
70 | } 71 | {!succeed && failedMessage && 72 |

{failedMessage}

73 | } 74 | {succeed && !profile.loginname && 75 | 76 | 77 | 78 | } 79 | {succeed && profile.loginname && 80 |
81 | 82 |
83 | } 84 |
85 |
86 | ) 87 | } 88 | } 89 | 90 | 91 | function mapStateToProps(state) { 92 | const {article,profile,login,currentRouter} = state; 93 | const {failedMessage,succeed,loginName,loginId,accessToken} = login; 94 | const {collectedTopics} = profile 95 | return {article,profile,succeed,loginName,loginId,accessToken,failedMessage,collectedTopics} 96 | } 97 | 98 | // 用connect方法连接HomePage组件,实际上是在HomePage组件上加上了Connect(HomePage)这个父组件,HomePage里有关Connect的state变化的props就是通过mapStateToProps设置的 99 | // connect方法的第二个参数如果不传的话就会默认将store.dispatch默认作为了dispatch参数传进HomePage的props,所以HomePage的props里就有一个dispatch 100 | export default connect(mapStateToProps)(Login) -------------------------------------------------------------------------------- /src/components/common/react-pullrefresh.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import RefreshIndicator from 'material-ui/RefreshIndicator'; 3 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 4 | // import './rotate.less' 5 | // import image from './pull.svg' 6 | 7 | const MAX_DEFAULT = 100 8 | 9 | class Pull extends Component { 10 | static defaultProps = { 11 | max: MAX_DEFAULT 12 | } 13 | 14 | constructor(props) { 15 | super(props) 16 | this.state = { 17 | pulled:0 18 | } 19 | } 20 | 21 | componentDidMount() { 22 | this.pullhelper = require('pullhelper') 23 | 24 | let { disabled,onRefresh,max } = this.props 25 | let maxPull = max || MAX_DEFAULT 26 | let that= this 27 | 28 | this.pullhelper 29 | .on('start',function(pulled) { 30 | that.setState({ 31 | pulling:true 32 | }) 33 | }) 34 | .on('stepback',function(pulled,next) { 35 | that.setState({ 36 | pulled:pulled 37 | }) 38 | let nextPulled = Math.min(pulled - Math.min(pulled/2,10),max) 39 | next(nextPulled) 40 | }) 41 | .on('step',function(pulled) { 42 | that.setState({ 43 | pulled:pulled 44 | }) 45 | }) 46 | .on('pull',function(pulled,next) { 47 | that.setState({ 48 | pulling:false 49 | }) 50 | if(!onRefresh || pulled < maxPull) { 51 | next() 52 | return 53 | } 54 | that.setState({ 55 | loading:true 56 | }) 57 | onRefresh(_ => { 58 | that.setState({ 59 | loading:false 60 | }) 61 | next() 62 | }) 63 | }) 64 | .load() 65 | if(disabled) { 66 | this.pullhelper.pause() 67 | } 68 | } 69 | componentWillReceiveProps(nextProps) { 70 | let { disabled } = this.props 71 | if(disabled !== nextProps.disabled) { 72 | if(nextProps.disabled) { 73 | this.pullhelper.pause() 74 | } else { 75 | this.pullhelper.resume() 76 | } 77 | } 78 | } 79 | componentWillUnmount() { 80 | this.pullhelper.unload() 81 | } 82 | render() { 83 | let { pulling,loading,pulled } = this.state 84 | let maxPull = this.props.max || MAX_DEFAULT 85 | let size = this.props.size || 30 86 | let color = this.props.color || '#00BCD4' 87 | let style = this.props.style || {} 88 | return ( 89 |
90 |
100 |
113 | 114 | 0.9999 ? 'loading':'ready'} 122 | style={{display:'inline-block', 123 | position:'relative', 124 | opacity:pulled/maxPull}} 125 | /> 126 | 127 |
128 |
129 | ) 130 | } 131 | } 132 | 133 | export default Pull; 134 | 135 | 136 | 137 | 138 | 139 | 140 | // 152 | // -------------------------------------------------------------------------------- /src/components/Article/Content/Content.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import { Link } from 'react-router-dom' 3 | import { connect } from 'react-redux' 4 | import prefix from '../../../utils/routePrefix' 5 | import { setTransition } from '../../../actions/hashUrl' 6 | import { fetchProfile, switchCollected } from '../../../actions' 7 | import styles from './styles.scss' 8 | import LinkToLogin from '../../common/LinkToLogin/LinkToLogin' 9 | import transformDate from '../../../utils/transformDate' 10 | import classnames from 'classnames' 11 | import { List, ListItem } from 'material-ui/List' 12 | import Divider from 'material-ui/Divider' 13 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' 14 | import fetch from 'isomorphic-fetch' 15 | 16 | @connect() 17 | class Content extends Component { 18 | constructor() { 19 | super() 20 | this.state = { 21 | isCollected: false 22 | } 23 | } 24 | componentWillMount() { 25 | this.update(this.props) 26 | } 27 | componentWillReceiveProps(newProps) { 28 | if (this.props.collectedTopics.userName !== newProps.collectedTopics.userName) { 29 | this.update(newProps); 30 | } 31 | } 32 | update(props) { 33 | const { article, login, collectedTopics } = props 34 | if (login.succeed && collectedTopics.length !== 0) { 35 | let isCollected = collectedTopics.some(topic => { 36 | return article.id === topic.id 37 | }) 38 | this.setState({ 39 | isCollected: isCollected 40 | }) 41 | } 42 | } 43 | render() { 44 | const { article, dispatch, fetchProfile, login, collectedTopics, profile } = this.props 45 | return ( 46 | 47 | 48 |
49 | 50 |
51 |
52 |
53 | {article.author.loginname} 54 |
55 |
56 | { 57 | this.props.dispatch(setTransition({ transition: 'up' })) 58 | if (profile.loginname !== article.author.loginname) { 59 | dispatch(fetchProfile(article.author.loginname)) 60 | } 61 | }}> 62 | {article.author.loginname} 63 | 64 | 发表于{transformDate(article.create_at)} 65 |
66 |
67 | {login.succeed && 68 | 69 | 收藏 70 | { 74 | this.setState({ 75 | isCollected: !this.state.isCollected 76 | }) 77 | dispatch(switchCollected(this.state.isCollected, login.accessToken, article.id)) 78 | // fetch(`https://cnodejs.org/api/v1/topic_collect/${this.state.isCollected?'de_collect':'collect'}`, { 79 | // method: 'POST', 80 | // headers: { 81 | // "Content-Type": "application/x-www-form-urlencoded" 82 | // }, 83 | // body: `accesstoken=${login.accessToken}&topic_Id=${article.id}` 84 | // }) 85 | }}> 86 | 87 | } 88 | 89 | 90 | {article.visit_count} 91 | 92 | 93 | 94 | {article.reply_count} 95 | 96 |
97 | 98 |
99 |
100 |
101 | 102 |
{article.title}
103 |
104 | 105 | 106 |
107 |
108 |
109 |
110 |
111 |
112 | 113 |
114 | ) 115 | } 116 | 117 | } 118 | 119 | export default Content 120 | -------------------------------------------------------------------------------- /src/styles/iconfont/demo_unicode.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | IconFont 7 | 8 | 9 | 29 | 30 | 31 |
32 |

IconFont 图标

33 |
    34 | 35 |
  • 36 | 37 |
    喜爱
    38 |
    &#xe600;
    39 |
  • 40 | 41 |
  • 42 | 43 |
    44 |
    &#xe610;
    45 |
  • 46 | 47 |
  • 48 | 49 |
    用户
    50 |
    &#xe60f;
    51 |
  • 52 | 53 |
  • 54 | 55 |
    返回
    56 |
    &#xe611;
    57 |
  • 58 | 59 |
  • 60 | 61 |
    信息
    62 |
    &#xe617;
    63 |
  • 64 | 65 |
  • 66 | 67 |
    回复
    68 |
    &#xe63f;
    69 |
  • 70 | 71 |
  • 72 | 73 |
    查看过
    74 |
    &#xe6ae;
    75 |
  • 76 | 77 |
78 |

unicode引用

79 |
80 | 81 |

unicode是字体在网页端最原始的应用方式,特点是:

82 |
    83 |
  • 兼容性最好,支持ie6+,及所有现代浏览器。
  • 84 |
  • 支持按字体的方式去动态调整图标大小,颜色等等。
  • 85 |
  • 但是因为是字体,所以不支持多色。只能使用平台里单色的图标,就算项目里有多色图标也会自动去色。
  • 86 |
87 |
88 |

注意:新版iconfont支持多色图标,这些多色图标在unicode模式下将不能使用,如果有需求建议使用symbol的引用方式

89 |
90 |

unicode使用步骤如下:

91 |

第一步:拷贝项目下面生成的font-face

92 |
@font-face {
 93 |   font-family: 'iconfont';
 94 |   src: url('iconfont.eot');
 95 |   src: url('iconfont.eot?#iefix') format('embedded-opentype'),
 96 |   url('iconfont.woff') format('woff'),
 97 |   url('iconfont.ttf') format('truetype'),
 98 |   url('iconfont.svg#iconfont') format('svg');
 99 | }
100 | 
101 |

第二步:定义使用iconfont的样式

102 |
.iconfont{
103 |   font-family:"iconfont" !important;
104 |   font-size:16px;font-style:normal;
105 |   -webkit-font-smoothing: antialiased;
106 |   -webkit-text-stroke-width: 0.2px;
107 |   -moz-osx-font-smoothing: grayscale;
108 | }
109 | 
110 |

第三步:挑选相应图标并获取字体编码,应用于页面

111 |
<i class="iconfont">&#x33;</i>
112 | 113 |
114 |

"iconfont"是你项目下的font-family。可以通过编辑项目查看,默认是"iconfont"。

115 |
116 |
117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /src/styles/iconfont/demo_symbol.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | IconFont 7 | 8 | 9 | 10 | 24 | 25 | 26 |
27 |

IconFont 图标

28 |
    29 | 30 |
  • 31 | 34 |
    喜爱
    35 |
    #icon-xiai
    36 |
  • 37 | 38 |
  • 39 | 42 |
    43 |
    #icon-ding
    44 |
  • 45 | 46 |
  • 47 | 50 |
    用户
    51 |
    #icon-user
    52 |
  • 53 | 54 |
  • 55 | 58 |
    返回
    59 |
    #icon-back
    60 |
  • 61 | 62 |
  • 63 | 66 |
    信息
    67 |
    #icon-informatiom
    68 |
  • 69 | 70 |
  • 71 | 74 |
    回复
    75 |
    #icon-huifu
    76 |
  • 77 | 78 |
  • 79 | 82 |
    查看过
    83 |
    #icon-chakanguo
    84 |
  • 85 | 86 |
87 | 88 | 89 |

symbol引用

90 |
91 | 92 |

这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇文章 93 | 这种用法其实是做了一个svg的集合,与另外两种相比具有如下特点:

94 |
    95 |
  • 支持多色图标了,不再受单色限制。
  • 96 |
  • 通过一些技巧,支持像字体那样,通过font-size,color来调整样式。
  • 97 |
  • 兼容性较差,支持 ie9+,及现代浏览器。
  • 98 |
  • 浏览器渲染svg的性能一般,还不如png。
  • 99 |
100 |

使用步骤如下:

101 |

第一步:引入项目下面生成的symbol代码:

102 |
<script src="./iconfont.js"></script>
103 |

第二步:加入通用css代码(引入一次就行):

104 |
<style type="text/css">
105 | .icon {
106 |    width: 1em; height: 1em;
107 |    vertical-align: -0.15em;
108 |    fill: currentColor;
109 |    overflow: hidden;
110 | }
111 | </style>
112 |

第三步:挑选相应图标并获取类名,应用于页面:

113 |
<svg class="icon" aria-hidden="true">
114 |   <use xlink:href="#icon-xxx"></use>
115 | </svg>
116 |         
117 |
118 | 119 | 120 | -------------------------------------------------------------------------------- /src/components/Message/Content/Content.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import transformDate from '../../../utils/transformDate' 4 | import styles from './styles.scss' 5 | import classnames from 'classnames' 6 | import { Link } from 'react-router-dom' 7 | import { setTransition } from '../../../actions/hashUrl' 8 | import prefix from '../../../utils/routePrefix' 9 | import { Tabs, Tab } from 'material-ui/Tabs'; 10 | import SwipeableViews from 'react-swipeable-views'; 11 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 12 | import { List, ListItem } from 'material-ui/List'; 13 | import Divider from 'material-ui/Divider'; 14 | import Avatar from 'material-ui/Avatar'; 15 | import RaisedButton from 'material-ui/RaisedButton'; 16 | import Dialog from '../../common/Dialog' 17 | import CircleLoading from '../../common/CircleLoading' 18 | import { markAllMessages, fetchMessage } from '../../../actions' 19 | 20 | @connect() 21 | class Content extends Component { 22 | constructor(props) { 23 | super(props); 24 | this.state = { 25 | slideIndex: 0, 26 | isOpen: false, 27 | isUpdating: false 28 | } 29 | } 30 | 31 | handleChange = (value) => { 32 | this.setState({ 33 | slideIndex: value, 34 | }); 35 | }; 36 | markMessages = () => { 37 | let { dispatch, login } = this.props 38 | let accessToken = login.accessToken 39 | dispatch(markAllMessages(accessToken)) 40 | this.setState({ 41 | isUpdating: true 42 | }) 43 | } 44 | close = () => { 45 | this.setState({ 46 | isOpen: false 47 | }) 48 | } 49 | componentWillUpdate(newProps) { 50 | let { dispatch, login, isMarked, hasNotReadMessage } = newProps 51 | let accessToken = login.accessToken 52 | if (isMarked && isMarked !== this.props.isMarked) { 53 | dispatch(fetchMessage(accessToken)) 54 | } 55 | if (isMarked && hasNotReadMessage.length === 0 && 56 | hasNotReadMessage.length !== this.props.hasNotReadMessage.length) { 57 | this.setState({ 58 | isUpdating: false 59 | }) 60 | } 61 | } 62 | render() { 63 | const { dispatch, hasNotReadMessage, hasReadMessage, article, fetchArticle } = this.props 64 | return ( 65 |
66 | {this.state.isUpdating &&
} 67 | {!this.state.isUpdating && 68 |
69 | 70 | 未读消息:{hasNotReadMessage && hasNotReadMessage.length}} value={0} /> 71 | 已读消息:{hasReadMessage && hasReadMessage.length}} value={1} /> 72 | 73 | 74 |
75 | {hasNotReadMessage && hasNotReadMessage.length === 0 && 76 |
77 |
暂无未读消息
78 |
79 | } 80 | {hasNotReadMessage.length > 0 && 81 |
82 | 83 | {hasNotReadMessage.map((msg, index) => 84 | { 85 | dispatch(setTransition({ transition: 'up' })) 86 | if (!article[msg.topic.id]) { 87 | dispatch(fetchArticle(msg.topic.id)) 88 | } else if (article.currentTopicId !== topic.id) { 89 | dispatch(fetchArticle(msg.topic.id, false)) 90 | } 91 | }}> 92 | } 94 | primaryText={msg.author.loginname} 95 | secondaryText={ 96 |
97 |
98 |

99 | 来自:{msg.topic.title} 100 | {transformDate(msg.reply.create_at)} 101 |

102 |
103 | } 104 | secondaryTextLines={2} 105 | /> 106 | 107 | )} 108 |
109 |
110 | { 111 | this.setState({ 112 | isOpen: true 113 | }) 114 | }} /> 115 |
116 | 117 | 118 | 是否将所有未读消息标记为已读? 119 | 120 |
121 | } 122 |
123 |
124 | {hasReadMessage.length === 0 && 125 |
您还没有查看过任何消息哦
126 | } 127 | {hasReadMessage.length > 0 && 128 | 129 | {hasReadMessage.map((msg, index) => 130 | { 131 | dispatch(setTransition({ transition: 'up' })) 132 | if (!article[msg.topic.id]) { 133 | dispatch(fetchArticle(msg.topic.id)) 134 | } else if (article.currentTopicId !== topic.id) { 135 | dispatch(fetchArticle(msg.topic.id, false)) 136 | } 137 | }}> 138 | } 140 | primaryText={msg.author.loginname} 141 | secondaryText={ 142 |
143 |
144 |

145 | 来自:{msg.topic.title} 146 | {transformDate(msg.reply.create_at)} 147 |

148 |
149 | } 150 | secondaryTextLines={2} 151 | /> 152 | 153 | 154 | )} 155 |
} 156 |
157 |
158 |
159 |
} 160 |
161 | ); 162 | } 163 | 164 | } 165 | 166 | export default Content 167 | 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Route, Router, Redirect, Switch, withRouter } from 'react-router-dom' 3 | import { connect } from 'react-redux' 4 | import createHistory from 'history/createHashHistory' 5 | import lazyLoadComponent from 'lazy-load-component' 6 | import App from './containers/App' 7 | import HomePage from './containers/HomePage' 8 | import Article from './containers/Article' 9 | import Message from './containers/Message' 10 | import Login from './containers/Login' 11 | import Profile from './containers/Profile' 12 | import PublishTopic from './containers/PublishTopic' 13 | import prefix from './utils/routePrefix' 14 | import getSize from './utils/getSize' 15 | import { setHashUrl, setTransition } from './actions/hashUrl' 16 | // import { clearUserInfo } from './actions/login' 17 | import { clearError } from './actions/fetchError' 18 | import { CSSTransition, TransitionGroup, Transition } from 'react-transition-group' 19 | 20 | const history = createHistory() 21 | 22 | // const Article = lazyLoadComponent(() => import(/*webpackChunkName:"Article" */'./containers/Article')) 23 | // const Message = lazyLoadComponent(() => import(/*webpackChunkName:"Message" */'./containers/Message')) 24 | // const Login = lazyLoadComponent(() => import(/*webpackChunkName:"Login" */'./containers/Login')) 25 | // const Profile = lazyLoadComponent(() => import(/*webpackChunkName:"Profile" */'./containers/Profile')) 26 | // const PublishTopic = lazyLoadComponent(() => import(/*webpackChunkName:"PublishTopic" */'./containers/PublishTopic')) 27 | 28 | 29 | 30 | 31 | 32 | 33 | @connect(store => ({ store })) 34 | class Routes extends Component { 35 | constructor() { 36 | super() 37 | this.state = {} 38 | } 39 | 40 | hashChange = (ev) => { 41 | if (this.props.store.hashUrl.oldURL != '/') { 42 | this.setState({ overflow: 'hidden' }) 43 | setTimeout(() => this.setState({ overflow: 'visible' }), 500) 44 | } 45 | 46 | const dispatch = this.props.dispatch 47 | let hashUrl = null; 48 | if (ev.oldURL) { 49 | hashUrl = { oldUrl: ev.oldURL.split('#')[1], currentUrl: ev.newURL.split('#')[1] } 50 | } else { 51 | this.oldUrl = this.currentUrl 52 | this.currentUrl = window.location.href.split('#')[1] 53 | hashUrl = { oldUrl: this.oldUrl, currentUrl: this.currentUrl } 54 | } 55 | dispatch(setHashUrl(hashUrl)) 56 | 57 | // if (this.props.hashUrl.transition != 'none') { 58 | // clearTimeout(this.transitionTimeOut) 59 | // this.transitionTimeOut = setTimeout(() => { 60 | // dispatch(setTransition({ transition: 'none' })) 61 | // }, 50) 62 | // } 63 | } 64 | 65 | oldUrl = '/' 66 | currentUrl = '/' 67 | 68 | // changeTransition = (transition) => { 69 | // this.setState({ transition: transition }) 70 | // } 71 | 72 | componentWillMount() { 73 | let dispatch = this.props.dispatch 74 | window.myDispatch = dispatch 75 | window.width = getSize().windowW 76 | window.height = getSize().windowH 77 | let menu = window.location.href.split('#')[1].split('/') 78 | if (menu[1]) { 79 | this.currentUrl = window.location.href.split('#')[1] 80 | dispatch(setHashUrl({ oldUrl: this.oldUrl, currentUrl: window.location.href.split('#')[1] })) 81 | } else { 82 | dispatch(setHashUrl({ oldUrl: this.oldUrl, currentUrl: this.currentUrl })) 83 | } 84 | window.addEventListener('hashchange', this.hashChange) 85 | // 由于头部组件fix定位,在路由切换时,width:100%在手机上的判定会有问题,暂时采取全局变量储存页面加载时的宽度 86 | // window.width = document.getElementById('root').offsetWidth 87 | // console.log('****************',document.getElementById('root').offsetWidth) 88 | } 89 | 90 | saveState = () => { 91 | let store = this.props.store 92 | sessionStorage.setItem('store', JSON.stringify(store)) 93 | } 94 | 95 | 96 | 97 | componentWillUpdate(nextProps) { 98 | // if (this.props.store.hashUrl.oldUrl == nextProps.store.hashUrl.currentUrl) { 99 | // this.props.dispatch(setTransition({ transition: 'left' })) 100 | // } 101 | } 102 | 103 | render() { 104 | this.transition = this.props.store.hashUrl.transition 105 | // let transition = this.props.store.hashUrl.transition 106 | return ( 107 | 108 | ( 109 |
110 | 111 | this.enterCN = `${this.transition}-enter`} 113 | onEntering={() => this.enterCN = `${this.transition}-enter ${this.transition}-enter-active`} 114 | onEntered={() => this.enterCN = ``} 115 | onExit={() => this.exitCN = `${this.transition}-exit`} 116 | onExiting={() => this.exitCN = `${this.transition}-exit ${this.transition}-exit-active`} 117 | onExited={() => this.exitCN = ``} 118 | > 119 | {(status) => ( 120 |
121 | 122 | } /> 123 | } /> 124 |
} /> 125 | } /> 126 | } /> 127 | } /> 128 | } /> 129 | 130 |
131 | )} 132 |
133 |
134 |
135 | )} /> 136 |
137 | ) 138 | } 139 | 140 | 141 | componentDidMount() { 142 | window.width = document.getElementById('root').offsetWidth 143 | window.addEventListener('beforeunload', this.saveState) 144 | } 145 | 146 | componentWillUnmount() { 147 | window.removeEventListener('beforeunload', this.saveState) 148 | window.removeEventListener('hashchange', () => { }) 149 | } 150 | 151 | 152 | } 153 | 154 | export default Routes -------------------------------------------------------------------------------- /src/styles/iconfont/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 | var script = function() { 53 | var scripts = document.getElementsByTagName('script') 54 | return scripts[scripts.length - 1] 55 | }() 56 | var shouldInjectCss = script.getAttribute("data-injectcss") 57 | 58 | /** 59 | * document ready 60 | */ 61 | var ready = function(fn) { 62 | if (document.addEventListener) { 63 | if (~["complete", "loaded", "interactive"].indexOf(document.readyState)) { 64 | setTimeout(fn, 0) 65 | } else { 66 | var loadFn = function() { 67 | document.removeEventListener("DOMContentLoaded", loadFn, false) 68 | fn() 69 | } 70 | document.addEventListener("DOMContentLoaded", loadFn, false) 71 | } 72 | } else if (document.attachEvent) { 73 | IEContentLoaded(window, fn) 74 | } 75 | 76 | function IEContentLoaded(w, fn) { 77 | var d = w.document, 78 | done = false, 79 | // only fire once 80 | init = function() { 81 | if (!done) { 82 | done = true 83 | fn() 84 | } 85 | } 86 | // polling for no errors 87 | var polling = function() { 88 | try { 89 | // throws errors until after ondocumentready 90 | d.documentElement.doScroll('left') 91 | } catch (e) { 92 | setTimeout(polling, 50) 93 | return 94 | } 95 | // no errors, fire 96 | 97 | init() 98 | }; 99 | 100 | polling() 101 | // trying to always fire before onload 102 | d.onreadystatechange = function() { 103 | if (d.readyState == 'complete') { 104 | d.onreadystatechange = null 105 | init() 106 | } 107 | } 108 | } 109 | } 110 | 111 | /** 112 | * Insert el before target 113 | * 114 | * @param {Element} el 115 | * @param {Element} target 116 | */ 117 | 118 | var before = function(el, target) { 119 | target.parentNode.insertBefore(el, target) 120 | } 121 | 122 | /** 123 | * Prepend el to target 124 | * 125 | * @param {Element} el 126 | * @param {Element} target 127 | */ 128 | 129 | var prepend = function(el, target) { 130 | if (target.firstChild) { 131 | before(el, target.firstChild) 132 | } else { 133 | target.appendChild(el) 134 | } 135 | } 136 | 137 | function appendSvg() { 138 | var div, svg 139 | 140 | div = document.createElement('div') 141 | div.innerHTML = svgSprite 142 | svgSprite = null 143 | svg = div.getElementsByTagName('svg')[0] 144 | if (svg) { 145 | svg.setAttribute('aria-hidden', 'true') 146 | svg.style.position = 'absolute' 147 | svg.style.width = 0 148 | svg.style.height = 0 149 | svg.style.overflow = 'hidden' 150 | prepend(svg, document.body) 151 | } 152 | } 153 | 154 | if (shouldInjectCss && !window.__iconfont__svg__cssinject__) { 155 | window.__iconfont__svg__cssinject__ = true 156 | try { 157 | document.write(""); 158 | } catch (e) { 159 | console && console.log(e) 160 | } 161 | } 162 | 163 | ready(appendSvg) 164 | 165 | 166 | })(window) -------------------------------------------------------------------------------- /src/styles/iconfont/demo.css: -------------------------------------------------------------------------------- 1 | *{margin: 0;padding: 0;list-style: none;} 2 | /* 3 | KISSY CSS Reset 4 | 理念:1. reset 的目的不是清除浏览器的默认样式,这仅是部分工作。清除和重置是紧密不可分的。 5 | 2. reset 的目的不是让默认样式在所有浏览器下一致,而是减少默认样式有可能带来的问题。 6 | 3. reset 期望提供一套普适通用的基础样式。但没有银弹,推荐根据具体需求,裁剪和修改后再使用。 7 | 特色:1. 适应中文;2. 基于最新主流浏览器。 8 | 维护:玉伯, 正淳 9 | */ 10 | 11 | /** 清除内外边距 **/ 12 | body, h1, h2, h3, h4, h5, h6, hr, p, blockquote, /* structural elements 结构元素 */ 13 | dl, dt, dd, ul, ol, li, /* list elements 列表元素 */ 14 | pre, /* text formatting elements 文本格式元素 */ 15 | form, fieldset, legend, button, input, textarea, /* form elements 表单元素 */ 16 | th, td /* table elements 表格元素 */ { 17 | margin: 0; 18 | padding: 0; 19 | } 20 | 21 | /** 设置默认字体 **/ 22 | body, 23 | button, input, select, textarea /* for ie */ { 24 | font: 12px/1.5 tahoma, arial, \5b8b\4f53, sans-serif; 25 | } 26 | h1, h2, h3, h4, h5, h6 { font-size: 100%; } 27 | address, cite, dfn, em, var { font-style: normal; } /* 将斜体扶正 */ 28 | code, kbd, pre, samp { font-family: courier new, courier, monospace; } /* 统一等宽字体 */ 29 | small { font-size: 12px; } /* 小于 12px 的中文很难阅读,让 small 正常化 */ 30 | 31 | /** 重置列表元素 **/ 32 | ul, ol { list-style: none; } 33 | 34 | /** 重置文本格式元素 **/ 35 | a { text-decoration: none; } 36 | a:hover { text-decoration: underline; } 37 | 38 | 39 | /** 重置表单元素 **/ 40 | legend { color: #000; } /* for ie6 */ 41 | fieldset, img { border: 0; } /* img 搭车:让链接里的 img 无边框 */ 42 | button, input, select, textarea { font-size: 100%; } /* 使得表单元素在 ie 下能继承字体大小 */ 43 | /* 注:optgroup 无法扶正 */ 44 | 45 | /** 重置表格元素 **/ 46 | table { border-collapse: collapse; border-spacing: 0; } 47 | 48 | /* 清除浮动 */ 49 | .ks-clear:after, .clear:after { 50 | content: '\20'; 51 | display: block; 52 | height: 0; 53 | clear: both; 54 | } 55 | .ks-clear, .clear { 56 | *zoom: 1; 57 | } 58 | 59 | .main { 60 | padding: 30px 100px; 61 | width: 960px; 62 | margin: 0 auto; 63 | } 64 | .main h1{font-size:36px; color:#333; text-align:left;margin-bottom:30px; border-bottom: 1px solid #eee;} 65 | 66 | .helps{margin-top:40px;} 67 | .helps pre{ 68 | padding:20px; 69 | margin:10px 0; 70 | border:solid 1px #e7e1cd; 71 | background-color: #fffdef; 72 | overflow: auto; 73 | } 74 | 75 | .icon_lists{ 76 | width: 100% !important; 77 | 78 | } 79 | 80 | .icon_lists li{ 81 | float:left; 82 | width: 100px; 83 | height:180px; 84 | text-align: center; 85 | list-style: none !important; 86 | } 87 | .icon_lists .icon{ 88 | font-size: 42px; 89 | line-height: 100px; 90 | margin: 10px 0; 91 | color:#333; 92 | -webkit-transition: font-size 0.25s ease-out 0s; 93 | -moz-transition: font-size 0.25s ease-out 0s; 94 | transition: font-size 0.25s ease-out 0s; 95 | 96 | } 97 | .icon_lists .icon:hover{ 98 | font-size: 100px; 99 | } 100 | 101 | 102 | 103 | .markdown { 104 | color: #666; 105 | font-size: 14px; 106 | line-height: 1.8; 107 | } 108 | 109 | .highlight { 110 | line-height: 1.5; 111 | } 112 | 113 | .markdown img { 114 | vertical-align: middle; 115 | max-width: 100%; 116 | } 117 | 118 | .markdown h1 { 119 | color: #404040; 120 | font-weight: 500; 121 | line-height: 40px; 122 | margin-bottom: 24px; 123 | } 124 | 125 | .markdown h2, 126 | .markdown h3, 127 | .markdown h4, 128 | .markdown h5, 129 | .markdown h6 { 130 | color: #404040; 131 | margin: 1.6em 0 0.6em 0; 132 | font-weight: 500; 133 | clear: both; 134 | } 135 | 136 | .markdown h1 { 137 | font-size: 28px; 138 | } 139 | 140 | .markdown h2 { 141 | font-size: 22px; 142 | } 143 | 144 | .markdown h3 { 145 | font-size: 16px; 146 | } 147 | 148 | .markdown h4 { 149 | font-size: 14px; 150 | } 151 | 152 | .markdown h5 { 153 | font-size: 12px; 154 | } 155 | 156 | .markdown h6 { 157 | font-size: 12px; 158 | } 159 | 160 | .markdown hr { 161 | height: 1px; 162 | border: 0; 163 | background: #e9e9e9; 164 | margin: 16px 0; 165 | clear: both; 166 | } 167 | 168 | .markdown p, 169 | .markdown pre { 170 | margin: 1em 0; 171 | } 172 | 173 | .markdown > p, 174 | .markdown > blockquote, 175 | .markdown > .highlight, 176 | .markdown > ol, 177 | .markdown > ul { 178 | width: 80%; 179 | } 180 | 181 | .markdown ul > li { 182 | list-style: circle; 183 | } 184 | 185 | .markdown > ul li, 186 | .markdown blockquote ul > li { 187 | margin-left: 20px; 188 | padding-left: 4px; 189 | } 190 | 191 | .markdown > ul li p, 192 | .markdown > ol li p { 193 | margin: 0.6em 0; 194 | } 195 | 196 | .markdown ol > li { 197 | list-style: decimal; 198 | } 199 | 200 | .markdown > ol li, 201 | .markdown blockquote ol > li { 202 | margin-left: 20px; 203 | padding-left: 4px; 204 | } 205 | 206 | .markdown code { 207 | margin: 0 3px; 208 | padding: 0 5px; 209 | background: #eee; 210 | border-radius: 3px; 211 | } 212 | 213 | .markdown pre { 214 | border-radius: 6px; 215 | background: #f7f7f7; 216 | padding: 20px; 217 | } 218 | 219 | .markdown pre code { 220 | border: none; 221 | background: #f7f7f7; 222 | margin: 0; 223 | } 224 | 225 | .markdown strong, 226 | .markdown b { 227 | font-weight: 600; 228 | } 229 | 230 | .markdown > table { 231 | border-collapse: collapse; 232 | border-spacing: 0px; 233 | empty-cells: show; 234 | border: 1px solid #e9e9e9; 235 | width: 95%; 236 | margin-bottom: 24px; 237 | } 238 | 239 | .markdown > table th { 240 | white-space: nowrap; 241 | color: #333; 242 | font-weight: 600; 243 | 244 | } 245 | 246 | .markdown > table th, 247 | .markdown > table td { 248 | border: 1px solid #e9e9e9; 249 | padding: 8px 16px; 250 | text-align: left; 251 | } 252 | 253 | .markdown > table th { 254 | background: #F7F7F7; 255 | } 256 | 257 | .markdown blockquote { 258 | font-size: 90%; 259 | color: #999; 260 | border-left: 4px solid #e9e9e9; 261 | padding-left: 0.8em; 262 | margin: 1em 0; 263 | font-style: italic; 264 | } 265 | 266 | .markdown blockquote p { 267 | margin: 0; 268 | } 269 | 270 | .markdown .anchor { 271 | opacity: 0; 272 | transition: opacity 0.3s ease; 273 | margin-left: 8px; 274 | } 275 | 276 | .markdown .waiting { 277 | color: #ccc; 278 | } 279 | 280 | .markdown h1:hover .anchor, 281 | .markdown h2:hover .anchor, 282 | .markdown h3:hover .anchor, 283 | .markdown h4:hover .anchor, 284 | .markdown h5:hover .anchor, 285 | .markdown h6:hover .anchor { 286 | opacity: 1; 287 | display: inline-block; 288 | } 289 | 290 | .markdown > br, 291 | .markdown > p > br { 292 | clear: both; 293 | } 294 | 295 | 296 | .hljs { 297 | display: block; 298 | background: white; 299 | padding: 0.5em; 300 | color: #333333; 301 | overflow-x: auto; 302 | } 303 | 304 | .hljs-comment, 305 | .hljs-meta { 306 | color: #969896; 307 | } 308 | 309 | .hljs-string, 310 | .hljs-variable, 311 | .hljs-template-variable, 312 | .hljs-strong, 313 | .hljs-emphasis, 314 | .hljs-quote { 315 | color: #df5000; 316 | } 317 | 318 | .hljs-keyword, 319 | .hljs-selector-tag, 320 | .hljs-type { 321 | color: #a71d5d; 322 | } 323 | 324 | .hljs-literal, 325 | .hljs-symbol, 326 | .hljs-bullet, 327 | .hljs-attribute { 328 | color: #0086b3; 329 | } 330 | 331 | .hljs-section, 332 | .hljs-name { 333 | color: #63a35c; 334 | } 335 | 336 | .hljs-tag { 337 | color: #333333; 338 | } 339 | 340 | .hljs-title, 341 | .hljs-attr, 342 | .hljs-selector-id, 343 | .hljs-selector-class, 344 | .hljs-selector-attr, 345 | .hljs-selector-pseudo { 346 | color: #795da3; 347 | } 348 | 349 | .hljs-addition { 350 | color: #55a532; 351 | background-color: #eaffea; 352 | } 353 | 354 | .hljs-deletion { 355 | color: #bd2c00; 356 | background-color: #ffecec; 357 | } 358 | 359 | .hljs-link { 360 | text-decoration: underline; 361 | } 362 | 363 | pre{ 364 | background: #fff; 365 | } 366 | 367 | 368 | 369 | 370 | 371 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack') 3 | var autoprefixer = require('autoprefixer') 4 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | var redBox = require('redbox-react') 6 | var host = 'localhost' 7 | var port = '5678' 8 | 9 | //判断当前运行环境是开发模式还是生产模式 10 | const nodeEnv = process.env.NODE_ENV || 'development'; 11 | const isPro = nodeEnv !== 'development'; 12 | 13 | const os = require('os').platform() 14 | const network = require('os').networkInterfaces() 15 | 16 | console.log("当前运行系统:", os) 17 | console.log("当前运行环境:", isPro ? 'production' : 'development') 18 | 19 | // Object.keys(network).forEach(item => { 20 | // if (network[item] && network[item][0] && network[item][0].internal == false && ['以太网', '本地连接'].includes(item)) { 21 | // network[item].forEach(ips => { 22 | // if (ips.family == 'IPv4') { 23 | // host = ips.address 24 | // } 25 | // }) 26 | // } 27 | // }) 28 | 29 | 30 | // var fs = require("fs") 31 | // var data = `export const os = '${os}';export const host = '${host}'` 32 | // var writerStream = fs.createWriteStream('src/utils/getOS.js') 33 | // writerStream.write(data, 'UTF8') 34 | // writerStream.end() 35 | // writerStream.on('finish', () => { 36 | // console.log("成功写入:src/utils/getOS.js") 37 | // }) 38 | 39 | // writerStream.on('error', err => { 40 | // console.log(err.stack) 41 | // }) 42 | 43 | const moduleCss = new ExtractTextPlugin('module.css') 44 | const globalCss = new ExtractTextPlugin('global.css') 45 | const uiCss = new ExtractTextPlugin('ui.css') 46 | 47 | var plugins = [ 48 | globalCss, 49 | moduleCss, 50 | uiCss, 51 | // 这个插件的作用是将打包的js文件拆分,默认拆分出2个 52 | new webpack.optimize.CommonsChunkPlugin({ 53 | // 这个vendor就是打包后的文件名字,需要写在index.html里面 54 | name: 'vendor', 55 | minChunks: function (module) { 56 | // 该配置假定你引入的 vendor 存在于 node_modules 目录中 57 | return module.context && module.context.indexOf('node_modules') !== -1; 58 | } 59 | }), 60 | //DefinePlugin 在原始的源码中执行查找和替换操作,在导入的代码中, 61 | // 任何出现 process.env.NODE_ENV的地方都会被替换为nodeEnv变量转成的json字符串 62 | new webpack.DefinePlugin({ 63 | // 定义全局变量 64 | 'process.env': { 65 | 'NODE_ENV': JSON.stringify(nodeEnv) 66 | } 67 | }) 68 | ] 69 | 70 | var app = ['./index.js'] 71 | 72 | if (isPro) { 73 | plugins.push( 74 | new webpack.optimize.UglifyJsPlugin({ 75 | sourceMap: true, 76 | comments: false, 77 | compress: { 78 | warnings: false, 79 | // 去掉debugger和console 80 | drop_debugger: true, 81 | drop_console: true 82 | } 83 | }) 84 | ) 85 | } else { 86 | app.unshift('react-hot-loader/patch', `webpack-dev-server/client?http://${host}:${port}`, 'webpack/hot/only-dev-server') 87 | plugins.push( 88 | new webpack.HotModuleReplacementPlugin(), 89 | new webpack.NamedModulesPlugin(), 90 | new webpack.NoEmitOnErrorsPlugin() 91 | ) 92 | } 93 | 94 | // // 开启服务器后不能用相对路径 95 | // var outpath = path.resolve(__dirname, 'build'); 96 | 97 | module.exports = { 98 | context: path.resolve(__dirname, 'src'), 99 | devtool: isPro ? 'source-map' : 'inline-source-map', 100 | entry: { 101 | app: app 102 | }, 103 | output: { 104 | filename: '[name].js', 105 | path: path.join(__dirname, 'build'), 106 | publicPath: isPro ? './build/' : '/build/', 107 | chunkFilename: '[name].js' 108 | }, 109 | module: { 110 | rules: [ 111 | { 112 | test: /\.js$/, 113 | exclude: /node_modules/, 114 | use: ['babel-loader?cacheDirectory=true'] 115 | }, 116 | // { 117 | // test: /\.scss$/, 118 | // // exclude: [nodeModulesPath]用来排除不处理的目录 119 | // exclude: path.resolve(__dirname, 'src/styles'), 120 | // // webpack配置loader时是可以不写loader的后缀明-loader,因此css-loader可以写为css 121 | // // css-loader使你能够使用类似@import 和 url(...)的方法实现 require()的功能 122 | // // style-loader将所有的计算后的样式通过