├── .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 |
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 |
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 |
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 |
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 |

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 |
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 |
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 |
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 |
81 |
82 |
88 |
89 |
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 |
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 |

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 |
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 | 
39 |
40 |
41 | -
42 |
43 |
顶
44 | 
45 |
46 |
47 | -
48 |
49 |
用户
50 | 
51 |
52 |
53 | -
54 |
55 |
返回
56 | 
57 |
58 |
59 | -
60 |
61 |
信息
62 | 
63 |
64 |
65 | -
66 |
67 |
回复
68 | 
69 |
70 |
71 | -
72 |
73 |
查看过
74 | 
75 |
76 |
77 |
78 |
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">3</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 |
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 |
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 |
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 = ''
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将所有的计算后的样式通过