Repository: lumia2046/cnode Branch: master Commit: a1a4487dbf23 Files: 71 Total size: 138.9 KB Directory structure: gitextract_305ial8u/ ├── .babelrc ├── .gitignore ├── README.md ├── index.html ├── package.json ├── redBoxBlackStyle.js ├── src/ │ ├── actions/ │ │ ├── fetchError.js │ │ └── hashUrl.js │ ├── actions.js │ ├── components/ │ │ ├── Article/ │ │ │ ├── Content/ │ │ │ │ ├── Content.js │ │ │ │ └── styles.scss │ │ │ └── Reply/ │ │ │ ├── Reply.js │ │ │ └── styles.scss │ │ ├── HomePage/ │ │ │ ├── Drawer/ │ │ │ │ ├── Drawer.js │ │ │ │ └── styles.scss │ │ │ ├── FloatingActionButton.js │ │ │ ├── Header/ │ │ │ │ ├── Header.js │ │ │ │ └── styles.scss │ │ │ └── Lists/ │ │ │ ├── Lists.js │ │ │ └── styles.scss │ │ ├── Message/ │ │ │ └── Content/ │ │ │ ├── Content.js │ │ │ └── styles.scss │ │ ├── PublishTopic/ │ │ │ └── Form/ │ │ │ ├── Form.js │ │ │ └── styles.scss │ │ └── common/ │ │ ├── AsyncContainer.js │ │ ├── CircleLoading.js │ │ ├── Dialog.js │ │ ├── Header/ │ │ │ ├── Header.js │ │ │ └── styles.scss │ │ ├── LinkToLogin/ │ │ │ ├── LinkToLogin.js │ │ │ └── styles.scss │ │ ├── Profile/ │ │ │ ├── Profile.js │ │ │ └── styles.scss │ │ ├── Snackbar.js │ │ └── react-pullrefresh.js │ ├── configureStore.js │ ├── containers/ │ │ ├── App.js │ │ ├── Article.js │ │ ├── HomePage.js │ │ ├── Login.js │ │ ├── Message.js │ │ ├── Profile.js │ │ └── PublishTopic.js │ ├── index.js │ ├── reducers/ │ │ ├── article.js │ │ ├── collectedTopics.js │ │ ├── fetchError.js │ │ ├── hashUrl.js │ │ ├── homePage.js │ │ ├── index.js │ │ ├── login.js │ │ ├── message.js │ │ ├── profile.js │ │ └── publishTopic.js │ ├── routes.js │ ├── styles/ │ │ ├── iconfont/ │ │ │ ├── demo.css │ │ │ ├── demo_fontclass.html │ │ │ ├── demo_symbol.html │ │ │ ├── demo_unicode.html │ │ │ ├── iconfont.css │ │ │ └── iconfont.js │ │ └── index.css │ └── utils/ │ ├── getOS.js │ ├── getPosition.js │ ├── getSize.js │ ├── getStrLength.js │ ├── myFetch.js │ ├── routePrefix.js │ ├── transformDate.js │ └── urlPrefix.js └── webpack.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ //babel配置文件,不需要做修改,因为都配置好了 { "presets": [ ["env", {"modules": false}], "react", "stage-0" ], "plugins": [ "react-hot-loader/babel", ["transform-runtime", { "helpers": false, "polyfill": false, "regenerator": true, "moduleName": "babel-runtime" }], // ['import', {libraryName: 'antd', style:true}], "transform-decorators-legacy", "transform-async-to-generator", "transform-do-expressions", "syntax-do-expressions", ["babel-plugin-react-transform", { transforms: [ { transform: 'react-transform-catch-errors', imports: ['react','redbox-react','redBoxBlackStyle'] } ], }] ] } ================================================ FILE: .gitignore ================================================ /node_modules/ ================================================ FILE: README.md ================================================ ## 项目简介 一个WebApp版的cnode客户端,项目采用react技术栈构建。组件选用的是[Material-UI](http://www.material-ui.com/),让界面更适合触控操作。 - 感谢来自[cnodejs论坛](https://cnodejs.org/)官方提供的api! ## 功能 - 首页列表,下拉时自动加载下一页,在顶端上拉刷新 - 主题详情,登陆后能够收藏,评论和点赞 - 消息提醒,能查看消息详情和清空所有未读消息 - 个人主页,包括最近参与,回复,以及收藏的主题 - 发表主题,成功后能跳转到相应主题页面 - 页面后退,能还原数据和滚动位置 ## 运用的技术主要有: - 采用react技术栈,通过Redux来管理页面状态,通过Router来设置页面路由 - 组件选用的是Material-UI,不再自己造轮子,既美观又能方便触控操作 - 使用react-route v4 和 react原生的react-transition-group v2 来实现路由切换动画 - 使用react-flip-move插件来实现list的加载动画 - 应用`isomorphic-fetch`库代替`XMLHttpRequest`实现网络请求 - 使用`PostCSS`对CSS进行预处理 - 通过`CSSModules`处理模块内部的类名 ## 预览 [DEMO](https://lumia2046.github.io/cnode/) ## 运行项目 ``` git clone https://github.com/lumia2046/cnode.git cd cnode npm install npm start 打开浏览器访问:http://localhost:5678 ``` ## 生产项目 ``` windows下 npm run build-win linux、mac下 npm run build-win ``` ================================================ FILE: index.html ================================================ Cnode
================================================ FILE: package.json ================================================ { "name": "cnode", "version": "2.0.0", "description": "", "main": "index.js", "scripts": { "start": "webpack-dev-server", "lint": "eslint src", "build-mac": "export NODE_ENV=production && webpack --progress --hide-modules --config webpack.config.js", "build-win": "set NODE_ENV=production && webpack --progress --hide-modules --config webpack.config.js", "test": "jest" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "babel-polyfill": "^6.20.0", "github-markdown-css": "^2.4.1", "history": "^4.6.1", "isomorphic-fetch": "^2.2.1", "lazy-load-component": "^1.1.2", "material-ui": "^0.20.0", "normalize.css": "^7.0.0", "pullhelper": "^1.1.8", "react": "^16.0.0", "react-dom": "^16.0.0", "react-flip-move": "^3.0.0", "react-modal": "^3.1.0", "react-motion": "^0.5.2", "react-redux": "^5.0.6", "react-router": "^4.2.0", "react-router-dom": "^4.1.2", "react-router-redux": "^4.0.8", "react-swipeable-views": "^0.12.9", "react-tap-event-plugin": "^3.0.2", "react-transition-group": "^2.2.1", "redux": "^3.6.0", "redux-logger": "^3.0.6", "redux-thunk": "^2.1.0" }, "devDependencies": { "autoprefixer": "^7.1.6", "babel-cli": "^6.24.1", "babel-core": "^6.25.0", "babel-loader": "^7.1.1", "babel-plugin-import": "^1.2.1", "babel-plugin-react-transform": "^3.0.0", "babel-plugin-transform-decorators-legacy": "^1.3.4", "babel-plugin-transform-runtime": "^6.23.0", "babel-polyfill": "^6.23.0", "babel-preset-env": "^1.6.1", "babel-preset-react": "^6.24.1", "babel-preset-react-hmre": "^1.1.1", "babel-preset-stage-0": "^6.16.0", "babel-preset-stage-1": "^6.16.0", "babel-preset-stage-2": "^6.17.0", "babel-preset-stage-3": "^6.17.0", "cross-env": "^5.1.1", "css-loader": "^0.28.7", "eventsource-polyfill": "^0.9.6", "express": "^4.13.3", "extract-text-webpack-plugin": "^3.0.2", "file-loader": "^1.1.5", "html-webpack-plugin": "^2.24.1", "node-sass": "^4.6.0", "open": "0.0.5", "postcss-loader": "^2.0.8", "postcss-scss": "^1.0.2", "react-hot-loader": "^4.0.0-beta.14", "react-transform-catch-errors": "^1.0.2", "redbox-react": "^1.3.3", "redux-devtools-extension": "^2.13.2", "rimraf": "^2.4.3", "sass-loader": "^6.0.6", "style-loader": "^0.19.0", "url-loader": "^0.6.2", "webpack": "^3.8.1", "webpack-dev-server": "^2.9.4", "moment": "^2.18.1" } } ================================================ FILE: redBoxBlackStyle.js ================================================ module.exports = { style: { redbox: { boxSizing: 'border-box', fontFamily: 'sans-serif', position: 'fixed', padding: 10, top: '0px', left: '0px', bottom: '0px', right: '0px', width: '100%', background: 'rgba(0, 0, 0, 0.75)', color: 'red', zIndex: 9999, textAlign: 'left', fontSize: '16px', lineHeight: 1.2 } } }; ================================================ FILE: src/actions/fetchError.js ================================================ import urlPrefix from '../utils/urlPrefix' export const FETCH_ERROR = 'FETCH_ERROR' export const CLEAR_ERROR = 'CLEAR_ERROR' export const FETCH_START = 'FETCH_START' export const FETCH_END = 'FETCH_END' export const fetchStart = () => ({ type: FETCH_START }) export const fetchEnd = () => ({ type: FETCH_END }) export const fetchError = e => ({ type: FETCH_ERROR, data: e }) export const clearError = () => ({ type: CLEAR_ERROR }) ================================================ FILE: src/actions/hashUrl.js ================================================ export const SET_TRANSITION = 'SET_TRANSITION' export const SET_HASH_URL = 'SET_HASH_URL' export const setHashUrl = (hashUrl) => { return { type:SET_HASH_URL, data:hashUrl } } export const setTransition = (transiton) => { return { type:SET_TRANSITION, data:transiton } } ================================================ FILE: src/actions.js ================================================ import fetch from 'isomorphic-fetch' export const REQUEST_TOPICS = 'REQUEST_TOPICS' export const RECEIVE_TOPICS = 'RECEIVE_TOPICS' export const SELECT_TAB= 'SELECT_TAB' export const RECORD_SCROLLT='RECORD_SCROLLT' export const REQUEST_ARTICLE = 'REQUEST_ARTICLE' export const RECEIVE_ARTICLE = 'RECEIVE_ARTICLE' export const CHANGE_CURRENT_TOPICID = 'CHANGE_CURRENT_TOPICID' export const CURRENT_ROUTER = 'CURRENT_ROUTER' export const LOGIN_SUCCESS = 'LOGIN_SUCCESS' export const LOGIN_FAILED = 'LOGIN_FAILED' export const LOGOUT = 'LOGOUT' export const REQUEST_PROFILE = 'REQUEST_PROFILE' export const RECEIVE_PROFILE = 'RECEIVE_PROFILE' export const SWITCH_SUPPORT = 'SWITCH_SUPPORT' export const FETCH_COMMENT = 'FETCH_COMMENT' export const SWITCH_COLLECTED = 'SWITCH_COLLECTED' export const RECORD_ARTICLE_SCROLLT = 'RECORD_ARTICLE_SCROLLT' export const PUBLISH_TOPIC = 'PUBLISH_TOPIC' export const FETCH_MESSAGE = 'FETCH_MESSAGE' export const MARK_ALL_MESSAGES = 'MARK_ALL_MESSAGES' export const GET_COLLECTED_TOPICS = 'GET_COLLECTED_TOPICS' // HomePage export const selectTab = tab => ({ type:SELECT_TAB, tab }) const requestTopics = tab => ({ type:REQUEST_TOPICS, tab }) const receiveTopics = (tab,topics,page,limit) => ({ type:RECEIVE_TOPICS, tab, topics, page, limit }) export const fetchTopics = (tab,page=1,limit=20) => { return dispatch => { dispatch(requestTopics(tab)) fetch(`https://cnodejs.org/api/v1/topics?tab=${tab}&page=${page}&limit=${limit}`) .then(response => response.json()) .then(json => dispatch(receiveTopics(tab,json.data,page,limit))) } } export const recordScrollT = (tab,scrollT) => { return ({ type:RECORD_SCROLLT, tab, scrollT }) } // Article const requestArticle = (topicId) => ({ type:REQUEST_ARTICLE, topicId }) const receiveArticle = (topicId,article) => ({ type:RECEIVE_ARTICLE, topicId, article }) const changeCurrentTopicId = topicId => ({ type:CHANGE_CURRENT_TOPICID, topicId }) export const recordArticleScrollT = (topicId,scrollT) => ({ type:RECORD_ARTICLE_SCROLLT, topicId, scrollT }) export const fetchArticle = (topicId,request=true) => { return dispatch => { if(request){ dispatch(requestArticle(topicId)) fetch(`https://cnodejs.org/api/v1/topic/${topicId}`) .then(response => response.json()) .then(json => dispatch(receiveArticle(topicId,json.data))) }else{ dispatch(changeCurrentTopicId(topicId)) } } } export const switchSupport = (accessToken,replyId,index) => { return dispatch => { fetch(`https://cnodejs.org/api/v1/reply/${replyId}/ups`, { method: 'POST', headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: `accesstoken=${accessToken}` }) .then(response => response.json()) .then(json => dispatch({ type:SWITCH_SUPPORT, replyId, index, success:json.success, action:json.action })) } } export const fetchComment = (accessToken,topicId,content,replyId) => { return dispatch => { const postConent = replyId ? `accesstoken=${accessToken}&content=${content}&replyId=${replyId}`:`accesstoken=${accessToken}&content=${content}` fetch(`https://cnodejs.org/api/v1/topic/${topicId}/replies`, { method: 'POST', headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: postConent }) .then(response => response.json()) .then(json => dispatch({ type:FETCH_COMMENT, success:json.success, replyId:json.reply_id })) } } export const switchCollected = (isCollected,accessToken,articleId) => { return dispatch => { fetch(`https://cnodejs.org/api/v1/topic_collect/${isCollected?'de_collect':'collect'}`, { method: 'POST', headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: `accesstoken=${accessToken}&topic_Id=${articleId}` }) .then(response => response.json()) .then(json => dispatch({ type:SWITCH_COLLECTED, success:json.success })) } } // Footer export const setCurrentRouter = router => ({ type:CURRENT_ROUTER, router }) // Login export const fetchAccess = accessToken => { return dispatch => { fetch('https://cnodejs.org/api/v1/accesstoken', { method: 'POST', headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: `accesstoken=${accessToken}` }) .then(response => response.json()) .then(json => { if (json.success){ dispatch(loginSucceed(json.loginname,json.id,accessToken)) } else { dispatch(loginFailed(json.error_msg)); } }) } } const loginSucceed = (loginName,loginId,accessToken) => ({ type:LOGIN_SUCCESS, loginName, loginId, accessToken }) const loginFailed = failedMessage => ({ type:LOGIN_FAILED, failedMessage }) export const logout = () => ({ type:LOGOUT }) // profile const requestProfile = loginname => ({ type:REQUEST_PROFILE, loginname }) const receiveProfile = (loginname,profile) => ({ type:RECEIVE_PROFILE, loginname, profile }) const getCollectedTopics = (userName) => { return dispatch => { fetch(`https://cnodejs.org/api/v1/topic_collect/${userName}`) .then(response => response.json()) .then(json => dispatch({ type:GET_COLLECTED_TOPICS, success:json.success, data:json.data })) } } export const fetchProfile = (loginname) => { return dispatch => { dispatch(requestProfile(loginname)) dispatch(getCollectedTopics(loginname)) fetch(`https://cnodejs.org/api/v1/user/${loginname}`) .then(response => response.json()) .then(json => dispatch(receiveProfile(loginname,json.data))) } } // publishTopic export const fetchPublishTopic = (accessToken,tab,title,content) => { return dispatch => { const postConent = `accesstoken=${accessToken}&tab=${tab}&content=${content}&title=${title}` fetch(`https://cnodejs.org/api/v1/topics`, { method: 'POST', headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: postConent }) .then(response => response.json()) .then(json => dispatch({ type:PUBLISH_TOPIC, success:json.success, topicId:json.topic_id })) } } // message export const fetchMessage = (accessToken) => { return dispatch => { fetch(`https://cnodejs.org/api/v1/messages?accesstoken=${accessToken}`) .then(response => response.json()) .then(json => dispatch({ type:FETCH_MESSAGE, hasReadMessage:json.data.has_read_messages, hasNotReadMessage:json.data.hasnot_read_messages })) } } export const markAllMessages = (accessToken) => { return dispatch => { fetch(`https://cnodejs.org/api/v1/message/mark_all`, { method: 'POST', headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: `accesstoken=${accessToken}` }) .then(response => response.json()) .then(json => dispatch({ type:MARK_ALL_MESSAGES, isMarked:json.success })) } } ================================================ FILE: src/components/Article/Content/Content.js ================================================ import React, { PropTypes, Component } from 'react' import { Link } from 'react-router-dom' import { connect } from 'react-redux' import prefix from '../../../utils/routePrefix' import { setTransition } from '../../../actions/hashUrl' import { fetchProfile, switchCollected } from '../../../actions' import styles from './styles.scss' import LinkToLogin from '../../common/LinkToLogin/LinkToLogin' import transformDate from '../../../utils/transformDate' import classnames from 'classnames' import { List, ListItem } from 'material-ui/List' import Divider from 'material-ui/Divider' import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' import fetch from 'isomorphic-fetch' @connect() class Content extends Component { constructor() { super() this.state = { isCollected: false } } componentWillMount() { this.update(this.props) } componentWillReceiveProps(newProps) { if (this.props.collectedTopics.userName !== newProps.collectedTopics.userName) { this.update(newProps); } } update(props) { const { article, login, collectedTopics } = props if (login.succeed && collectedTopics.length !== 0) { let isCollected = collectedTopics.some(topic => { return article.id === topic.id }) this.setState({ isCollected: isCollected }) } } render() { const { article, dispatch, fetchProfile, login, collectedTopics, profile } = this.props return (
{article.author.loginname}
{ this.props.dispatch(setTransition({ transition: 'up' })) if (profile.loginname !== article.author.loginname) { dispatch(fetchProfile(article.author.loginname)) } }}> {article.author.loginname} 发表于{transformDate(article.create_at)}
{login.succeed && 收藏 { this.setState({ isCollected: !this.state.isCollected }) dispatch(switchCollected(this.state.isCollected, login.accessToken, article.id)) // fetch(`https://cnodejs.org/api/v1/topic_collect/${this.state.isCollected?'de_collect':'collect'}`, { // method: 'POST', // headers: { // "Content-Type": "application/x-www-form-urlencoded" // }, // body: `accesstoken=${login.accessToken}&topic_Id=${article.id}` // }) }}> } {article.visit_count} {article.reply_count}
{article.title}
) } } export default Content ================================================ FILE: src/components/Article/Content/styles.scss ================================================ .head{ margin-top: 64px; background: #ccc; overflow: hidden; padding: 10px; img{ width: 50px; height: 50px; border-radius: 50px; } } .imgbox{ float: left; text-align: center; margin-right: 10px; } .info{ height: 25px; line-height: 25px; } .title{ text-align: center; font-weight: bold; font-size: 24px; line-height: 32px; } .main{ padding: 20px; p{ text-indent: 2em; } img{ display: block; margin: 20px auto; width: 600px; } } @media (max-width: 750px){ .main{ img{ width: 80%; } } } ================================================ FILE: src/components/Article/Reply/Reply.js ================================================ import React, { PropTypes,Component } from 'react' import {Link} from 'react-router-dom' import { connect } from 'react-redux' import prefix from '../../../utils/routePrefix' import styles from './styles.scss' import { setTransition } from '../../../actions/hashUrl' import {fetchComment,fetchArticle,recordArticleScrollT,switchSupport,fetchProfile} from '../../../actions' import transformDate from '../../../utils/transformDate' import getSize from '../../../utils/getSize' import LinkToLogin from '../../common/LinkToLogin/LinkToLogin' import Dialog from '../../common/Dialog' import classnames from 'classnames' import {List, ListItem} from 'material-ui/List'; import Divider from 'material-ui/Divider'; import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; import TextField from 'material-ui/TextField'; import RaisedButton from 'material-ui/RaisedButton'; @connect() class Reply extends Component { constructor(){ super(); this.state = { isSupported:[], supportNum:[], height:[], name:[], openDialog:false } } supportState = (replies,login) => { let isSupported = replies.map(reply => { return reply.ups.some(up => up === login.loginId) }) let supportNum = replies.map(reply => reply.ups.length) this.setState({isSupported,supportNum}) } closeDialog = () => { this.setState({ openDialog:false }) } componentWillMount(){ const {replies,login} = this.props this.supportState(replies,login) } componentWillReceiveProps(newProps){ const {switchSupportInfo,replies,login,isCommented,currentTopicId,dispatch} = newProps if(this.state.height.length !== 0){ this.setState({ height:[], name:[] }) } if(replies.length !== this.props.replies.length){ this.supportState(replies,login) } if(isCommented && this.props.isCommented !== isCommented){ dispatch(fetchArticle(currentTopicId)) } // if(switchSupportInfo){ // if(!this.props.switchSupportInfo || !(this.props.switchSupportInfo.action===switchSupportInfo.action && this.props.switchSupportInfo.index===switchSupportInfo.index)){ // if(switchSupportInfo.success){ // let isSupported = this.state.isSupported; // let supportNum = this.state.supportNum; // isSupported[switchSupportInfo.index] = switchSupportInfo.action === 'up'; // switchSupportInfo.action === 'up' ? ++supportNum[switchSupportInfo.index] : --supportNum[switchSupportInfo.index] // this.setState({ // isSupported, // supportNum // }) // }else{ // } // } // } } render(){ let {profile,replies,dispatch,login,switchSupportInfo,currentTopicId}= this.props return (
不能给自己点赞!

共有{replies.length}条回复

{replies.map((reply,index) => (
{reply.author.loginname}/
{index+1}楼
{ this.props.dispatch(setTransition({ transition: 'up' })) if(profile.loginname !== reply.author.loginname){ dispatch(fetchProfile(reply.author.loginname)) } }}> {reply.author.loginname} {transformDate(reply.create_at)}
{ let heightArr = []; let nameArr = []; heightArr[index] = this.state.height[index] ? 0 : 120 nameArr[index] = `@${reply.author.loginname} ` this.setState({ height:heightArr, name:nameArr }) }} style={{cursor:'pointer',marginRight:10}}> 回复 { e.stopPropagation() if(login.loginId){ if(reply.author.loginname !== login.loginName){ const {scrollT} = getSize() // 点赞的时候也会发送数据请求,所以Article组件会刷新,如果不保存位置的话,Article的位置会变成上次记录的位置或者默认位置0 dispatch(recordArticleScrollT(currentTopicId,scrollT)) let isSupported = this.state.isSupported; let supportNum = this.state.supportNum; isSupported[index]? --supportNum[index] : ++supportNum[index] isSupported[index] = !isSupported[index] this.setState({ isSupported, supportNum }) dispatch(switchSupport(login.accessToken,reply.id,index)) }else{ this.setState({ openDialog:true }) } }else{ let heightArr = []; heightArr[index] = this.state.height[index] ? 0 : 150 this.setState({ height:heightArr }) } }} style={{color:this.state.isSupported[index] ? 'red':'black',cursor:'pointer'}} > {this.state.supportNum[index]}
))}
) } } class NeedComment extends Component{ render(){ const {login,dispatch,currentTopicId,pHeight} = this.props const sHeight = pHeight ? pHeight : 0 const style = pHeight ? {overflow:'hidden',minHeight:pHeight} : {overflow:'hidden',height:0} // const tail = '—— —— 来自lumia2046专版客户端' const tail = '

— — 来自lumia2046-react-cnode

' if(login.loginId){ return (
{ const defaultValue = this.props.defaultValue || '' e.target.value = e.target.value || defaultValue }} />
{ e.preventDefault(); const textarea = this.refs.textarea.input.refs.input.value + tail if(!textarea.trim()){ return null; } const {scrollT,contentH,windowH} = getSize() dispatch(fetchComment(login.accessToken,currentTopicId,textarea)) if(pHeight === 120){ dispatch(recordArticleScrollT(currentTopicId,contentH-windowH)) }else{ dispatch(recordArticleScrollT(currentTopicId,scrollT)) } this.refs.textarea.input.refs.input.value = '' }}/>
) }else{ return (
) } } } export default Reply ================================================ FILE: src/components/Article/Reply/styles.scss ================================================ .reply{ li{ padding: 10px 20px; overflow: hidden; border-bottom: 1px solid #ccc; } h2{ padding: 10px 20px; font-size: 18px; background: #ccc; } img{ width: 50px; height: 50px; border-radius: 50%; } b{ display: block; text-align: right; } } .author{ float: left; text-align: center; padding-top: 10px; } .main{ padding: 0 0 0 60px; } .item{ padding: 5px; overflow: hidden; } .form{ padding: 20px; } .textarea{ transition: all 0.3s ease-out; } ================================================ FILE: src/components/HomePage/Drawer/Drawer.js ================================================ import React from 'react'; import { Link } from 'react-router-dom' import { connect } from 'react-redux' import prefix from '../../../utils/routePrefix' import Drawer from 'material-ui/Drawer' import MenuItem from 'material-ui/MenuItem' import RaisedButton from 'material-ui/RaisedButton' import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' import Divider from 'material-ui/Divider' import Avatar from 'material-ui/Avatar' import { logout, fetchProfile } from '../../../actions' import { setTransition } from '../../../actions/hashUrl' import styles from './styles.scss' import getSize from '../../../utils/getSize' import transformDate from '../../../utils/transformDate' import Dialog from '../../common/Dialog' @connect() export default class myDrawer extends React.Component { constructor() { super(); this.state = { isOpen: false } } excuteLogout = () => { const { dispatch } = this.props window.localStorage.removeItem('masterInfo') window.sessionStorage.removeItem('masterProfile') dispatch(logout()) } close = () => { this.setState({ isOpen: false }) } componentWillUpdate(nextProps) { } render() { let { contentW } = getSize() if (contentW > 800) { contentW = 640 } else { contentW = 0.8 * contentW } let { login, profile } = this.props const { succeed } = login if (login.loginName !== profile.loginname && window.sessionStorage.masterProfile) { profile = JSON.parse(window.sessionStorage.masterProfile) } const { avatar_url, loginname, score, create_at } = profile return ( {succeed &&

{loginname}

积分:{score}

注册于:{transformDate(create_at)}

{ this.setState({ isOpen: true }) }} /> 确定要注销登陆?
个人主页 消息
} {!succeed &&
this.props.dispatch(setTransition({ transition: 'up' }))}>

