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 (
{
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) => (
{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 (
)
}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 (
)
}
}
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 (
);
}
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}
积分:{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 (
)
}
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 (
)
}
}
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 (
)
}
}
// 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 && }
)
}
}
// 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代码:
第二步:挑选相应图标并获取类名,应用于页面:
<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代码:
第二步:加入通用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 图标
-
喜爱

-
顶

-
用户

-
返回

-
信息

-
回复

-
查看过

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">3</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将所有的计算后的样式通过