点击头像登陆

}
); } } ================================================ FILE: src/components/HomePage/Drawer/styles.scss ================================================ .header{ text-align: center; padding: 100px 10px 50px; background: #DCE775; p{ margin: 10px; } } .link{ vertical-align: center; display: block; border-bottom: 1px solid #DCE775; } ================================================ FILE: src/components/HomePage/FloatingActionButton.js ================================================ import React from 'react' import { connect } from 'react-redux' import { setTransition } from '../../actions/hashUrl' import FloatingActionButton from 'material-ui/FloatingActionButton' import ContentAdd from 'material-ui/svg-icons/content/add' import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' import { Link } from 'react-router-dom' import prefix from '../../utils/routePrefix' const style = { position: 'fixed', bottom: 50, right: 50 } @connect() class FloatActionButton extends React.Component { render() { return ( this.props.dispatch(setTransition({ transition: 'up' }))}> ) } } export default FloatActionButton ================================================ FILE: src/components/HomePage/Header/Header.js ================================================ import React, { Component } from 'react' import { connect } from 'react-redux' import styles from './styles.scss' import { setTransition } from '../../../actions/hashUrl' import { Link } from 'react-router-dom' import prefix from '../../../utils/routePrefix' import { Tabs, Tab } from 'material-ui/Tabs' import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' import AppBar from 'material-ui/AppBar' import Badge from 'material-ui/Badge' import NotificationsIcon from 'material-ui/svg-icons/social/notifications' import IconButton from 'material-ui/IconButton' import SwipeableViews from 'react-swipeable-views' import getSize from '../../../utils/getSize' @connect() class Header extends Component { constructor(props) { super(props); this.state = { slideIndex: 0 } } componentWillMount() { const { tabs, filter } = this.props; let slideIndex; tabs.forEach((tab, index) => { if (tab.filter === filter) { slideIndex = index; } }) this.setState({ slideIndex: slideIndex }) } handleChange = (value) => { this.setState({ slideIndex: value, }); this.props.onClick(this.props.tabs[value].filter) }; render() { // this.props.tabs.forEach(tab => { // tab.cn = classnames({[styles.active] : tab.filter === this.props.filter}) // }) return (
NodeJS论坛

} onLeftIconButtonClick={this.props.toggleDrawer} iconElementRight={
this.props.dispatch(setTransition({ transition: 'up' }))}>
} /> {this.props.tabs.map((tab, i) => )}
{this.props.children}
) } } // Header.propTypes = { // filter: PropTypes.string.isRequired, // onClick: PropTypes.func.isRequired // } export default Header ================================================ FILE: src/components/HomePage/Header/styles.scss ================================================ .header{ position: fixed; overflow: hidden; z-index: 10; // width: 100%; transition: all 0.3s ; } ================================================ FILE: src/components/HomePage/Lists/Lists.js ================================================ import React from 'react' import FlipMove from 'react-flip-move' import transformDate from '../../../utils/transformDate' import styles from './styles.scss' import { setTransition } from '../../../actions/hashUrl' import { Link } from 'react-router-dom' import prefix from '../../../utils/routePrefix' import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' import { List, ListItem } from 'material-ui/List' import Divider from 'material-ui/Divider' import Avatar from 'material-ui/Avatar' import getSize from '../../../utils/getSize' const Lists = props => { const tabChn = { all: '全部', good: '精华', share: '分享', ask: '问答', job: '招聘' } const { topics, fetchArticle, dispatch, article, isFetching, selectedTab, history } = props let disableAllAnimations = topics.length === 20 ? false : true // disableAllAnimations从启用到禁用时enterAnimation设定的动画会不起作用,原因不明。 let enterAnimation = { from: { transform: 'translateY(-80px)', opacity: 0 }, to: { transform: 'translateY(0)', opacity: 1 } } return (
{topics.map((topic, i) => { dispatch(setTransition({ transition: 'move' })) if (!article[topic.id]) { dispatch(fetchArticle(topic.id)) } else if (article.currentTopicId !== topic.id) { dispatch(fetchArticle(topic.id, false)) } }}> } primaryText={
{topic.top && } {topic.good && } {topic.title}
} secondaryText={
{topic.reply_count + '/' + topic.visit_count} {tabChn[topic.tab]} {transformDate(topic.create_at)}
} /> )}
) } // Lists.propTypes = { // topics: PropTypes.array.isRequired, // fetchArticle: PropTypes.func.isRequired // } export default Lists ================================================ FILE: src/components/HomePage/Lists/styles.scss ================================================ .lists{ position: relative; margin-top: 100px; } .li{ position: relative; top:0; height: 61px; padding: 5px 15px; border-bottom: 1px solid #ccc; } .link{ display: block; } .text{ margin: 0 0 10px; span{ padding-right: 10px; vertical-align: middle; font-weight:bold; } .title{ display: inline-block; padding: 0; width:80%; overflow: hidden; text-overflow:ellipsis; white-space: nowrap; } } .spinner { margin: 24px auto; width: 100%; text-align: center; } .spinner > div { width: 30px; height: 30px; margin: 0 20px; background-color: #00BCD4; border-radius: 100%; display: inline-block; -webkit-animation: bouncedelay 1.4s infinite ease-in-out; animation: bouncedelay 1.4s infinite ease-in-out; /* Prevent first frame from flickering when animation starts */ -webkit-animation-fill-mode: both; animation-fill-mode: both; } .spinner .bounce1 { -webkit-animation-delay: -0.32s; animation-delay: -0.32s; } .spinner .bounce2 { -webkit-animation-delay: -0.16s; animation-delay: -0.16s; } @-webkit-keyframes bouncedelay { 0%, 80%, 100% { -webkit-transform: scale(0.0) } 40% { -webkit-transform: scale(1.0) } } @keyframes bouncedelay { 0%, 80%, 100% { transform: scale(0.0); -webkit-transform: scale(0.0); } 40% { transform: scale(1.0); -webkit-transform: scale(1.0); } } ================================================ FILE: src/components/Message/Content/Content.js ================================================ import React, { PropTypes, Component } from 'react' import { connect } from 'react-redux' import transformDate from '../../../utils/transformDate' import styles from './styles.scss' import classnames from 'classnames' import { Link } from 'react-router-dom' import { setTransition } from '../../../actions/hashUrl' import prefix from '../../../utils/routePrefix' import { Tabs, Tab } from 'material-ui/Tabs'; import SwipeableViews from 'react-swipeable-views'; import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; import { List, ListItem } from 'material-ui/List'; import Divider from 'material-ui/Divider'; import Avatar from 'material-ui/Avatar'; import RaisedButton from 'material-ui/RaisedButton'; import Dialog from '../../common/Dialog' import CircleLoading from '../../common/CircleLoading' import { markAllMessages, fetchMessage } from '../../../actions' @connect() class Content extends Component { constructor(props) { super(props); this.state = { slideIndex: 0, isOpen: false, isUpdating: false } } handleChange = (value) => { this.setState({ slideIndex: value, }); }; markMessages = () => { let { dispatch, login } = this.props let accessToken = login.accessToken dispatch(markAllMessages(accessToken)) this.setState({ isUpdating: true }) } close = () => { this.setState({ isOpen: false }) } componentWillUpdate(newProps) { let { dispatch, login, isMarked, hasNotReadMessage } = newProps let accessToken = login.accessToken if (isMarked && isMarked !== this.props.isMarked) { dispatch(fetchMessage(accessToken)) } if (isMarked && hasNotReadMessage.length === 0 && hasNotReadMessage.length !== this.props.hasNotReadMessage.length) { this.setState({ isUpdating: false }) } } render() { const { dispatch, hasNotReadMessage, hasReadMessage, article, fetchArticle } = this.props return (
{this.state.isUpdating &&
} {!this.state.isUpdating &&
未读消息:{hasNotReadMessage && hasNotReadMessage.length}} value={0} /> 已读消息:{hasReadMessage && hasReadMessage.length}} value={1} />
{hasNotReadMessage && hasNotReadMessage.length === 0 &&
暂无未读消息
} {hasNotReadMessage.length > 0 &&
{hasNotReadMessage.map((msg, index) => { dispatch(setTransition({ transition: 'up' })) if (!article[msg.topic.id]) { dispatch(fetchArticle(msg.topic.id)) } else if (article.currentTopicId !== topic.id) { dispatch(fetchArticle(msg.topic.id, false)) } }}> } primaryText={msg.author.loginname} secondaryText={

来自:{msg.topic.title} {transformDate(msg.reply.create_at)}

} secondaryTextLines={2} /> )}
{ this.setState({ isOpen: true }) }} />
是否将所有未读消息标记为已读?
}
{hasReadMessage.length === 0 &&
您还没有查看过任何消息哦
} {hasReadMessage.length > 0 && {hasReadMessage.map((msg, index) => { dispatch(setTransition({ transition: 'up' })) if (!article[msg.topic.id]) { dispatch(fetchArticle(msg.topic.id)) } else if (article.currentTopicId !== topic.id) { dispatch(fetchArticle(msg.topic.id, false)) } }}> } primaryText={msg.author.loginname} secondaryText={

来自:{msg.topic.title} {transformDate(msg.reply.create_at)}

} secondaryTextLines={2} /> )}
}
}
); } } export default Content ================================================ FILE: src/components/Message/Content/styles.scss ================================================ .content{ margin-top: 64px; overflow: hidden; } .link{ display: block; } .msg{ padding: 20px; text-align: center; font-size: 24px; } .oneline{ font-weight: bold; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } ================================================ FILE: src/components/PublishTopic/Form/Form.js ================================================ import React, { PropTypes,Component } from 'react' import getStrLength from '../../../utils/getStrLength' import styles from './styles.scss' import classnames from 'classnames' import TextField from 'material-ui/TextField'; import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' import RaisedButton from 'material-ui/RaisedButton' import DropDownMenu from 'material-ui/DropDownMenu'; import MenuItem from 'material-ui/MenuItem'; class Form extends Component { constructor(props) { super(props); this.state = {value: "ask"}; } handleChange = (event, index, value) => this.setState({value}); render(){ const {ifTitleErr,ifContentErr,showDialog,fetchPublishTopic,dispatch,login,state} = this.props return (
请选择主题类别:
{ let titleErr = getStrLength(e.target.value)<10 ? true:false ifTitleErr(titleErr) }}/>
标题不得少于十个字符!
{ let contentErr = getStrLength(e.target.value)===0 ? true:false ifContentErr(contentErr) }}/>
内容不能为空!
{ e.preventDefault() const input = this.refs.input.input.value const textarea = this.refs.textarea.input.refs.input.value const select = this.refs.select.props.value if(getStrLength(input) < 10 || !textarea.trim()){ return null } dispatch(fetchPublishTopic(login.accessToken,select,input,textarea)) showDialog() }} />
) } } export default Form ================================================ FILE: src/components/PublishTopic/Form/styles.scss ================================================ .form{ text-align: center; margin-top: 80px; } .errorInfo{ color: red; margin: 5px; overflow: hidden; transition: all 0.3s; } .content{ margin: 10px; } .title{ position: relative; top: -20px; /*vertical-align: top;*/ } ================================================ FILE: src/components/common/AsyncContainer.js ================================================ import React, { Component } from 'react' import CircleLoading from './CircleLoading' class AsyncContainer extends Component { state = { mounted: false } componentDidMount() { this.timeOut = setTimeout(() => { this.setState({ mounted: true }) }, this.props.delay || 500) } componentWillUnmount() { clearTimeout(this.timeOut) } render() { return (
{this.state.mounted && this.props.children}
) } componentWillUnmount() { clearTimeout(this.timeOut) } } export default AsyncContainer ================================================ FILE: src/components/common/CircleLoading.js ================================================ import React from 'react' import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' import CircularProgress from 'material-ui/CircularProgress' import getSize from '../../utils/getSize' const { windowH, windowW } = getSize() const style = { position:'relative', paddingTop: 0.5 * windowH - 40, textAlign: 'center' } const CircleLoading = () => (
); export default CircleLoading; ================================================ FILE: src/components/common/Dialog.js ================================================ import React from 'react'; // import { hashHistory,browserHistory } from 'react-router'; import Dialog from 'material-ui/Dialog'; import FlatButton from 'material-ui/FlatButton'; import RaisedButton from 'material-ui/RaisedButton'; import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; const DialogExample = (props) => { const handleClose = () => { props.close() }; const handleJump = () => { props.close() if(props.link){ // hashHistory.push(props.link) // browserHistory.push(props.link) // pros.location = props.link alert('a') } if(props.action){ props.action() } } const actions = props.singleButton ? [] : [, ]; return ( {props.children} ); } export default DialogExample ================================================ FILE: src/components/common/Header/Header.js ================================================ import React, { Component } from 'react' import { connect } from 'react-redux' import { withRouter } from 'react-router-dom' import { setTransition } from '../../../actions/hashUrl' import styles from './styles.scss' import AppBar from 'material-ui/AppBar' import IconButton from 'material-ui/IconButton' import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' import getSize from '../../../utils/getSize' // @withRouter @connect(store => ({ hashUrl: store.hashUrl })) class Header extends Component { constructor() { super() this.state = { position: 'static' } } componentWillMount() { if (this.props.position) { this.state.position = 'absolute' this.state.left = '100%' } } componentDidMount() { if (this.props.position) { this.setState({ left: 0 }) setTimeout(() => this.setState({ position: 'fixed' }), 500) } } componentWillUnmount() { } render() { const { isFetching, title, history, hashUrl } = this.props return (
{isFetching ? '加载中' : title}

} iconElementLeft={ } onLeftIconButtonClick={() => { this.props.dispatch(setTransition({ transition: 'left' })) history.goBack() if (this.props.position) { setTimeout(() => this.setState({ left: '100%' }), 450) } // window.location.href = `${window.location.href.split('#')[0]}#${hashUrl.oldUrl}` }} />
) } } export default withRouter(Header) ================================================ FILE: src/components/common/Header/styles.scss ================================================ .header{ // position: fixed; width: 100%; z-index: 10; transition: all 0.5s ease-out; } .title{ padding-right: 30px; text-align: center; } ================================================ FILE: src/components/common/LinkToLogin/LinkToLogin.js ================================================ import React, { PropTypes } from 'react' import { Link } from 'react-router-dom' import { setTransition } from '../../../actions/hashUrl' import prefix from '../../../utils/routePrefix' import classnames from 'classnames' import { setCurrentRouter } from '../../../actions' import styles from './styles.scss' import FlatButton from 'material-ui/FlatButton'; import RefreshIndicator from 'material-ui/RefreshIndicator'; import CircularProgress from 'material-ui/CircularProgress'; import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; const LinkToLogin = props => { let { dispatch } = props const masterInfo = window.localStorage.getItem('masterInfo') ? true : false return (
{!masterInfo && { dispatch(setTransition({ transition: 'up' })) dispatch(setCurrentRouter('login')) }}> } {masterInfo &&
}
) } export default LinkToLogin ================================================ FILE: src/components/common/LinkToLogin/styles.scss ================================================ .linkToLogin{ display: block; text-align: center; } ================================================ FILE: src/components/common/Profile/Profile.js ================================================ import React from 'react' import {Link} from 'react-router-dom' import prefix from '../../../utils/routePrefix' import styles from './styles.scss' import { setTransition } from '../../../actions/hashUrl' import transformDate from '../../../utils/transformDate' import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; import Avatar from 'material-ui/Avatar'; import {List, ListItem} from 'material-ui/List'; import Subheader from 'material-ui/Subheader'; import Divider from 'material-ui/Divider'; const Profile = props => { let {collectedTopics,profile} = props let {avatar_url,create_at,loginname,recent_replies,recent_topics,score} = profile; recent_replies = recent_replies ? recent_replies : []; recent_topics = recent_topics ? recent_topics : []; return (
{loginname}/

{loginname}

积分:{score}

注册于:{transformDate(create_at)}

收藏的话题
最近参与的话题
最近创建的话题
) } const TopicList = props => { const {dispatch,article,fetchArticle,topics} = props; return (
{topics.length === 0 && } {topics.length > 0 && topics.map((topic,index) => { dispatch(setTransition({ transition: 'up' })) if(!article[topic.id]){ dispatch(fetchArticle(topic.id)) }else if(article.currentTopicId !== topic.id){ dispatch(fetchArticle(topic.id,false)) } }}> }/> ) }
) } const ListExampleChat = () => ( Recent chats } /> ); export default Profile ================================================ FILE: src/components/common/Profile/styles.scss ================================================ .header{ text-align: center; padding: 0 20px 20px; img{ width: 50px; height: 50px; border-radius: 50%; } p{ margin: 10px; } } .boxs{ padding: 20px; overflow: hidden; } .box{ float: left; margin: 1.6%; width: 30%; /*padding: 0 20px 20px;*/ box-shadow: 0 0 5px grey; background: white; h2{ padding: 10px; } } .title{ font-weight:bold; padding: 5px 10px; border-bottom: 1px solid grey; text-align: left; overflow: hidden; text-overflow:ellipsis; white-space: nowrap; } .link{ display: block; } @media only screen and (max-width: 768px) { .box { margin: 5%; width: 90%; } } ================================================ FILE: src/components/common/Snackbar.js ================================================ import React from 'react'; import Snackbar from 'material-ui/Snackbar'; import RaisedButton from 'material-ui/RaisedButton'; import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; const SnackbarExample = (props) => { return ( ); } export default SnackbarExample ================================================ FILE: src/components/common/react-pullrefresh.js ================================================ import React, { Component } from 'react' import RefreshIndicator from 'material-ui/RefreshIndicator'; import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; // import './rotate.less' // import image from './pull.svg' const MAX_DEFAULT = 100 class Pull extends Component { static defaultProps = { max: MAX_DEFAULT } constructor(props) { super(props) this.state = { pulled:0 } } componentDidMount() { this.pullhelper = require('pullhelper') let { disabled,onRefresh,max } = this.props let maxPull = max || MAX_DEFAULT let that= this this.pullhelper .on('start',function(pulled) { that.setState({ pulling:true }) }) .on('stepback',function(pulled,next) { that.setState({ pulled:pulled }) let nextPulled = Math.min(pulled - Math.min(pulled/2,10),max) next(nextPulled) }) .on('step',function(pulled) { that.setState({ pulled:pulled }) }) .on('pull',function(pulled,next) { that.setState({ pulling:false }) if(!onRefresh || pulled < maxPull) { next() return } that.setState({ loading:true }) onRefresh(_ => { that.setState({ loading:false }) next() }) }) .load() if(disabled) { this.pullhelper.pause() } } componentWillReceiveProps(nextProps) { let { disabled } = this.props if(disabled !== nextProps.disabled) { if(nextProps.disabled) { this.pullhelper.pause() } else { this.pullhelper.resume() } } } componentWillUnmount() { this.pullhelper.unload() } render() { let { pulling,loading,pulled } = this.state let maxPull = this.props.max || MAX_DEFAULT let size = this.props.size || 30 let color = this.props.color || '#00BCD4' let style = this.props.style || {} return (
0.9999 ? 'loading':'ready'} style={{display:'inline-block', position:'relative', opacity:pulled/maxPull}} />
) } } export default Pull; // // ================================================ FILE: src/configureStore.js ================================================ import { createStore, applyMiddleware } from 'redux'; import { composeWithDevTools } from 'redux-devtools-extension'; import thunk from 'redux-thunk'; import { createLogger } from 'redux-logger'; import { ConnectedRouter, routerReducer, routerMiddleware, push } from 'react-router-redux'; import createHistory from 'history/createBrowserHistory'; import rootReducer from './reducers/index' const history = createHistory(); const middleware = routerMiddleware(history) const logger = createLogger({ collapsed: true }) const middlewares = [thunk, middleware] if(process.env.NODE_ENV === 'development') { middlewares.push(logger) } const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(...middlewares))) export default store ================================================ FILE: src/containers/App.js ================================================ import React, { Component, PropTypes } from 'react' import { connect } from 'react-redux' import {fetchAccess,fetchMessage,fetchProfile} from '../actions' import Header from '../components/common/Header/Header' import Content from '../components/Article/Content/Content' import Reply from '../components/Article/Reply/Reply' import getSize from '../utils/getSize' class App extends Component { componentWillMount(){ // console.log('componentWillMount') const {dispatch} = this.props; const LoadingAction = (accessToken,loginName) => { dispatch(fetchAccess(accessToken)) dispatch(fetchMessage(accessToken)) dispatch(fetchProfile(loginName)) } if(window.localStorage.getItem('masterInfo')){ let masterInfo = window.localStorage.getItem('masterInfo') masterInfo = JSON.parse(masterInfo) const accessToken = masterInfo.accessToken const loginName = masterInfo.loginName LoadingAction(accessToken,loginName) }else{ // const accessToken = '1cbc2a58-6c1b-426f-971d-070676fb849d' // const loginName = 'lumia2046' // LoadingAction(accessToken,loginName) } } render() { return (
{this.props.children}
) } } App.propTypes = { // currentTopicId: PropTypes.string.isRequired, // article: PropTypes.object.isRequired, // isFetching: PropTypes.bool.isRequired, // dispatch: PropTypes.func.isRequired } function mapStateToProps(state) { const {login,profile} = state return {login,profile} } export default connect(mapStateToProps)(App) ================================================ FILE: src/containers/Article.js ================================================ import React, { Component, PropTypes } from 'react' import { connect } from 'react-redux' import { switchSupport, fetchComment, fetchArticle, recordArticleScrollT, fetchProfile } from '../actions' import Header from '../components/common/Header/Header' import CircleLoading from '../components/common/CircleLoading' import AsyncContainer from '../components/common/AsyncContainer' import Content from '../components/Article/Content/Content' import Reply from '../components/Article/Reply/Reply' import getSize from '../utils/getSize' class Article extends Component { constructor() { super() this.state = { fadeIn: true } } componentWillMount() { const { scrollT, dispatch, article, isFetching } = this.props if (scrollT) { // window.scrollTo(0, scrollT) } // window.scrollTo(0, scrollT) if (!article.author && !isFetching) { const topicId = window.location.href.split('topic/')[1].split('?_')[0] dispatch(fetchArticle(topicId)) } } componentWillReceiveProps(newProps) { const { scrollT } = newProps document.getElementById('articleContainer').scrollTop = scrollT } componentDidMount() { const { scrollT } = this.props document.getElementById('articleContainer').scrollTop = scrollT } componentWillUnmount() { const { currentTopicId, dispatch, profile, login } = this.props dispatch(recordArticleScrollT(currentTopicId, document.getElementById('articleContainer').scrollTop)) if (!window.sessionStorage.masterProfile && login.loginName === profile.loginname) { window.sessionStorage.masterProfile = JSON.stringify(profile) } } render() { let { isFetching, article, currentTopicId, login, switchSupportInfo, isCommented, dispatch, collectedTopics, profile } = this.props if (login.loginName !== profile.loginname && window.sessionStorage.masterProfile) { collectedTopics = JSON.parse(window.sessionStorage.masterProfile).collectedTopics } return (
{Object.keys(article).length === 0 && } {Object.keys(article).length !== 0 &&
}
) } } function mapStateToProps(state) { const { currentRouter, login, profile } = state; const { currentTopicId, switchSupportInfo, isCommented } = state.article; const { collectedTopics } = profile const isFetching = state.article[currentTopicId] ? state.article[currentTopicId].isFetching : false; const scrollT = state.article[currentTopicId] ? state.article[currentTopicId].scrollT : '0'; const article = state.article[currentTopicId] && state.article[currentTopicId].article ? state.article[currentTopicId].article : {}; return { isFetching, scrollT, article, currentTopicId, login, switchSupportInfo, currentRouter, collectedTopics, profile, isCommented } } export default connect(mapStateToProps)(Article) ================================================ FILE: src/containers/HomePage.js ================================================ import React, { Component, PropTypes } from 'react' import { connect } from 'react-redux' import { withRouter } from 'react-router-dom' import { selectTab, fetchTopics, recordScrollT, fetchArticle, fetchAccess, fetchMessage } from '../actions' import Header from '../components/HomePage/Header/Header' import FloatingActionButton from '../components/HomePage/FloatingActionButton' import CircleLoading from '../components/common/CircleLoading' import Drawer from '../components/HomePage/Drawer/Drawer' import Lists from '../components/HomePage/Lists/Lists' import Snackbar from '../components/common/Snackbar' import getSize from '../utils/getSize' import { setTransition } from '../actions/hashUrl' import Pull from '../components/common/react-pullrefresh' // @withRouter @connect(state => { const { homePage, article, login, profile, message, hashUrl } = state const { selectedTab, tabData } = homePage; const unreadMessageCount = message.hasNotReadMessage.length // 当组件第一次render时,还未进行任何action,初始化的state里没有tabData[selectedTab],所以这里需要初始化 const { isFetching, page, scrollT, topics } = tabData[selectedTab] || { isFetching: false, page: 0, scrollT: 0, topics: [] } return { isFetching, page, scrollT, topics, selectedTab, article, login, profile, tabData, unreadMessageCount, hashUrl } }) class HomePage extends Component { constructor() { super() this.state = { fadeIn: true, openDrawer: false, openSnackbar: false, isFreshing: false, fixedTop: 0, scrollT: 0 } } onRefresh = (next) => { if (!this.state.isFreshing) { this.setState({ isFreshing: true }) this.fresh() setTimeout(_ => { next() this.openSnackbar() this.setState({ isFreshing: false }) }, 3000) } } fresh = () => { const { selectedTab, login, dispatch } = this.props; dispatch(fetchTopics(selectedTab, 1)) dispatch(fetchMessage(login.accessToken)) } openSnackbar = () => { this.setState({ openSnackbar: true }) setTimeout(() => { this.setState({ openSnackbar: false }) }, 2500) } handleClick = (tab) => { let homeContainerDom = document.getElementById('homeContainer') let scrollT = homeContainerDom.scrollTop const { selectedTab, dispatch, tabData } = this.props dispatch(recordScrollT(selectedTab, scrollT)) dispatch(selectTab(tab)) homeContainerDom.scrollTop = tabData[tab].scrollT || 0 const ua = navigator.userAgent if (!tabData[tab] && ua.indexOf('Mobile') === -1) { if (scrollT >= 64) { dispatch(recordScrollT(tab, 64)) this.setState({ scrollT: 64 }) } else { dispatch(recordScrollT(tab, scrollT)) this.setState({ scrollT: scrollT }) } } // 在事件的回调函数中的action,是将action都发送并改变state完毕,再更新state // 也就是说事件回调函数中发送多个不带异步的action,只在最后一个action更新完state进行一次状态更新 // 要想每次action更新state后,都更新一次状态(方便在更新时做一些操作),就要把action封装成异步操作 // 但是在react生命周期里的一个函数里发送多个action,每个action更新state都会对相应组件进行一次状态更新,不需要封装成异步操作 // let asyncDispatch = async function(){ // await dispatch(recordScrollT(selectedTab,scrollT)) // await dispatch(selectTab(tab)) // } // asyncDispatch() } loadMore = () => { const { selectedTab, page, isFetching, dispatch } = this.props; let ipage = page if (!isFetching) { dispatch(fetchTopics(selectedTab, ++ipage)) } } toggleDrawer = () => { this.setState({ openDrawer: !this.state.openDrawer }) } componentWillMount() { const { scrollT } = this.props } componentWillUpdate(newProps, newState) { const { topics, isFetching, selectedTab, scrollT, dispatch } = newProps // 去除刷新时记住的滚动条位置 if (topics.length === 0 && this.props.scrollT === 0) { window.scrollTo(0, 0) } // fetchTopics开始后会先发送一个request的action,这个ation也会改变state从而触发该方法。如果不对isFetching进行判断,会再次进行fetchTopics从而进行了不必要的重复数据请求 if (!isFetching && topics.length === 0) { dispatch(fetchTopics(selectedTab)); } if (selectedTab !== this.props.selectedTab) { if (scrollT) { // console.log(scrollT, " window.scrollTo",'componentWillUpdate') window.scrollTo(0, scrollT) } } } tabs = [ { title: '全部', filter: 'all', }, { title: '精华', filter: 'good', }, { title: '分享', filter: 'share', }, { title: '问答', filter: 'ask', }, { title: '招聘', filter: 'job', } ] render() { const { selectedTab, isFetching, page, topics, dispatch, article, currentRouter, login, profile, unreadMessageCount, tabData, history } = this.props return (
{this.tabs.map((tab, index) =>
{((isFetching && page === 0) || (tab.filter !== selectedTab && !tabData[tab.filter])) && } {tab.filter === selectedTab &&
= 1) ? 1 : 0 }}>
}
)}
{!isFetching && }
) } componentDidMount() { const { selectedTab, page, scrollT, dispatch } = this.props if (page === 0) { dispatch(fetchTopics(selectedTab)) } let homeContainerDom = document.getElementById('homeContainer') if (scrollT) { homeContainerDom.scrollTop = scrollT } homeContainerDom.onscroll = () => { let scrollT = homeContainerDom.scrollTop let contentH = homeContainerDom.scrollHeight let { windowH } = getSize() if (windowH + scrollT + 10 > contentH) { this.loadMore() } // 由于下面的操作比较费cpu,所以进行判断是否为手机端 let lastScrollY = this.lastScrollY const ua = navigator.userAgent; if (ua.indexOf('Mobile') === -1) { if (!lastScrollY || !scrollT) { lastScrollY = scrollT } let diff = scrollT - lastScrollY if (diff >= 0) { if (scrollT > 64 && this.state.fixedTop !== 64) { this.setState({ fixedTop: 64 }) } if (scrollT <= 64) { this.setState({ fixedTop: scrollT }) } } else { this.setState({ scrollT: 0 }) if (scrollT > 64 && this.state.fixedTop !== 0) { this.setState({ fixedTop: 0 }) } } lastScrollY = scrollT } } } componentDidUpdate() { let { windowH, contentH, scrollT } = getSize(); const { topics } = this.props // 第一次切换到没有加载数据的tab时,在willReceiveProp中已经将页面滚动了一定距离,在render中打印scrollT也不为0 // 但是一进入这个函数scrollT就变为0,而且也未触发onscroll事件(问题待解决),所以目前只能去重新判断这种情况 if (scrollT === 0 && this.state.scrollT > 0) { // window.scrollTo(0, this.state.scrollT) } // 判断内容加载后,内容的高度是否填满屏幕,若网页太高,加载一次内容的高度不能填满整个网页,则继续请求数据 if (topics.length > 0 && windowH + 200 > contentH) { // this.loadMore() } } componentWillUnmount() { const { selectedTab, dispatch } = this.props dispatch(recordScrollT(selectedTab, document.getElementById('homeContainer').scrollTop)) // 必须解绑事件,否则当组件再次被加载时,该事件会监听两个组件 window.onscroll = null } } // 用connect方法连接HomePage组件,实际上是在HomePage组件上加上了Connect(HomePage)这个父组件,HomePage里有关Connect的state变化的props就是通过mapStateToProps设置的 // connect方法的第二个参数如果不传的话就会默认将store.dispatch默认作为了dispatch参数传进HomePage的props,所以HomePage的props里就有一个dispatch export default withRouter(HomePage) ================================================ FILE: src/containers/Login.js ================================================ import React, { Component, PropTypes } from 'react' import { connect } from 'react-redux' import {fetchAccess,fetchMessage,logout,fetchArticle,fetchProfile} from '../actions' import Header from '../components/common/Header/Header' import Profile from '../components/common/Profile/Profile' import getSize from '../utils/getSize' import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; import CircularProgress from 'material-ui/CircularProgress'; import RaisedButton from 'material-ui/RaisedButton'; import TextField from 'material-ui/TextField'; import Toggle from 'material-ui/Toggle'; class Login extends Component { constructor(){ super(); this.state = { toggleOn:true } } componentWillReceiveProps(newProps){ let {succeed,loginName,accessToken,dispatch,profile} = newProps; if(succeed && !profile.isFetching && profile.loginname !== loginName){ if(this.state.toggleOn && !window.localStorage.getItem('masterInfo')){ accessToken = accessToken.trim() loginName = loginName.trim() let masterInfo = {accessToken,loginName} masterInfo = JSON.stringify(masterInfo) window.localStorage.setItem('masterInfo',masterInfo) } dispatch(fetchProfile(loginName)) dispatch(fetchMessage(accessToken)) } } onToggle = () => { this.setState({ toggleOn:!this.state.toggleOn }) } render(){ let {dispatch,article,profile,failedMessage,succeed,loginName,loginId,accessToken,collectedTopics} = this.props; if(loginName !== profile.loginname && window.sessionStorage.masterProfile){ profile = JSON.parse(window.sessionStorage.masterProfile) collectedTopics = profile.collectedTopics } const masterInfo = window.localStorage.getItem('masterInfo') ? true : false return (
{!masterInfo && !succeed &&
{ const input = this.refs.input.input.value if(!input.trim()){ return null; } dispatch(fetchAccess(input)) }}/>
} {!succeed && failedMessage &&

{failedMessage}

} {succeed && !profile.loginname && } {succeed && profile.loginname &&
}
) } } function mapStateToProps(state) { const {article,profile,login,currentRouter} = state; const {failedMessage,succeed,loginName,loginId,accessToken} = login; const {collectedTopics} = profile return {article,profile,succeed,loginName,loginId,accessToken,failedMessage,collectedTopics} } // 用connect方法连接HomePage组件,实际上是在HomePage组件上加上了Connect(HomePage)这个父组件,HomePage里有关Connect的state变化的props就是通过mapStateToProps设置的 // connect方法的第二个参数如果不传的话就会默认将store.dispatch默认作为了dispatch参数传进HomePage的props,所以HomePage的props里就有一个dispatch export default connect(mapStateToProps)(Login) ================================================ FILE: src/containers/Message.js ================================================ import React, { Component, PropTypes } from 'react' import { connect } from 'react-redux' import {fetchMessage,fetchArticle} from '../actions' import Content from '../components/Message/Content/Content' import LinkToLogin from '../components/common/LinkToLogin/LinkToLogin' import Header from '../components/common/Header/Header' import getSize from '../utils/getSize' class Message extends Component { componentDidMount(){ const {login,dispatch,message} = this.props if(login.accessToken && message.hasReadMessage.length === 0){ dispatch(fetchMessage(login.accessToken)) } } render(){ const {dispatch,currentRouter,message,article,login} = this.props; return (
{login.succeed && } {!login.succeed && }
) } } function mapStateToProps(state) { const {login,message,article} = state; return {login,message,article} } export default connect(mapStateToProps)(Message) ================================================ FILE: src/containers/Profile.js ================================================ import React, { Component, PropTypes } from 'react' import { connect } from 'react-redux' import {fetchArticle} from '../actions' import Header from '../components/common/Header/Header' import ProfileComponent from '../components/common/Profile/Profile' import getSize from '../utils/getSize' class Profile extends Component { render(){ const {profile} = this.props return (
{profile.loginname &&
}
) } } // HomePage.propTypes = { // selectedTab: PropTypes.string.isRequired, // topics: PropTypes.array.isRequired, // isFetching: PropTypes.bool.isRequired, // page:PropTypes.number.isRequired, // scrollT:PropTypes.number.isRequired, // dispatch: PropTypes.func.isRequired // } function mapStateToProps(state) { const {profile,article} = state; const {collectedTopics} = profile; return {profile,article,collectedTopics} } export default connect(mapStateToProps)(Profile) ================================================ FILE: src/containers/PublishTopic.js ================================================ import React, { Component, PropTypes } from 'react' import { connect } from 'react-redux' import {Link} from 'react-router-dom' import prefix from '../utils/routePrefix' import {fetchArticle,fetchPublishTopic} from '../actions' import Header from '../components/common/Header/Header' import Dialog from '../components/common/Dialog' import LinkToLogin from '../components/common/LinkToLogin/LinkToLogin' import Form from '../components/PublishTopic/Form/Form' class PublishTopic extends Component { constructor(){ super(); this.state = { isOpen:false, isFetching: false, titleErr:false, contentErr:false } } componentWillReceiveProps(newProps){ const {publishTopic,dispatch} = newProps; if(!this.props.publishTopic.topicId || this.props.publishTopic.topicId !== publishTopic.topicId){ this.setState({isFetching:false}) dispatch(fetchArticle(publishTopic.topicId)) } } showDialog() { this.setState({ isOpen:true, isFetching:true }) } close = () => { this.setState({ isOpen:false }) } ifTitleErr(boolean) { this.setState({ titleErr: boolean }) } ifContentErr(boolean) { this.setState({ contentErr: boolean }) } render(){ const {dispatch,publishTopic,currentRouter,login} = this.props; const ifTitleErr = this.ifTitleErr.bind(this) const ifContentErr = this.ifContentErr.bind(this) const showDialog = this.showDialog.bind(this) const state = this.state return (
{login.succeed &&
} {!login.succeed && }
{state.isFetching &&
加载中
} {!state.isFetching &&
发送成功,去查看
}
) } } // HomePage.propTypes = { // selectedTab: PropTypes.string.isRequired, // topics: PropTypes.array.isRequired, // isFetching: PropTypes.bool.isRequired, // page:PropTypes.number.isRequired, // scrollT:PropTypes.number.isRequired, // dispatch: PropTypes.func.isRequired // } function mapStateToProps(state) { const {publishTopic,login} = state; // const {selectedTab,tabData} = state.homePage; // // 当组件第一次render时,还未进行任何action,初始化的state里没有tabData[selectedTab],所以这里需要初始化 // const {isFetching,page,scrollT,topics} = tabData[selectedTab] || {isFetching:false,page:0,scrollT:0,topics:[]} return {publishTopic,login} } // 用connect方法连接HomePage组件,实际上是在HomePage组件上加上了Connect(HomePage)这个父组件,HomePage里有关Connect的state变化的props就是通过mapStateToProps设置的 // connect方法的第二个参数如果不传的话就会默认将store.dispatch默认作为了dispatch参数传进HomePage的props,所以HomePage的props里就有一个dispatch export default connect(mapStateToProps)(PublishTopic) ================================================ FILE: src/index.js ================================================ import 'babel-polyfill'; import React from 'react'; import ReactDOM from 'react-dom' import store from './configureStore' import { Provider } from 'react-redux' // import { Router,hashHistory } from 'react-router' import Routes from './Routes' import './styles/index.css' import { AppContainer } from 'react-hot-loader' import injectTapEventPlugin from 'react-tap-event-plugin' injectTapEventPlugin() const render = (Component) => { ReactDOM.render( , document.getElementById('root') ) } render(Routes) if(module.hot) { module.hot.accept('./Routes', () => { render(Routes) }); } // ReactDOM.render( // // //
aaaaaaaa
//
//
, // document.getElementById('root') // ) ================================================ FILE: src/reducers/article.js ================================================ import { REQUEST_ARTICLE, RECEIVE_ARTICLE, CHANGE_CURRENT_TOPICID, SWITCH_SUPPORT, FETCH_COMMENT, RECORD_ARTICLE_SCROLLT } from '../actions' const initState = sessionStorage.getItem('store') ? JSON.parse(sessionStorage.getItem('store')).article : { currentTopicId: '' } const article = (state = initState, action) => { let stateItem = state[action.topicId] || {} switch (action.type) { case CHANGE_CURRENT_TOPICID: return { ...state, currentTopicId: action.topicId } case SWITCH_SUPPORT: return { ...state, switchSupportInfo: { replyId: action.replyId, index: action.index, success: action.success, action: action.action } } case FETCH_COMMENT: return { ...state, isCommented: action.success } case RECORD_ARTICLE_SCROLLT: stateItem = { ...stateItem, scrollT: action.scrollT } return { ...state, [action.topicId]: stateItem, currentTopicId: action.topicId } case REQUEST_ARTICLE: stateItem = { ...stateItem, isFetching: true } return { ...state, [action.topicId]: stateItem, currentTopicId: action.topicId, isCommented: false } case RECEIVE_ARTICLE: stateItem = { ...stateItem, isFetching: false, article: action.article } return { ...state, [action.topicId]: stateItem } default: return state } } export default article ================================================ FILE: src/reducers/collectedTopics.js ================================================ import { GET_COLLECTED_TOPICS } from '../actions' const initState = sessionStorage.getItem('store') ? JSON.parse(sessionStorage.getItem('store')).collectedTopics : { success: false } const collectedTopics = (state = initState, action) => { switch (action.type) { case GET_COLLECTED_TOPICS: return { ...state, success: action.success, data: action.data, userName: action.userName } default: return state } } export default collectedTopics ================================================ FILE: src/reducers/fetchError.js ================================================ import { FETCH_START, FETCH_END, FETCH_ERROR, CLEAR_ERROR } from '../actions/fetchError' const initState = sessionStorage.getItem('store') ? JSON.parse(sessionStorage.getItem('store')).fetchError : { error: null, fetched: null } const fetchError = (state = initState, action) => { switch (action.type) { case FETCH_START: return { ...state, fetched: 'start' } case FETCH_END: return { ...state, fetched: 'end' } case FETCH_ERROR: return { ...state, error: action.data, fetched: 'failed' } case CLEAR_ERROR: return { ...state, error: null } default: return state } } export default fetchError ================================================ FILE: src/reducers/hashUrl.js ================================================ import { SET_HASH_URL, SET_TRANSITION } from '../actions/hashUrl' const initState = sessionStorage.getItem('store') ? JSON.parse(sessionStorage.getItem('store')).hashUrl : { oldUrl: '/', currentUrl: '/', transition: 'none' } const hashUrl = (state = initState, action) => { switch (action.type) { case SET_HASH_URL: case SET_TRANSITION: return { ...state, ...action.data } default: return state } } export default hashUrl; ================================================ FILE: src/reducers/homePage.js ================================================ import { SELECT_TAB, RECORD_SCROLLT, REQUEST_TOPICS, RECEIVE_TOPICS } from '../actions' const selectedTab = (state, action) => { switch (action.type) { case SELECT_TAB: return action.tab default: return state } } // 当组件第一次发出REQUEST_TOPICS后,需要对其返回的state进行初始化,否则没有topics等属性会报错 function tabDataItem(state = { isFetching: false, page: 0, scrollT: 0, topics: [] }, action) { switch (action.type) { case REQUEST_TOPICS: return { ...state, isFetching: true } case RECEIVE_TOPICS: if (state.page < action.page) { let topics = state.topics action.topics = topics.concat(action.topics) } return { ...state, isFetching: false, page: action.page, topics: action.topics, limit: action.limit } case RECORD_SCROLLT: return { ...state, scrollT: action.scrollT } default: return state } } const tabData = (state = {}, action) => { switch (action.type) { case RECEIVE_TOPICS: case REQUEST_TOPICS: case RECORD_SCROLLT: return { ...state, [action.tab]: tabDataItem(state[action.tab], action) } default: return state } } const initState = sessionStorage.getItem('store') ? JSON.parse(sessionStorage.getItem('store')).homePage : { selectedTab: 'all', tabData: {} } const homePage = (state = initState, action) => { if (state) { const sTab = selectedTab(state.selectedTab, action); const tData = tabData(state.tabData, action); // 返回的一定要是一个新的对象,如果只是改变原来的state,返回的还是原来的state对象,就无法被store.subscribe监听到,从而不会对组件状态进行更新 return { ...state, selectedTab: sTab, tabData: tData } } return state } export default homePage; ================================================ FILE: src/reducers/index.js ================================================ import { combineReducers } from 'redux' import homePage from './homePage' import article from './article' import login from './login' import profile from './profile' import message from './message' import publishTopic from './publishTopic' import hashUrl from './hashUrl' import fetchError from './fetchError' const rootReducer = combineReducers({ homePage, article, login, profile, publishTopic, message, hashUrl, fetchError }) export default rootReducer ================================================ FILE: src/reducers/login.js ================================================ import { LOGIN_SUCCESS, LOGIN_FAILED, LOGOUT } from '../actions' const initState = sessionStorage.getItem('store') ? JSON.parse(sessionStorage.getItem('store')).login : { succeed: false } const login = (state = initState, action) => { switch (action.type) { case LOGIN_SUCCESS: return { ...state, succeed: true, loginName: action.loginName, loginId: action.loginId, accessToken: action.accessToken } case LOGIN_FAILED: return { ...state, succeed: false, failedMessage: action.failedMessage } case LOGOUT: return { succeed: false } default: return state } } export default login ================================================ FILE: src/reducers/message.js ================================================ import { FETCH_MESSAGE, MARK_ALL_MESSAGES } from '../actions' const initState = sessionStorage.getItem('store') ? JSON.parse(sessionStorage.getItem('store')).message : { isMarked: false, hasReadMessage: [], hasNotReadMessage: [] } const message = (state = initState, action) => { switch (action.type) { case FETCH_MESSAGE: return { ...state, hasReadMessage: action.hasReadMessage, hasNotReadMessage: action.hasNotReadMessage } case MARK_ALL_MESSAGES: return { ...state, isMarked: action.isMarked } default: return state } } export default message ================================================ FILE: src/reducers/profile.js ================================================ import { REQUEST_PROFILE, RECEIVE_PROFILE, GET_COLLECTED_TOPICS } from '../actions' const initState = sessionStorage.getItem('store') ? JSON.parse(sessionStorage.getItem('store')).profile : { isFetching: false, collectedTopics: [] } const profile = (state = initState, action) => { switch (action.type) { case REQUEST_PROFILE: return { ...state, isFetching: true } case RECEIVE_PROFILE: return { ...state, ...action.profile, isFetching: false } case GET_COLLECTED_TOPICS: return { ...state, collectedTopics: action.data } default: return state } } export default profile ================================================ FILE: src/reducers/publishTopic.js ================================================ import { PUBLISH_TOPIC } from '../actions' const initState = sessionStorage.getItem('store') ? JSON.parse(sessionStorage.getItem('store')).publishTopic : { success: false } const publishTopic = (state = initState, action) => { switch (action.type) { case PUBLISH_TOPIC: return { ...state, success: action.success, topicId: action.topicId } default: return state } } export default publishTopic ================================================ FILE: src/routes.js ================================================ import React, { Component } from 'react' import { Route, Router, Redirect, Switch, withRouter } from 'react-router-dom' import { connect } from 'react-redux' import createHistory from 'history/createHashHistory' import lazyLoadComponent from 'lazy-load-component' import App from './containers/App' import HomePage from './containers/HomePage' import Article from './containers/Article' import Message from './containers/Message' import Login from './containers/Login' import Profile from './containers/Profile' import PublishTopic from './containers/PublishTopic' import prefix from './utils/routePrefix' import getSize from './utils/getSize' import { setHashUrl, setTransition } from './actions/hashUrl' // import { clearUserInfo } from './actions/login' import { clearError } from './actions/fetchError' import { CSSTransition, TransitionGroup, Transition } from 'react-transition-group' const history = createHistory() // const Article = lazyLoadComponent(() => import(/*webpackChunkName:"Article" */'./containers/Article')) // const Message = lazyLoadComponent(() => import(/*webpackChunkName:"Message" */'./containers/Message')) // const Login = lazyLoadComponent(() => import(/*webpackChunkName:"Login" */'./containers/Login')) // const Profile = lazyLoadComponent(() => import(/*webpackChunkName:"Profile" */'./containers/Profile')) // const PublishTopic = lazyLoadComponent(() => import(/*webpackChunkName:"PublishTopic" */'./containers/PublishTopic')) @connect(store => ({ store })) class Routes extends Component { constructor() { super() this.state = {} } hashChange = (ev) => { if (this.props.store.hashUrl.oldURL != '/') { this.setState({ overflow: 'hidden' }) setTimeout(() => this.setState({ overflow: 'visible' }), 500) } const dispatch = this.props.dispatch let hashUrl = null; if (ev.oldURL) { hashUrl = { oldUrl: ev.oldURL.split('#')[1], currentUrl: ev.newURL.split('#')[1] } } else { this.oldUrl = this.currentUrl this.currentUrl = window.location.href.split('#')[1] hashUrl = { oldUrl: this.oldUrl, currentUrl: this.currentUrl } } dispatch(setHashUrl(hashUrl)) // if (this.props.hashUrl.transition != 'none') { // clearTimeout(this.transitionTimeOut) // this.transitionTimeOut = setTimeout(() => { // dispatch(setTransition({ transition: 'none' })) // }, 50) // } } oldUrl = '/' currentUrl = '/' // changeTransition = (transition) => { // this.setState({ transition: transition }) // } componentWillMount() { let dispatch = this.props.dispatch window.myDispatch = dispatch window.width = getSize().windowW window.height = getSize().windowH let menu = window.location.href.split('#')[1].split('/') if (menu[1]) { this.currentUrl = window.location.href.split('#')[1] dispatch(setHashUrl({ oldUrl: this.oldUrl, currentUrl: window.location.href.split('#')[1] })) } else { dispatch(setHashUrl({ oldUrl: this.oldUrl, currentUrl: this.currentUrl })) } window.addEventListener('hashchange', this.hashChange) // 由于头部组件fix定位,在路由切换时,width:100%在手机上的判定会有问题,暂时采取全局变量储存页面加载时的宽度 // window.width = document.getElementById('root').offsetWidth // console.log('****************',document.getElementById('root').offsetWidth) } saveState = () => { let store = this.props.store sessionStorage.setItem('store', JSON.stringify(store)) } componentWillUpdate(nextProps) { // if (this.props.store.hashUrl.oldUrl == nextProps.store.hashUrl.currentUrl) { // this.props.dispatch(setTransition({ transition: 'left' })) // } } render() { this.transition = this.props.store.hashUrl.transition // let transition = this.props.store.hashUrl.transition return ( (
this.enterCN = `${this.transition}-enter`} onEntering={() => this.enterCN = `${this.transition}-enter ${this.transition}-enter-active`} onEntered={() => this.enterCN = ``} onExit={() => this.exitCN = `${this.transition}-exit`} onExiting={() => this.exitCN = `${this.transition}-exit ${this.transition}-exit-active`} onExited={() => this.exitCN = ``} > {(status) => (
} /> } />
} /> } /> } /> } /> } />
)}
)} />
) } componentDidMount() { window.width = document.getElementById('root').offsetWidth window.addEventListener('beforeunload', this.saveState) } componentWillUnmount() { window.removeEventListener('beforeunload', this.saveState) window.removeEventListener('hashchange', () => { }) } } export default Routes ================================================ FILE: src/styles/iconfont/demo.css ================================================ *{margin: 0;padding: 0;list-style: none;} /* KISSY CSS Reset 理念:1. reset 的目的不是清除浏览器的默认样式,这仅是部分工作。清除和重置是紧密不可分的。 2. reset 的目的不是让默认样式在所有浏览器下一致,而是减少默认样式有可能带来的问题。 3. reset 期望提供一套普适通用的基础样式。但没有银弹,推荐根据具体需求,裁剪和修改后再使用。 特色:1. 适应中文;2. 基于最新主流浏览器。 维护:玉伯, 正淳 */ /** 清除内外边距 **/ body, h1, h2, h3, h4, h5, h6, hr, p, blockquote, /* structural elements 结构元素 */ dl, dt, dd, ul, ol, li, /* list elements 列表元素 */ pre, /* text formatting elements 文本格式元素 */ form, fieldset, legend, button, input, textarea, /* form elements 表单元素 */ th, td /* table elements 表格元素 */ { margin: 0; padding: 0; } /** 设置默认字体 **/ body, button, input, select, textarea /* for ie */ { font: 12px/1.5 tahoma, arial, \5b8b\4f53, sans-serif; } h1, h2, h3, h4, h5, h6 { font-size: 100%; } address, cite, dfn, em, var { font-style: normal; } /* 将斜体扶正 */ code, kbd, pre, samp { font-family: courier new, courier, monospace; } /* 统一等宽字体 */ small { font-size: 12px; } /* 小于 12px 的中文很难阅读,让 small 正常化 */ /** 重置列表元素 **/ ul, ol { list-style: none; } /** 重置文本格式元素 **/ a { text-decoration: none; } a:hover { text-decoration: underline; } /** 重置表单元素 **/ legend { color: #000; } /* for ie6 */ fieldset, img { border: 0; } /* img 搭车:让链接里的 img 无边框 */ button, input, select, textarea { font-size: 100%; } /* 使得表单元素在 ie 下能继承字体大小 */ /* 注:optgroup 无法扶正 */ /** 重置表格元素 **/ table { border-collapse: collapse; border-spacing: 0; } /* 清除浮动 */ .ks-clear:after, .clear:after { content: '\20'; display: block; height: 0; clear: both; } .ks-clear, .clear { *zoom: 1; } .main { padding: 30px 100px; width: 960px; margin: 0 auto; } .main h1{font-size:36px; color:#333; text-align:left;margin-bottom:30px; border-bottom: 1px solid #eee;} .helps{margin-top:40px;} .helps pre{ padding:20px; margin:10px 0; border:solid 1px #e7e1cd; background-color: #fffdef; overflow: auto; } .icon_lists{ width: 100% !important; } .icon_lists li{ float:left; width: 100px; height:180px; text-align: center; list-style: none !important; } .icon_lists .icon{ font-size: 42px; line-height: 100px; margin: 10px 0; color:#333; -webkit-transition: font-size 0.25s ease-out 0s; -moz-transition: font-size 0.25s ease-out 0s; transition: font-size 0.25s ease-out 0s; } .icon_lists .icon:hover{ font-size: 100px; } .markdown { color: #666; font-size: 14px; line-height: 1.8; } .highlight { line-height: 1.5; } .markdown img { vertical-align: middle; max-width: 100%; } .markdown h1 { color: #404040; font-weight: 500; line-height: 40px; margin-bottom: 24px; } .markdown h2, .markdown h3, .markdown h4, .markdown h5, .markdown h6 { color: #404040; margin: 1.6em 0 0.6em 0; font-weight: 500; clear: both; } .markdown h1 { font-size: 28px; } .markdown h2 { font-size: 22px; } .markdown h3 { font-size: 16px; } .markdown h4 { font-size: 14px; } .markdown h5 { font-size: 12px; } .markdown h6 { font-size: 12px; } .markdown hr { height: 1px; border: 0; background: #e9e9e9; margin: 16px 0; clear: both; } .markdown p, .markdown pre { margin: 1em 0; } .markdown > p, .markdown > blockquote, .markdown > .highlight, .markdown > ol, .markdown > ul { width: 80%; } .markdown ul > li { list-style: circle; } .markdown > ul li, .markdown blockquote ul > li { margin-left: 20px; padding-left: 4px; } .markdown > ul li p, .markdown > ol li p { margin: 0.6em 0; } .markdown ol > li { list-style: decimal; } .markdown > ol li, .markdown blockquote ol > li { margin-left: 20px; padding-left: 4px; } .markdown code { margin: 0 3px; padding: 0 5px; background: #eee; border-radius: 3px; } .markdown pre { border-radius: 6px; background: #f7f7f7; padding: 20px; } .markdown pre code { border: none; background: #f7f7f7; margin: 0; } .markdown strong, .markdown b { font-weight: 600; } .markdown > table { border-collapse: collapse; border-spacing: 0px; empty-cells: show; border: 1px solid #e9e9e9; width: 95%; margin-bottom: 24px; } .markdown > table th { white-space: nowrap; color: #333; font-weight: 600; } .markdown > table th, .markdown > table td { border: 1px solid #e9e9e9; padding: 8px 16px; text-align: left; } .markdown > table th { background: #F7F7F7; } .markdown blockquote { font-size: 90%; color: #999; border-left: 4px solid #e9e9e9; padding-left: 0.8em; margin: 1em 0; font-style: italic; } .markdown blockquote p { margin: 0; } .markdown .anchor { opacity: 0; transition: opacity 0.3s ease; margin-left: 8px; } .markdown .waiting { color: #ccc; } .markdown h1:hover .anchor, .markdown h2:hover .anchor, .markdown h3:hover .anchor, .markdown h4:hover .anchor, .markdown h5:hover .anchor, .markdown h6:hover .anchor { opacity: 1; display: inline-block; } .markdown > br, .markdown > p > br { clear: both; } .hljs { display: block; background: white; padding: 0.5em; color: #333333; overflow-x: auto; } .hljs-comment, .hljs-meta { color: #969896; } .hljs-string, .hljs-variable, .hljs-template-variable, .hljs-strong, .hljs-emphasis, .hljs-quote { color: #df5000; } .hljs-keyword, .hljs-selector-tag, .hljs-type { color: #a71d5d; } .hljs-literal, .hljs-symbol, .hljs-bullet, .hljs-attribute { color: #0086b3; } .hljs-section, .hljs-name { color: #63a35c; } .hljs-tag { color: #333333; } .hljs-title, .hljs-attr, .hljs-selector-id, .hljs-selector-class, .hljs-selector-attr, .hljs-selector-pseudo { color: #795da3; } .hljs-addition { color: #55a532; background-color: #eaffea; } .hljs-deletion { color: #bd2c00; background-color: #ffecec; } .hljs-link { text-decoration: underline; } pre{ background: #fff; } ================================================ FILE: src/styles/iconfont/demo_fontclass.html ================================================ IconFont

IconFont 图标

  • 喜爱
    .icon-xiai
  • .icon-ding
  • 用户
    .icon-user
  • 返回
    .icon-back
  • 信息
    .icon-informatiom
  • 回复
    .icon-huifu
  • 查看过
    .icon-chakanguo

font-class引用


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

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

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

使用步骤如下:

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

<link rel="stylesheet" type="text/css" href="./iconfont.css">

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

<i class="iconfont icon-xxx"></i>

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

================================================ FILE: src/styles/iconfont/demo_symbol.html ================================================ IconFont

IconFont 图标

  • 喜爱
    #icon-xiai
  • #icon-ding
  • 用户
    #icon-user
  • 返回
    #icon-back
  • 信息
    #icon-informatiom
  • 回复
    #icon-huifu
  • 查看过
    #icon-chakanguo

symbol引用


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

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

使用步骤如下:

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

<script src="./iconfont.js"></script>

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

<style type="text/css">
.icon {
   width: 1em; height: 1em;
   vertical-align: -0.15em;
   fill: currentColor;
   overflow: hidden;
}
</style>

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

<svg class="icon" aria-hidden="true">
  <use xlink:href="#icon-xxx"></use>
</svg>
        
================================================ FILE: src/styles/iconfont/demo_unicode.html ================================================ IconFont

IconFont 图标

  • 喜爱
    &#xe600;
  • &#xe610;
  • 用户
    &#xe60f;
  • 返回
    &#xe611;
  • 信息
    &#xe617;
  • 回复
    &#xe63f;
  • 查看过
    &#xe6ae;

unicode引用


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

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

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

unicode使用步骤如下:

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

@font-face {
  font-family: 'iconfont';
  src: url('iconfont.eot');
  src: url('iconfont.eot?#iefix') format('embedded-opentype'),
  url('iconfont.woff') format('woff'),
  url('iconfont.ttf') format('truetype'),
  url('iconfont.svg#iconfont') format('svg');
}

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

.iconfont{
  font-family:"iconfont" !important;
  font-size:16px;font-style:normal;
  -webkit-font-smoothing: antialiased;
  -webkit-text-stroke-width: 0.2px;
  -moz-osx-font-smoothing: grayscale;
}

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

<i class="iconfont">&#x33;</i>

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

================================================ FILE: src/styles/iconfont/iconfont.css ================================================ @font-face {font-family: "iconfont"; src: url('iconfont.eot?t=1481708482838'); /* IE9*/ src: url('iconfont.eot?t=1481708482838#iefix') format('embedded-opentype'), /* IE6-IE8 */ url('iconfont.woff?t=1481708482838') format('woff'), /* chrome, firefox */ url('iconfont.ttf?t=1481708482838') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ url('iconfont.svg?t=1481708482838#iconfont') format('svg'); /* iOS 4.1- */ } .iconfont { font-family:"iconfont" !important; font-size:16px; font-style:normal; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .icon-xiai:before { content: "\e600"; } .icon-ding:before { content: "\e610"; } .icon-user:before { content: "\e60f"; } .icon-back:before { content: "\e611"; } .icon-informatiom:before { content: "\e617"; } .icon-huifu:before { content: "\e63f"; } .icon-chakanguo:before { content: "\e6ae"; } ================================================ FILE: src/styles/iconfont/iconfont.js ================================================ ;(function(window) { var svgSprite = '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' var script = function() { var scripts = document.getElementsByTagName('script') return scripts[scripts.length - 1] }() var shouldInjectCss = script.getAttribute("data-injectcss") /** * document ready */ var ready = function(fn) { if (document.addEventListener) { if (~["complete", "loaded", "interactive"].indexOf(document.readyState)) { setTimeout(fn, 0) } else { var loadFn = function() { document.removeEventListener("DOMContentLoaded", loadFn, false) fn() } document.addEventListener("DOMContentLoaded", loadFn, false) } } else if (document.attachEvent) { IEContentLoaded(window, fn) } function IEContentLoaded(w, fn) { var d = w.document, done = false, // only fire once init = function() { if (!done) { done = true fn() } } // polling for no errors var polling = function() { try { // throws errors until after ondocumentready d.documentElement.doScroll('left') } catch (e) { setTimeout(polling, 50) return } // no errors, fire init() }; polling() // trying to always fire before onload d.onreadystatechange = function() { if (d.readyState == 'complete') { d.onreadystatechange = null init() } } } } /** * Insert el before target * * @param {Element} el * @param {Element} target */ var before = function(el, target) { target.parentNode.insertBefore(el, target) } /** * Prepend el to target * * @param {Element} el * @param {Element} target */ var prepend = function(el, target) { if (target.firstChild) { before(el, target.firstChild) } else { target.appendChild(el) } } function appendSvg() { var div, svg div = document.createElement('div') div.innerHTML = svgSprite svgSprite = null svg = div.getElementsByTagName('svg')[0] if (svg) { svg.setAttribute('aria-hidden', 'true') svg.style.position = 'absolute' svg.style.width = 0 svg.style.height = 0 svg.style.overflow = 'hidden' prepend(svg, document.body) } } if (shouldInjectCss && !window.__iconfont__svg__cssinject__) { window.__iconfont__svg__cssinject__ = true try { document.write(""); } catch (e) { console && console.log(e) } } ready(appendSvg) })(window) ================================================ FILE: src/styles/index.css ================================================ /*Normalize.css 只是一个很小的CSS文件,但它在默认的HTML元素样式上提供了跨浏览器的高度一致性*/ @import '~normalize.css/normalize.css'; @import "~github-markdown-css/github-markdown.css"; @font-face {font-family: "iconfont"; src: url('iconfont/iconfont.eot'); /* IE9*/ src: url('iconfont/iconfont.eot#iefix') format('embedded-opentype'), /* IE6-IE8 */ url('iconfont/iconfont.woff') format('woff'), /* chrome, firefox */ url('iconfont/iconfont.ttf') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ url('iconfont/iconfont.svg#iconfont') format('svg'); /* iOS 4.1- */ } .iconfont { font-family:"iconfont" !important; font-size:20px; font-style:normal; -webkit-font-smoothing: antialiased; -webkit-text-stroke-width: 0.2px; -moz-osx-font-smoothing: grayscale; } body,h2,p,ul,li{ margin:0; padding:0; } ul{ padding-left:0; } li{ list-style-type: none; } a:hover,a:link,a:visited,a:active{ text-decoration: none; } /* .fade-in{ animation: fadeIn 0.5s ease-out; } @keyframes fadeIn{ 0%{opacity:0;} 100%{opacity:1;} } */ /* .move-appear { transform: scale(50); background: grey; } .move-appear.move-appear-active { transform: scale(1); background: grey; transition: 60s; } */ .move-enter { position: absolute; z-index: 100; width: 100%; height: 100%; background: white; top: 0; left: 100%; } .move-enter.move-enter-active { left: 0%; transition: 0.5s; } .move-exit { position: absolute; z-index: 50; width: 100%; height: 100%; opacity: 50; background: white; top: 0; left: 0; } .move-exit.move-exit-active { opacity: 50; left: -100%; transition: 0.5s; } .left-enter { position: absolute; z-index: 100; width: 100%; height: 100%; background: white; top: 0; left: 0; } .left-enter.left-enter-active { left: 0; } .left-exit { position: absolute; z-index: 100; width: 100%; height: 100%; background: white; top: 0; left: 0; } .left-exit.left-exit-active { left: 100%; transition: 0.5s; } .up-enter { position: absolute; z-index: 10000; width: 100%; height: 100%; background: white; top: 100%; left: 0; } .up-enter.up-enter-active { top: 0; transition: 0.5s; } .up-exit { position: absolute; z-index: 100; width: 100%; height: 100%; background: white; top: 0; left: 0; } .up-exit.up-exit-active { top: 0; } ================================================ FILE: src/utils/getOS.js ================================================ export const os = 'win32';export const host = '192.168.6.183' ================================================ FILE: src/utils/getPosition.js ================================================ const getPosition = (direction, DOMNode, className) => { switch(document.getElementsByClassName(className).length){ case 0: alert('注意:传入的className对应的元素不存在!') break case 1: break default: alert('注意:传入的className对应多个元素!请给该元素唯一className') } let offset = DOMNode[`offset${direction}`] let parent = DOMNode.offsetParent if (parent) { if (className) { if (parent.className != className) { offset += getPosition(direction, parent, className); } } else { if (parent.nodeName != 'BODY') { offset += getPosition(direction, parent); } } } return offset } export const getTop = (DOMNode, className) => { return getPosition('Top', DOMNode, className) } export const getLeft = (DOMNode, className) => { return getPosition('Left', DOMNode, className) } export const getBottom = (DOMNode, className) => { let containerHeight = 0; let selfHeight = DOMNode.offsetHeight if (className) { containerHeight = document.getElementsByClassName(className)[0].offsetHeight } else { containerHeight = document.getElementsByTagName('body')[0].offsetHeight } return containerHeight - selfHeight - getTop(DOMNode, className) } export const getRight = (DOMNode, className) => { let containerWidth = 0; let selfWidth = DOMNode.offsetWidth if (className) { containerWidth = document.getElementsByClassName(className)[0].offsetWidth } else { containerWidth = document.getElementsByTagName('body')[0].offsetWidth } return containerWidth - selfWidth - getLeft(DOMNode, className) } ================================================ FILE: src/utils/getSize.js ================================================ const getSize = () => { let windowW,windowH,contentH,contentW,scrollT; windowH = window.innerHeight; windowW = window.innerWidth; scrollT = document.documentElement.scrollTop || document.body.scrollTop; contentH = (document.documentElement.scrollHeight > document.body.scrollHeight) ? document.documentElement.scrollHeight : document.body.scrollHeight; contentW = (document.documentElement.scrollWidth > document.body.scrollWidth) ? document.documentElement.scrollWidth : document.body.scrollWidth; return {windowW,windowH,contentH,contentW,scrollT} } export default getSize; ================================================ FILE: src/utils/getStrLength.js ================================================ // GBK字符集实际长度计算 const getStrLength = str => { let realLength = 0; let len = str.length; let charCode = -1; for(let i = 0; i < len; i++){ charCode = str.charCodeAt(i); if (charCode >= 0 && charCode <= 128) { realLength += 1; }else{ // 如果是中文则长度加2 realLength += 2; } } return realLength; } export default getStrLength ================================================ FILE: src/utils/myFetch.js ================================================ import originFetch from 'isomorphic-fetch' import { fetchError, fetchStart, fetchEnd } from '../actions/fetchError' const getData = async (url, option) => { try { let response = await originFetch(url, option) if (response.ok) { return response } else { let error = await response.json() throw error } } catch (error) { myDispatch(fetchError(error)) } return new Promise(() => { }) } const myFetch = (url, option) => { let myOption = { credentials: 'include', headers: { "Content-type": "application/json;charset=UTF-8" } } option = option || {} option = { ...myOption, ...option } // 方案1 // return new Promise((resolve, reject) => { // originFetch(url, option) // .then(response => { // if (response.ok) { // resolve(response) // } else { // response.json().then(json => { // //response的then里产生或抛出的异常,无法直接抛出到该链式调用外面去被originFetch的catch捕获,除非获取到了originFetch的reject // reject(json)//直接拿到这个return的new Promise的reject方法,能被它的catch方法(即最外面那个catch方法)捕捉到 // }) // } // }) // .catch(error => myDispatch(fetchError(error)))//获取originFetch链式调用里产生的异常,这里能获取到的是fetch请求失败,无返回response,服务器无响应的异常 // }) // .catch(error => { //获取的是reject(json)里的json数据 // myDispatch(fetchError(error)) // return new Promise(() => { }) //catch后面如果有then,then里面的方法执行可能会报错,要想其不执行的办法是返回一个promise对象携带空的方法 // }) //方案2 // return new Promise((resolve, reject) => { // originFetch(url, option) // .then(response => { // if (response.ok) { // resolve(response) // } else { // response.json().then(json => { // throw json // }) // .catch(error => myDispatch(fetchError(error)))//也可以直接在这里获取response里抛出的错误 // //promise对象如果不调用resolve方法,后面的then链路里的函数是不会被执行的 // } // }) // .catch(error => myDispatch(fetchError(error)))//获取originFetch链式调用里产生的异常,这里能获取到的是fetch请求失败,无返回response,服务器无响应的异常 // }) //方案3 //getData方法在最上面定义 return getData(url, option) } export default myFetch ================================================ FILE: src/utils/routePrefix.js ================================================ // 使用browserHistory需要进行判断,在生产环境下,如果编译的文件不是根目录文件,而是在子文件夹内,子文件夹的地址部分会被browserHistory解析成路由 // 比如:编译的文件放在站点www文件夹的cnode文件夹里,访问时用github.com/cnode/,但是此时cnode/被browserHistory解析成路由,这个路由不存在所以会出问题 // let prefix = process.env.NODE_ENV === 'production' ? '/cnode' : ''; // 使用hashHistory则不需要这样的判断,因为router被放在hash中,而浏览器能正确解析hash let prefix = ''; export default prefix; ================================================ FILE: src/utils/transformDate.js ================================================ export default function (date) { var createAt = new Date(date); var time = new Date().getTime() - createAt.getTime(); //现在的时间-传入的时间 = 相差的时间(单位 = 毫秒) if (time < 0) { return ''; } else if (time / 1000 < 60) { return '刚刚'; } else if ((time / 60000) < 60) { return parseInt((time / 60000)) + '分钟前'; } else if ((time / 3600000) < 24) { return parseInt(time / 3600000) + '小时前'; } else if ((time / 86400000) < 31) { return parseInt(time / 86400000) + '天前'; } else if ((time / 2592000000) < 12) { return parseInt(time / 2592000000) + '月前'; } else { return parseInt(time / 31536000000) + '年前'; } } ================================================ FILE: src/utils/urlPrefix.js ================================================ import { os, host } from './getOS' let prefix = os == 'win32' ? `http://${host}:8081` : '/api' // let prefix = 'http://192.168.30.90:8080/s'; // let prefix = '/api' export default prefix ================================================ FILE: webpack.config.js ================================================ var path = require('path'); var webpack = require('webpack') var autoprefixer = require('autoprefixer') var ExtractTextPlugin = require('extract-text-webpack-plugin') var redBox = require('redbox-react') var host = 'localhost' var port = '5678' //判断当前运行环境是开发模式还是生产模式 const nodeEnv = process.env.NODE_ENV || 'development'; const isPro = nodeEnv !== 'development'; const os = require('os').platform() const network = require('os').networkInterfaces() console.log("当前运行系统:", os) console.log("当前运行环境:", isPro ? 'production' : 'development') // Object.keys(network).forEach(item => { // if (network[item] && network[item][0] && network[item][0].internal == false && ['以太网', '本地连接'].includes(item)) { // network[item].forEach(ips => { // if (ips.family == 'IPv4') { // host = ips.address // } // }) // } // }) // var fs = require("fs") // var data = `export const os = '${os}';export const host = '${host}'` // var writerStream = fs.createWriteStream('src/utils/getOS.js') // writerStream.write(data, 'UTF8') // writerStream.end() // writerStream.on('finish', () => { // console.log("成功写入:src/utils/getOS.js") // }) // writerStream.on('error', err => { // console.log(err.stack) // }) const moduleCss = new ExtractTextPlugin('module.css') const globalCss = new ExtractTextPlugin('global.css') const uiCss = new ExtractTextPlugin('ui.css') var plugins = [ globalCss, moduleCss, uiCss, // 这个插件的作用是将打包的js文件拆分,默认拆分出2个 new webpack.optimize.CommonsChunkPlugin({ // 这个vendor就是打包后的文件名字,需要写在index.html里面 name: 'vendor', minChunks: function (module) { // 该配置假定你引入的 vendor 存在于 node_modules 目录中 return module.context && module.context.indexOf('node_modules') !== -1; } }), //DefinePlugin 在原始的源码中执行查找和替换操作,在导入的代码中, // 任何出现 process.env.NODE_ENV的地方都会被替换为nodeEnv变量转成的json字符串 new webpack.DefinePlugin({ // 定义全局变量 'process.env': { 'NODE_ENV': JSON.stringify(nodeEnv) } }) ] var app = ['./index.js'] if (isPro) { plugins.push( new webpack.optimize.UglifyJsPlugin({ sourceMap: true, comments: false, compress: { warnings: false, // 去掉debugger和console drop_debugger: true, drop_console: true } }) ) } else { app.unshift('react-hot-loader/patch', `webpack-dev-server/client?http://${host}:${port}`, 'webpack/hot/only-dev-server') plugins.push( new webpack.HotModuleReplacementPlugin(), new webpack.NamedModulesPlugin(), new webpack.NoEmitOnErrorsPlugin() ) } // // 开启服务器后不能用相对路径 // var outpath = path.resolve(__dirname, 'build'); module.exports = { context: path.resolve(__dirname, 'src'), devtool: isPro ? 'source-map' : 'inline-source-map', entry: { app: app }, output: { filename: '[name].js', path: path.join(__dirname, 'build'), publicPath: isPro ? './build/' : '/build/', chunkFilename: '[name].js' }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: ['babel-loader?cacheDirectory=true'] }, // { // test: /\.scss$/, // // exclude: [nodeModulesPath]用来排除不处理的目录 // exclude: path.resolve(__dirname, 'src/styles'), // // webpack配置loader时是可以不写loader的后缀明-loader,因此css-loader可以写为css // // css-loader使你能够使用类似@import 和 url(...)的方法实现 require()的功能 // // style-loader将所有的计算后的样式通过